Add sourcegen support for required & init-only properties. (#79828)
authorEirik Tsarpalis <eirik.tsarpalis@gmail.com>
Mon, 9 Jan 2023 20:04:42 +0000 (20:04 +0000)
committerGitHub <noreply@github.com>
Mon, 9 Jan 2023 20:04:42 +0000 (20:04 +0000)
* Add sourcegen support for required & init-only properties.

* Add test coverage for required fields & remove a few async void methods.

* Remove commented out code.

* Tweak JsonSerializerOptions resolution logic in wrapper implementation

42 files changed:
src/libraries/System.Text.Json/System.Text.Json.sln
src/libraries/System.Text.Json/gen/JsonSourceGenerator.Emitter.ExceptionMessages.cs
src/libraries/System.Text.Json/gen/JsonSourceGenerator.Emitter.cs
src/libraries/System.Text.Json/gen/JsonSourceGenerator.Parser.cs
src/libraries/System.Text.Json/gen/ParameterGenerationSpec.cs
src/libraries/System.Text.Json/gen/PropertyGenerationSpec.cs
src/libraries/System.Text.Json/gen/PropertyInitializerGenerationSpec.cs [new file with mode: 0644]
src/libraries/System.Text.Json/gen/Reflection/FieldInfoWrapper.cs
src/libraries/System.Text.Json/gen/Reflection/PropertyInfoWrapper.cs
src/libraries/System.Text.Json/gen/Reflection/ReflectionExtensions.cs
src/libraries/System.Text.Json/gen/Reflection/TypeExtensions.cs
src/libraries/System.Text.Json/gen/System.Text.Json.SourceGeneration.targets
src/libraries/System.Text.Json/gen/TypeGenerationSpec.cs
src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/JsonMetadataServicesConverter.cs
src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializerContext.cs
src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializerOptions.cs
src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/JsonTypeInfo.cs
src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/JsonTypeInfoOfT.WriteHelpers.cs
src/libraries/System.Text.Json/tests/Common/JsonSerializerWrapper.cs
src/libraries/System.Text.Json/tests/Common/ReferenceHandlerTests/ReferenceHandlerTests.Deserialize.cs
src/libraries/System.Text.Json/tests/Common/ReferenceHandlerTests/ReferenceHandlerTests.IgnoreCycles.cs
src/libraries/System.Text.Json/tests/Common/RequiredKeywordTests.cs [new file with mode: 0644]
src/libraries/System.Text.Json/tests/System.Text.Json.SourceGeneration.TestLibrary/System.Text.Json.TestLibrary.Roslyn4.4.csproj [moved from src/libraries/System.Text.Json/tests/System.Text.Json.SourceGeneration.TestLibrary/System.Text.Json.TestLibrary.Roslyn4.0.csproj with 77% similarity]
src/libraries/System.Text.Json/tests/System.Text.Json.SourceGeneration.Tests/Serialization/CollectionTests.cs
src/libraries/System.Text.Json/tests/System.Text.Json.SourceGeneration.Tests/Serialization/ConstructorTests.cs
src/libraries/System.Text.Json/tests/System.Text.Json.SourceGeneration.Tests/Serialization/ExtensionDataTests.cs
src/libraries/System.Text.Json/tests/System.Text.Json.SourceGeneration.Tests/Serialization/JsonSerializerWrapper.SourceGen.cs
src/libraries/System.Text.Json/tests/System.Text.Json.SourceGeneration.Tests/Serialization/NodeInteropTests.cs
src/libraries/System.Text.Json/tests/System.Text.Json.SourceGeneration.Tests/Serialization/PropertyNameTests.cs
src/libraries/System.Text.Json/tests/System.Text.Json.SourceGeneration.Tests/Serialization/PropertyVisibilityTests.cs
src/libraries/System.Text.Json/tests/System.Text.Json.SourceGeneration.Tests/Serialization/ReferenceHandlerTests.IgnoreCycles.cs
src/libraries/System.Text.Json/tests/System.Text.Json.SourceGeneration.Tests/Serialization/ReferenceHandlerTests.cs
src/libraries/System.Text.Json/tests/System.Text.Json.SourceGeneration.Tests/Serialization/RequiredKeywordTests.cs [new file with mode: 0644]
src/libraries/System.Text.Json/tests/System.Text.Json.SourceGeneration.Tests/Serialization/UnsupportedTypesTests.cs
src/libraries/System.Text.Json/tests/System.Text.Json.SourceGeneration.Tests/System.Text.Json.SourceGeneration.Roslyn4.4.Tests.csproj [moved from src/libraries/System.Text.Json/tests/System.Text.Json.SourceGeneration.Tests/System.Text.Json.SourceGeneration.Roslyn4.0.Tests.csproj with 55% similarity]
src/libraries/System.Text.Json/tests/System.Text.Json.SourceGeneration.Tests/System.Text.Json.SourceGeneration.Tests.targets
src/libraries/System.Text.Json/tests/System.Text.Json.SourceGeneration.Unit.Tests/CompilationHelper.cs
src/libraries/System.Text.Json/tests/System.Text.Json.SourceGeneration.Unit.Tests/JsonSourceGeneratorDiagnosticsTests.cs
src/libraries/System.Text.Json/tests/System.Text.Json.SourceGeneration.Unit.Tests/System.Text.Json.SourceGeneration.Roslyn4.4.Unit.Tests.csproj [moved from src/libraries/System.Text.Json/tests/System.Text.Json.SourceGeneration.Unit.Tests/System.Text.Json.SourceGeneration.Roslyn4.0.Unit.Tests.csproj with 69% similarity]
src/libraries/System.Text.Json/tests/System.Text.Json.Tests/Serialization/JsonSerializerWrapper.Reflection.cs
src/libraries/System.Text.Json/tests/System.Text.Json.Tests/Serialization/RequiredKeywordTests.cs
src/libraries/System.Text.Json/tests/System.Text.Json.Tests/System.Text.Json.Tests.csproj

index 948f9d3..46935b7 100644 (file)
@@ -47,15 +47,15 @@ Project("{6EC3EE1D-3C4E-46DD-8F32-0CC8E7565705}") = "System.Text.Json.FSharp.Tes
 EndProject
 Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "System.Text.Json.TestLibrary.Roslyn3.11", "tests\System.Text.Json.SourceGeneration.TestLibrary\System.Text.Json.TestLibrary.Roslyn3.11.csproj", "{5C0CE30B-DD4A-4F7A-87C0-5243F0C86885}"
 EndProject
-Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "System.Text.Json.TestLibrary.Roslyn4.0", "tests\System.Text.Json.SourceGeneration.TestLibrary\System.Text.Json.TestLibrary.Roslyn4.0.csproj", "{FCA21178-0411-45D6-B597-B7BE145CEE33}"
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "System.Text.Json.TestLibrary.Roslyn4.4", "tests\System.Text.Json.SourceGeneration.TestLibrary\System.Text.Json.TestLibrary.Roslyn4.4.csproj", "{FCA21178-0411-45D6-B597-B7BE145CEE33}"
 EndProject
 Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "System.Text.Json.SourceGeneration.Roslyn3.11.Tests", "tests\System.Text.Json.SourceGeneration.Tests\System.Text.Json.SourceGeneration.Roslyn3.11.Tests.csproj", "{66AD4B7E-CF15-4A8F-8BF8-7E1BC6176D07}"
 EndProject
-Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "System.Text.Json.SourceGeneration.Roslyn4.0.Tests", "tests\System.Text.Json.SourceGeneration.Tests\System.Text.Json.SourceGeneration.Roslyn4.0.Tests.csproj", "{33599A6C-F340-4E1B-9B4D-CB8946C22140}"
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "System.Text.Json.SourceGeneration.Roslyn4.4.Tests", "tests\System.Text.Json.SourceGeneration.Tests\System.Text.Json.SourceGeneration.Roslyn4.4.Tests.csproj", "{33599A6C-F340-4E1B-9B4D-CB8946C22140}"
 EndProject
 Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "System.Text.Json.SourceGeneration.Roslyn3.11.Unit.Tests", "tests\System.Text.Json.SourceGeneration.Unit.Tests\System.Text.Json.SourceGeneration.Roslyn3.11.Unit.Tests.csproj", "{256A4653-4287-44B3-BDEF-67FC1522ED2F}"
 EndProject
-Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "System.Text.Json.SourceGeneration.Roslyn4.0.Unit.Tests", "tests\System.Text.Json.SourceGeneration.Unit.Tests\System.Text.Json.SourceGeneration.Roslyn4.0.Unit.Tests.csproj", "{F6A18EB5-A8CC-4A39-9E85-5FA226019C3D}"
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "System.Text.Json.SourceGeneration.Roslyn4.4.Unit.Tests", "tests\System.Text.Json.SourceGeneration.Unit.Tests\System.Text.Json.SourceGeneration.Roslyn4.4.Unit.Tests.csproj", "{F6A18EB5-A8CC-4A39-9E85-5FA226019C3D}"
 EndProject
 Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "System.Text.Json.Tests", "tests\System.Text.Json.Tests\System.Text.Json.Tests.csproj", "{A0178BAA-A1AF-4C69-8E4A-A700A2723DDC}"
 EndProject
index faa8529..06d15bd 100644 (file)
@@ -22,8 +22,8 @@ namespace System.Text.Json.SourceGeneration
                 public const string IncompatibleConverterType =
                     "The converter '{0}' is not compatible with the type '{1}'.";
 
-                public const string InitOnlyPropertyDeserializationNotSupported =
-                    "Deserialization of init-only properties is currently not supported in source generation mode.";
+                public const string InitOnlyPropertySetterNotSupported =
+                    "Setting init-only properties is not supported in source generation mode.";
 
                 public const string InvalidJsonConverterFactoryOutput =
                     "The converter '{0}' cannot return null or a JsonConverterFactory instance.";
index 1eeefca..55c7e5a 100644 (file)
@@ -4,6 +4,7 @@
 using System.Collections.Generic;
 using System.Diagnostics;
 using System.Globalization;
+using System.Linq;
 using System.Reflection;
 using System.Text.Json.Reflection;
 using System.Text.Json.Serialization;
@@ -699,7 +700,7 @@ private static {JsonPropertyInfoTypeRef}[] {propInitMethodName}({JsonSerializerO
                     {
                         { DefaultIgnoreCondition: JsonIgnoreCondition.Always } => "null",
                         { CanUseSetter: true, IsInitOnlySetter: true }
-                            => @$"static (obj, value) => throw new {InvalidOperationExceptionTypeRef}(""{ExceptionMessages.InitOnlyPropertyDeserializationNotSupported}"")",
+                            => @$"static (obj, value) => throw new {InvalidOperationExceptionTypeRef}(""{ExceptionMessages.InitOnlyPropertySetterNotSupported}"")",
                         { CanUseSetter: true } when typeGenerationSpec.IsValueType
                             => $@"static (obj, value) => {UnsafeTypeRef}.Unbox<{declaringTypeCompilableName}>(obj).{nameSpecifiedInSourceCode} = value!",
                         { CanUseSetter: true }
@@ -743,7 +744,8 @@ private static {JsonPropertyInfoTypeRef}[] {propInitMethodName}({JsonSerializerO
 
     {JsonPropertyInfoTypeRef} {propertyInfoVarName} = {JsonMetadataServicesTypeRef}.CreatePropertyInfo<{memberTypeCompilableName}>({OptionsLocalVariableName}, {infoVarName});");
 
-                    if (memberMetadata.IsRequired)
+                    if (memberMetadata.HasJsonRequiredAttribute ||
+                        (memberMetadata.IsRequired && !typeGenerationSpec.ConstructorSetsRequiredParameters))
                     {
                         sb.Append($@"
     {propertyInfoVarName}.IsRequired = true;");
@@ -772,7 +774,8 @@ private static {JsonPropertyInfoTypeRef}[] {propInitMethodName}({JsonSerializerO
                 Debug.Assert(typeGenerationSpec.CtorParamGenSpecArray != null);
 
                 ParameterGenerationSpec[] parameters = typeGenerationSpec.CtorParamGenSpecArray;
-                int paramCount = parameters.Length;
+                List<PropertyInitializerGenerationSpec>? propertyInitializers = typeGenerationSpec.PropertyInitializerSpecList;
+                int paramCount = parameters.Length + (propertyInitializers?.Count(propInit => !propInit.MatchesConstructorParameter) ?? 0);
                 Debug.Assert(paramCount > 0);
 
                 StringBuilder sb = new($@"
@@ -782,10 +785,9 @@ private static {JsonParameterInfoValuesTypeRef}[] {typeGenerationSpec.TypeInfoPr
     {JsonParameterInfoValuesTypeRef}[] {parametersVarName} = new {JsonParameterInfoValuesTypeRef}[{paramCount}];
     {JsonParameterInfoValuesTypeRef} info;
 ");
-
-                for (int i = 0; i < paramCount; i++)
+                foreach (ParameterGenerationSpec spec in parameters)
                 {
-                    ParameterInfo reflectionInfo = parameters[i].ParameterInfo;
+                    ParameterInfo reflectionInfo = spec.ParameterInfo;
                     Type parameterType = reflectionInfo.ParameterType;
                     string parameterTypeRef = parameterType.GetCompilableName();
 
@@ -801,8 +803,31 @@ private static {JsonParameterInfoValuesTypeRef}[] {typeGenerationSpec.TypeInfoPr
         HasDefaultValue = {FormatBool(reflectionInfo.HasDefaultValue)},
         DefaultValue = {defaultValueAsStr}
     }};
-    {parametersVarName}[{i}] = {InfoVarName};
+    {parametersVarName}[{spec.ParameterIndex}] = {InfoVarName};
+");
+                }
+
+                if (propertyInitializers != null)
+                {
+                    Debug.Assert(propertyInitializers.Count > 0);
+
+                    foreach (PropertyInitializerGenerationSpec spec in propertyInitializers)
+                    {
+                        if (spec.MatchesConstructorParameter)
+                            continue;
+
+                        sb.Append(@$"
+    {InfoVarName} = new()
+    {{
+        Name = ""{spec.Property.JsonPropertyName ?? spec.Property.ClrName}"",
+        ParameterType = typeof({spec.Property.TypeGenerationSpec.TypeRef}),
+        Position = {spec.ParameterIndex},
+        HasDefaultValue = false,
+        DefaultValue = default({spec.Property.TypeGenerationSpec.TypeRef}),
+    }};
+    {parametersVarName}[{spec.ParameterIndex}] = {InfoVarName};
 ");
+                    }
                 }
 
                 sb.Append(@$"
@@ -959,27 +984,43 @@ private static {JsonParameterInfoValuesTypeRef}[] {typeGenerationSpec.TypeInfoPr
             private static string GetParameterizedCtorInvocationFunc(TypeGenerationSpec typeGenerationSpec)
             {
                 Debug.Assert(typeGenerationSpec.CtorParamGenSpecArray != null);
-
                 ParameterGenerationSpec[] parameters = typeGenerationSpec.CtorParamGenSpecArray;
-                int paramCount = parameters.Length;
-                Debug.Assert(paramCount != 0);
+                List<PropertyInitializerGenerationSpec>? propertyInitializers = typeGenerationSpec.PropertyInitializerSpecList;
 
                 const string ArgsVarName = "args";
-                int lastIndex = paramCount - 1;
 
                 StringBuilder sb = new($"static ({ArgsVarName}) => new {typeGenerationSpec.TypeRef}(");
 
-                for (int i = 0; i < lastIndex; i++)
+                if (parameters.Length > 0)
                 {
-                    sb.Append($"{GetParamUnboxing(parameters[i], i)}, ");
+                    foreach (ParameterGenerationSpec param in parameters)
+                    {
+                        int index = param.ParameterIndex;
+                        sb.Append($"{GetParamUnboxing(param.ParameterInfo.ParameterType, index)}, ");
+                    }
+
+                    sb.Length -= 2; // delete the last ", " token
                 }
 
-                sb.Append($"{GetParamUnboxing(parameters[lastIndex], lastIndex)})");
+                sb.Append(')');
+
+                if (propertyInitializers != null)
+                {
+                    Debug.Assert(propertyInitializers.Count > 0);
+                    sb.Append("{ ");
+                    foreach (PropertyInitializerGenerationSpec property in propertyInitializers)
+                    {
+                        sb.Append($"{property.Property.ClrName} = {GetParamUnboxing(property.Property.TypeGenerationSpec.Type, property.ParameterIndex)}, ");
+                    }
+
+                    sb.Length -= 2; // delete the last ", " token
+                    sb.Append(" }");
+                }
 
                 return sb.ToString();
 
-                static string GetParamUnboxing(ParameterGenerationSpec spec, int index)
-                    => $"({spec.ParameterInfo.ParameterType.GetCompilableName()}){ArgsVarName}[{index}]";
+                static string GetParamUnboxing(Type type, int index)
+                    => $"({type.GetCompilableName()}){ArgsVarName}[{index}]";
             }
 
             private string? GetWriterMethod(Type type)
index fe6bee3..f06442f 100644 (file)
@@ -43,6 +43,7 @@ namespace System.Text.Json.SourceGeneration
             private const string JsonRequiredAttributeFullName = "System.Text.Json.Serialization.JsonRequiredAttribute";
             private const string JsonSerializerContextFullName = "System.Text.Json.Serialization.JsonSerializerContext";
             private const string JsonSourceGenerationOptionsAttributeFullName = "System.Text.Json.Serialization.JsonSourceGenerationOptionsAttribute";
+            private const string SetsRequiredMembersAttributeFullName = "System.Diagnostics.CodeAnalysis.SetsRequiredMembersAttribute";
 
             internal const string JsonSerializableAttributeFullName = "System.Text.Json.Serialization.JsonSerializableAttribute";
 
@@ -162,14 +163,6 @@ namespace System.Text.Json.SourceGeneration
                 defaultSeverity: DiagnosticSeverity.Error,
                 isEnabledByDefault: true);
 
-            private static DiagnosticDescriptor InitOnlyPropertyDeserializationNotSupported { get; } = new DiagnosticDescriptor(
-                id: "SYSLIB1037",
-                title: new LocalizableResourceString(nameof(SR.InitOnlyPropertyDeserializationNotSupportedTitle), SR.ResourceManager, typeof(FxResources.System.Text.Json.SourceGeneration.SR)),
-                messageFormat: new LocalizableResourceString(nameof(SR.InitOnlyPropertyDeserializationNotSupportedFormat), SR.ResourceManager, typeof(FxResources.System.Text.Json.SourceGeneration.SR)),
-                category: JsonConstants.SystemTextJsonSourceGenerationName,
-                defaultSeverity: DiagnosticSeverity.Warning,
-                isEnabledByDefault: true);
-
             private static DiagnosticDescriptor InaccessibleJsonIncludePropertiesNotSupported { get; } = new DiagnosticDescriptor(
                 id: "SYSLIB1038",
                 title: new LocalizableResourceString(nameof(SR.InaccessibleJsonIncludePropertiesNotSupportedTitle), SR.ResourceManager, typeof(FxResources.System.Text.Json.SourceGeneration.SR)),
@@ -708,7 +701,9 @@ namespace System.Text.Json.SourceGeneration
                 string? runtimeTypeRef = null;
                 List<PropertyGenerationSpec>? propGenSpecList = null;
                 ObjectConstructionStrategy constructionStrategy = default;
+                bool constructorSetsRequiredMembers = false;
                 ParameterGenerationSpec[]? paramGenSpecArray = null;
+                List<PropertyInitializerGenerationSpec>? propertyInitializerSpecList = null;
                 CollectionType collectionType = CollectionType.NotApplicable;
                 JsonNumberHandling? numberHandling = null;
                 bool foundDesignTimeCustomConverter = false;
@@ -716,7 +711,6 @@ namespace System.Text.Json.SourceGeneration
                 bool implementsIJsonOnSerialized = false;
                 bool implementsIJsonOnSerializing = false;
                 bool isPolymorphic = false;
-                bool hasInitOnlyProperties = false;
                 bool hasTypeFactoryConverter = false;
                 bool hasPropertyFactoryConverters = false;
                 bool canContainNullableReferenceAnnotations = type.CanContainNullableReferenceTypeAnnotations();
@@ -783,9 +777,10 @@ namespace System.Text.Json.SourceGeneration
                 }
                 else if (type.GetCompatibleGenericInterface(_iasyncEnumerableOfTType) is Type iasyncEnumerableType)
                 {
-                    if (type.CanUseDefaultConstructorForDeserialization())
+                    if (type.CanUseDefaultConstructorForDeserialization(out ConstructorInfo? defaultCtor))
                     {
                         constructionStrategy = ObjectConstructionStrategy.ParameterlessConstructor;
+                        constructorSetsRequiredMembers = defaultCtor?.ContainsAttribute(SetsRequiredMembersAttributeFullName) == true;
                     }
 
                     Type elementType = iasyncEnumerableType.GetGenericArguments()[0];
@@ -795,9 +790,10 @@ namespace System.Text.Json.SourceGeneration
                 }
                 else if (_ienumerableType.IsAssignableFrom(type))
                 {
-                    if (type.CanUseDefaultConstructorForDeserialization())
+                    if (type.CanUseDefaultConstructorForDeserialization(out ConstructorInfo? defaultCtor))
                     {
                         constructionStrategy = ObjectConstructionStrategy.ParameterlessConstructor;
+                        constructorSetsRequiredMembers = defaultCtor?.ContainsAttribute(SetsRequiredMembersAttributeFullName) == true;
                     }
 
                     Type? actualTypeToConvert;
@@ -981,6 +977,7 @@ namespace System.Text.Json.SourceGeneration
 
                         if ((constructor != null || type.IsValueType) && !type.IsAbstract)
                         {
+                            constructorSetsRequiredMembers = constructor?.ContainsAttribute(SetsRequiredMembersAttributeFullName) == true;
                             ParameterInfo[]? parameters = constructor?.GetParameters();
                             int paramCount = parameters?.Length ?? 0;
 
@@ -1001,7 +998,8 @@ namespace System.Text.Json.SourceGeneration
                                     paramGenSpecArray[i] = new ParameterGenerationSpec()
                                     {
                                         TypeGenerationSpec = typeGenerationSpec,
-                                        ParameterInfo = parameterInfo
+                                        ParameterInfo = parameterInfo,
+                                        ParameterIndex = i
                                     };
 
                                     _implicitlyRegisteredTypes.Add(typeGenerationSpec);
@@ -1024,6 +1022,8 @@ namespace System.Text.Json.SourceGeneration
                             BindingFlags.DeclaredOnly;
 
                         bool propertyOrderSpecified = false;
+                        paramGenSpecArray ??= Array.Empty<ParameterGenerationSpec>();
+                        int nextParameterIndex = paramGenSpecArray.Length;
 
                         // Walk the type hierarchy starting from the current type up to the base type(s)
                         foreach (Type currentType in type.GetSortedTypeHierarchy())
@@ -1092,10 +1092,24 @@ namespace System.Text.Json.SourceGeneration
                                     _implicitlyRegisteredTypes.Add(dataExtensionPropGenSpec);
                                 }
 
-                                if (!hasInitOnlyProperties && spec.CanUseSetter && spec.IsInitOnlySetter && !PropertyIsConstructorParameter(spec, paramGenSpecArray))
+                                if (constructionStrategy is not ObjectConstructionStrategy.NotApplicable && spec.CanUseSetter &&
+                                    ((spec.IsRequired && !constructorSetsRequiredMembers) || spec.IsInitOnlySetter))
                                 {
-                                    _sourceGenerationContext.ReportDiagnostic(Diagnostic.Create(InitOnlyPropertyDeserializationNotSupported, memberLocation, new string[] { type.Name }));
-                                    hasInitOnlyProperties = true;
+                                    ParameterGenerationSpec? matchingConstructorParameter = GetMatchingConstructorParameter(spec, paramGenSpecArray);
+
+                                    if (spec.IsRequired || matchingConstructorParameter is null)
+                                    {
+                                        constructionStrategy = ObjectConstructionStrategy.ParameterizedConstructor;
+
+                                        var propInitializerSpec = new PropertyInitializerGenerationSpec
+                                        {
+                                            Property = spec,
+                                            MatchesConstructorParameter = matchingConstructorParameter is not null,
+                                            ParameterIndex = matchingConstructorParameter?.ParameterIndex ?? nextParameterIndex++,
+                                        };
+
+                                        (propertyInitializerSpecList ??= new()).Add(propInitializerSpec);
+                                    }
                                 }
 
                                 if (spec.HasJsonInclude && (!spec.CanUseGetter || !spec.CanUseSetter || !spec.IsPublic))
@@ -1118,10 +1132,12 @@ namespace System.Text.Json.SourceGeneration
                     numberHandling,
                     propGenSpecList,
                     paramGenSpecArray,
+                    propertyInitializerSpecList,
                     collectionType,
                     collectionKeyTypeSpec,
                     collectionValueTypeSpec,
                     constructionStrategy,
+                    constructorSetsRequiredMembers,
                     nullableUnderlyingTypeMetadata: nullableUnderlyingTypeGenSpec,
                     runtimeTypeRef,
                     dataExtensionPropGenSpec,
@@ -1146,7 +1162,7 @@ namespace System.Text.Json.SourceGeneration
                     return true;
                 }
 
-                Type? actualDictionaryType  = type.GetCompatibleGenericInterface(_idictionaryOfTKeyTValueType);
+                Type? actualDictionaryType = type.GetCompatibleGenericInterface(_idictionaryOfTKeyTValueType);
                 if (actualDictionaryType == null)
                 {
                     return false;
@@ -1173,8 +1189,13 @@ namespace System.Text.Json.SourceGeneration
                 }
             }
 
-            private static bool PropertyIsConstructorParameter(PropertyGenerationSpec propSpec, ParameterGenerationSpec[]? paramGenSpecArray)
-                => paramGenSpecArray != null && paramGenSpecArray.Any(paramSpec => propSpec.ClrName.Equals(paramSpec.ParameterInfo.Name, StringComparison.OrdinalIgnoreCase));
+            private static ParameterGenerationSpec? GetMatchingConstructorParameter(PropertyGenerationSpec propSpec, ParameterGenerationSpec[]? paramGenSpecArray)
+            {
+                return paramGenSpecArray?.FirstOrDefault(MatchesConstructorParameter);
+
+                bool MatchesConstructorParameter(ParameterGenerationSpec paramSpec)
+                    => propSpec.ClrName.Equals(paramSpec.ParameterInfo.Name, StringComparison.OrdinalIgnoreCase);
+            }
 
             private static bool PropertyIsOverriddenAndIgnored(
                 string currentMemberName,
@@ -1212,13 +1233,14 @@ namespace System.Text.Json.SourceGeneration
                     out int order,
                     out bool hasFactoryConverter,
                     out bool isExtensionData,
-                    out bool isRequired);
+                    out bool hasJsonRequiredAttribute);
 
                 ProcessMember(
                     memberInfo,
                     hasJsonInclude,
                     out bool isReadOnly,
                     out bool isPublic,
+                    out bool isRequired,
                     out bool canUseGetter,
                     out bool canUseSetter,
                     out bool getterIsVirtual,
@@ -1243,11 +1265,12 @@ namespace System.Text.Json.SourceGeneration
                     IsProperty = memberInfo.MemberType == MemberTypes.Property,
                     IsPublic = isPublic,
                     IsVirtual = isVirtual,
-                    IsRequired = isRequired,
                     JsonPropertyName = jsonPropertyName,
                     RuntimePropertyName = runtimePropertyName,
                     PropertyNameVarName = propertyNameVarName,
                     IsReadOnly = isReadOnly,
+                    IsRequired = isRequired,
+                    HasJsonRequiredAttribute = hasJsonRequiredAttribute,
                     IsInitOnlySetter = setterIsInitOnly,
                     CanUseGetter = canUseGetter,
                     CanUseSetter = canUseSetter,
@@ -1292,7 +1315,7 @@ namespace System.Text.Json.SourceGeneration
                 out int order,
                 out bool hasFactoryConverter,
                 out bool isExtensionData,
-                out bool isRequired)
+                out bool hasJsonRequiredAttribute)
             {
                 hasJsonInclude = false;
                 jsonPropertyName = null;
@@ -1301,7 +1324,7 @@ namespace System.Text.Json.SourceGeneration
                 converterInstantiationLogic = null;
                 order = 0;
                 isExtensionData = false;
-                isRequired = false;
+                hasJsonRequiredAttribute = false;
 
                 bool foundDesignTimeCustomConverter = false;
                 hasFactoryConverter = false;
@@ -1370,7 +1393,7 @@ namespace System.Text.Json.SourceGeneration
                                 break;
                             case JsonRequiredAttributeFullName:
                                 {
-                                    isRequired = true;
+                                    hasJsonRequiredAttribute = true;
                                 }
                                 break;
                             default:
@@ -1385,6 +1408,7 @@ namespace System.Text.Json.SourceGeneration
                 bool hasJsonInclude,
                 out bool isReadOnly,
                 out bool isPublic,
+                out bool isRequired,
                 out bool canUseGetter,
                 out bool canUseSetter,
                 out bool getterIsVirtual,
@@ -1392,6 +1416,7 @@ namespace System.Text.Json.SourceGeneration
                 out bool setterIsInitOnly)
             {
                 isPublic = false;
+                isRequired = false;
                 canUseGetter = false;
                 canUseSetter = false;
                 getterIsVirtual = false;
@@ -1404,6 +1429,7 @@ namespace System.Text.Json.SourceGeneration
                         {
                             MethodInfo? getMethod = propertyInfo.GetMethod;
                             MethodInfo? setMethod = propertyInfo.SetMethod;
+                            isRequired = propertyInfo.IsRequired();
 
                             if (getMethod != null)
                             {
@@ -1447,6 +1473,7 @@ namespace System.Text.Json.SourceGeneration
                         {
                             isPublic = fieldInfo.IsPublic;
                             isReadOnly = fieldInfo.IsInitOnly;
+                            isRequired = fieldInfo.IsRequired();
 
                             if (!fieldInfo.IsPrivate && !fieldInfo.IsFamily)
                             {
index bb021ef..1e806fd 100644 (file)
@@ -10,5 +10,7 @@ namespace System.Text.Json.SourceGeneration
         public required TypeGenerationSpec TypeGenerationSpec { get; init; }
 
         public required ParameterInfo ParameterInfo { get; init; }
+
+        public required int ParameterIndex { get; init; }
     }
 }
index ab7411e..b6d006f 100644 (file)
@@ -31,11 +31,6 @@ namespace System.Text.Json.SourceGeneration
         public bool IsVirtual { get; init; }
 
         /// <summary>
-        /// The property has JsonRequiredAttribute.
-        /// </summary>
-        public bool IsRequired { get; init; }
-
-        /// <summary>
         /// The property name specified via JsonPropertyNameAttribute, if available.
         /// </summary>
         public string? JsonPropertyName { get; init; }
@@ -55,6 +50,16 @@ namespace System.Text.Json.SourceGeneration
         public bool IsReadOnly { get; init; }
 
         /// <summary>
+        /// Whether the property is marked `required`.
+        /// </summary>
+        public bool IsRequired { get; init; }
+
+        /// <summary>
+        /// The property is marked with JsonRequiredAttribute.
+        /// </summary>
+        public bool HasJsonRequiredAttribute { get; init; }
+
+        /// <summary>
         /// Whether the property has an init-only set method.
         /// </summary>
         public bool IsInitOnlySetter { get; init; }
diff --git a/src/libraries/System.Text.Json/gen/PropertyInitializerGenerationSpec.cs b/src/libraries/System.Text.Json/gen/PropertyInitializerGenerationSpec.cs
new file mode 100644 (file)
index 0000000..6713818
--- /dev/null
@@ -0,0 +1,14 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+namespace System.Text.Json.SourceGeneration
+{
+    internal sealed class PropertyInitializerGenerationSpec
+    {
+        public required PropertyGenerationSpec Property { get; init; }
+
+        public required int ParameterIndex { get; init; }
+
+        public required bool MatchesConstructorParameter { get; init; }
+    }
+}
index 7fa70db..c6f3e75 100644 (file)
@@ -23,6 +23,8 @@ namespace System.Text.Json.Reflection
 
         private FieldAttributes? _attributes;
 
+        public IFieldSymbol Symbol => _field;
+
         public override FieldAttributes Attributes
         {
             get
index 3e6816a..aaa85f6 100644 (file)
@@ -36,6 +36,8 @@ namespace System.Text.Json.Reflection
 
         public bool NeedsAtSign { get; }
 
+        public IPropertySymbol Symbol => _property;
+
         public override Type ReflectedType => throw new NotImplementedException();
 
         public override MethodInfo[] GetAccessors(bool nonPublic)
index 7f6bd4e..fc1d162 100644 (file)
@@ -21,6 +21,9 @@ namespace System.Text.Json.Reflection
             return index < customAttributeData.ConstructorArguments.Count ? (TValue)customAttributeData.ConstructorArguments[index].Value! : default!;
         }
 
+        public static bool ContainsAttribute(this MemberInfo memberInfo, string attributeFullName)
+            => CustomAttributeData.GetCustomAttributes(memberInfo).Any(attr => attr.AttributeType.FullName == attributeFullName);
+
         public static bool IsInitOnly(this MethodInfo method)
         {
             if (method is null)
@@ -32,6 +35,36 @@ namespace System.Text.Json.Reflection
             return methodInfoWrapper.IsInitOnly;
         }
 
+        public static bool IsRequired(this PropertyInfo propertyInfo)
+        {
+#if ROSLYN4_4_OR_GREATER
+            if (propertyInfo is null)
+            {
+                throw new ArgumentNullException(nameof(propertyInfo));
+            }
+
+            PropertyInfoWrapper methodInfoWrapper = (PropertyInfoWrapper)propertyInfo;
+            return methodInfoWrapper.Symbol.IsRequired;
+#else
+            return false;
+#endif
+        }
+
+        public static bool IsRequired(this FieldInfo propertyInfo)
+        {
+#if ROSLYN4_4_OR_GREATER
+            if (propertyInfo is null)
+            {
+                throw new ArgumentNullException(nameof(propertyInfo));
+            }
+
+            FieldInfoWrapper fieldInfoWrapper = (FieldInfoWrapper)propertyInfo;
+            return fieldInfoWrapper.Symbol.IsRequired;
+#else
+            return false;
+#endif
+        }
+
         private static bool HasJsonConstructorAttribute(ConstructorInfo constructorInfo)
         {
             IList<CustomAttributeData> attributeDataList = CustomAttributeData.GetCustomAttributes(constructorInfo);
index ce78399..7e82a8d 100644 (file)
@@ -3,6 +3,8 @@
 
 using System.Diagnostics;
 using System.Diagnostics.CodeAnalysis;
+using System.Linq;
+using System.Reflection;
 
 namespace System.Text.Json.Reflection
 {
@@ -150,8 +152,18 @@ namespace System.Text.Json.Reflection
             return false;
         }
 
-        public static bool CanUseDefaultConstructorForDeserialization(this Type type)
-            => (type.GetConstructor(Type.EmptyTypes) != null || type.IsValueType) && !type.IsAbstract && !type.IsInterface;
+        public static bool CanUseDefaultConstructorForDeserialization(this Type type, out ConstructorInfo? constructorInfo)
+        {
+            constructorInfo = type.GetConstructor(Type.EmptyTypes);
+
+            if ((constructorInfo != null || type.IsValueType) && !type.IsAbstract && !type.IsInterface)
+            {
+                return true;
+            }
+
+            constructorInfo = null;
+            return false;
+        }
 
         public static bool IsObjectType(this Type type) => type.FullName == "System.Object";
 
index 9a25add..81ab875 100644 (file)
@@ -52,6 +52,7 @@
     <Compile Include="ObjectConstructionStrategy.cs" />
     <Compile Include="ParameterGenerationSpec.cs" />
     <Compile Include="PropertyGenerationSpec.cs" />
+    <Compile Include="PropertyInitializerGenerationSpec.cs" />
     <Compile Include="Reflection\AssemblyWrapper.cs" />
     <Compile Include="Reflection\TypeExtensions.cs" />
     <Compile Include="Reflection\FieldInfoWrapper.cs" />
index 5d6f095..e8dfb07 100644 (file)
@@ -67,6 +67,9 @@ namespace System.Text.Json.SourceGeneration
 
         public ParameterGenerationSpec[]? CtorParamGenSpecArray { get; private set; }
 
+        public List<PropertyInitializerGenerationSpec>? PropertyInitializerSpecList { get; private set; }
+        public int PropertyInitializersWithoutMatchingConstructorParameters { get; private set; }
+
         public CollectionType CollectionType { get; private set; }
 
         public TypeGenerationSpec? CollectionKeyTypeMetadata { get; private set; }
@@ -75,6 +78,8 @@ namespace System.Text.Json.SourceGeneration
 
         public ObjectConstructionStrategy ConstructionStrategy { get; private set; }
 
+        public bool ConstructorSetsRequiredParameters { get; private set; }
+
         public TypeGenerationSpec? NullableUnderlyingTypeMetadata { get; private set; }
 
         /// <summary>
@@ -126,10 +131,12 @@ namespace System.Text.Json.SourceGeneration
             JsonNumberHandling? numberHandling,
             List<PropertyGenerationSpec>? propertyGenSpecList,
             ParameterGenerationSpec[]? ctorParamGenSpecArray,
+            List<PropertyInitializerGenerationSpec>? propertyInitializerSpecList,
             CollectionType collectionType,
             TypeGenerationSpec? collectionKeyTypeMetadata,
             TypeGenerationSpec? collectionValueTypeMetadata,
             ObjectConstructionStrategy constructionStrategy,
+            bool constructorSetsRequiredMembers,
             TypeGenerationSpec? nullableUnderlyingTypeMetadata,
             string? runtimeTypeRef,
             TypeGenerationSpec? extensionDataPropertyTypeSpec,
@@ -147,11 +154,13 @@ namespace System.Text.Json.SourceGeneration
             IsPolymorphic = isPolymorphic;
             NumberHandling = numberHandling;
             PropertyGenSpecList = propertyGenSpecList;
+            PropertyInitializerSpecList = propertyInitializerSpecList;
             CtorParamGenSpecArray = ctorParamGenSpecArray;
             CollectionType = collectionType;
             CollectionKeyTypeMetadata = collectionKeyTypeMetadata;
             CollectionValueTypeMetadata = collectionValueTypeMetadata;
             ConstructionStrategy = constructionStrategy;
+            ConstructorSetsRequiredParameters = constructorSetsRequiredMembers;
             NullableUnderlyingTypeMetadata = nullableUnderlyingTypeMetadata;
             RuntimeTypeRef = runtimeTypeRef;
             ExtensionDataPropertyTypeSpec = extensionDataPropertyTypeSpec;
index 0e93c83..bd0f50e 100644 (file)
@@ -69,7 +69,7 @@ namespace System.Text.Json.Serialization.Converters
                 !state.CurrentContainsMetadata) // Do not use the fast path if state needs to write metadata.
             {
                 Debug.Assert(jsonTypeInfo is JsonTypeInfo<T> typeInfo && typeInfo.SerializeHandler != null);
-                Debug.Assert(options.SerializerContext?.CanUseSerializationLogic == true);
+                Debug.Assert(jsonTypeInfo.CanUseSerializeHandler);
                 ((JsonTypeInfo<T>)jsonTypeInfo).SerializeHandler!(writer, value);
                 return true;
             }
index c396eb9..4d04c55 100644 (file)
@@ -11,8 +11,6 @@ namespace System.Text.Json.Serialization
     /// </summary>
     public abstract partial class JsonSerializerContext : IJsonTypeInfoResolver
     {
-        private bool? _canUseSerializationLogic;
-
         private JsonSerializerOptions? _options;
 
         /// <summary>
@@ -51,43 +49,31 @@ namespace System.Text.Json.Serialization
         /// Indicates whether pre-generated serialization logic for types in the context
         /// is compatible with the run time specified <see cref="JsonSerializerOptions"/>.
         /// </summary>
-        internal bool CanUseSerializationLogic
+        internal bool CanUseFastPathSerializationLogic(JsonSerializerOptions options)
         {
-            get
-            {
-                if (!_canUseSerializationLogic.HasValue)
-                {
-                    if (GeneratedSerializerOptions == null)
-                    {
-                        _canUseSerializationLogic = false;
-                    }
-                    else
-                    {
-                        _canUseSerializationLogic =
-                            // Guard against unsupported features
-                            Options.Converters.Count == 0 &&
-                            Options.Encoder == null &&
-                            // Disallow custom number handling we'd need to honor when writing.
-                            // AllowReadingFromString and Strict are fine since there's no action to take when writing.
-                            (Options.NumberHandling & (JsonNumberHandling.WriteAsString | JsonNumberHandling.AllowNamedFloatingPointLiterals)) == 0 &&
-                            Options.ReferenceHandlingStrategy == ReferenceHandlingStrategy.None &&
+            Debug.Assert(options.TypeInfoResolver == this);
+
+            return
+                GeneratedSerializerOptions is not null &&
+                // Guard against unsupported features
+                options.Converters.Count == 0 &&
+                options.Encoder == null &&
+                // Disallow custom number handling we'd need to honor when writing.
+                // AllowReadingFromString and Strict are fine since there's no action to take when writing.
+                (options.NumberHandling & (JsonNumberHandling.WriteAsString | JsonNumberHandling.AllowNamedFloatingPointLiterals)) == 0 &&
+                options.ReferenceHandlingStrategy == ReferenceHandlingStrategy.None &&
 #pragma warning disable SYSLIB0020
-                            !Options.IgnoreNullValues && // This property is obsolete.
+                !options.IgnoreNullValues && // This property is obsolete.
 #pragma warning restore SYSLIB0020
 
-                            // Ensure options values are consistent with expected defaults.
-                            Options.DefaultIgnoreCondition == GeneratedSerializerOptions.DefaultIgnoreCondition &&
-                            Options.IgnoreReadOnlyFields == GeneratedSerializerOptions.IgnoreReadOnlyFields &&
-                            Options.IgnoreReadOnlyProperties == GeneratedSerializerOptions.IgnoreReadOnlyProperties &&
-                            Options.IncludeFields == GeneratedSerializerOptions.IncludeFields &&
-                            Options.PropertyNamingPolicy == GeneratedSerializerOptions.PropertyNamingPolicy &&
-                            Options.DictionaryKeyPolicy == GeneratedSerializerOptions.DictionaryKeyPolicy &&
-                            Options.WriteIndented == GeneratedSerializerOptions.WriteIndented;
-                    }
-                }
-
-                return _canUseSerializationLogic.Value;
-            }
+                // Ensure options values are consistent with expected defaults.
+                options.DefaultIgnoreCondition == GeneratedSerializerOptions.DefaultIgnoreCondition &&
+                options.IgnoreReadOnlyFields == GeneratedSerializerOptions.IgnoreReadOnlyFields &&
+                options.IgnoreReadOnlyProperties == GeneratedSerializerOptions.IgnoreReadOnlyProperties &&
+                options.IncludeFields == GeneratedSerializerOptions.IncludeFields &&
+                options.PropertyNamingPolicy == GeneratedSerializerOptions.PropertyNamingPolicy &&
+                options.DictionaryKeyPolicy == GeneratedSerializerOptions.DictionaryKeyPolicy &&
+                options.WriteIndented == GeneratedSerializerOptions.WriteIndented;
         }
 
         /// <summary>
index 698be48..f58bfa0 100644 (file)
@@ -587,6 +587,17 @@ namespace System.Text.Json
 
         internal JsonSerializerContext? SerializerContext => _typeInfoResolver as JsonSerializerContext;
 
+        internal bool CanUseFastPathSerializationLogic
+        {
+            get
+            {
+                Debug.Assert(IsReadOnly);
+                return _canUseFastPathSerializationLogic ??= SerializerContext?.CanUseFastPathSerializationLogic(this) ?? false;
+            }
+        }
+
+        private bool? _canUseFastPathSerializationLogic;
+
         // The cached value used to determine if ReferenceHandler should use Preserve or IgnoreCycles semanitcs or None of them.
         internal ReferenceHandlingStrategy ReferenceHandlingStrategy = ReferenceHandlingStrategy.None;
         // Workaround https://github.com/dotnet/linker/issues/2715
index ecec559..dc020c3 100644 (file)
@@ -554,7 +554,7 @@ namespace System.Text.Json.Serialization.Metadata
             PropertyInfoForTypeInfo.EnsureChildOf(this);
             PropertyInfoForTypeInfo.EnsureConfigured();
 
-            CanUseSerializeHandler &= Options.SerializerContext?.CanUseSerializationLogic == true;
+            CanUseSerializeHandler &= Options.CanUseFastPathSerializationLogic;
 
             JsonConverter converter = Converter;
             Debug.Assert(PropertyInfoForTypeInfo.EffectiveConverter.ConverterStrategy == Converter.ConverterStrategy,
index db6dde9..cf3b807 100644 (file)
@@ -32,7 +32,7 @@ namespace System.Text.Json.Serialization.Metadata
                 // this avoids creating a WriteStack and calling into the converter infrastructure.
 
                 Debug.Assert(SerializeHandler != null);
-                Debug.Assert(Options.SerializerContext?.CanUseSerializationLogic == true);
+                Debug.Assert(CanUseSerializeHandler);
                 Debug.Assert(Converter is JsonMetadataServicesConverter<T>);
 
                 SerializeHandler(writer, rootValue!);
@@ -77,7 +77,7 @@ namespace System.Text.Json.Serialization.Metadata
                 // Short-circuit calls into SerializeHandler, if the `CanUseSerializeHandlerInStreaming` heuristic allows it.
 
                 Debug.Assert(SerializeHandler != null);
-                Debug.Assert(Options.SerializerContext?.CanUseSerializationLogic == true);
+                Debug.Assert(CanUseSerializeHandler);
                 Debug.Assert(Converter is JsonMetadataServicesConverter<T>);
 
                 using var bufferWriter = new PooledByteBufferWriter(Options.DefaultBufferSize);
@@ -208,7 +208,7 @@ namespace System.Text.Json.Serialization.Metadata
                 // Short-circuit calls into SerializeHandler, if the `CanUseSerializeHandlerInStreaming` heuristic allows it.
 
                 Debug.Assert(SerializeHandler != null);
-                Debug.Assert(Options.SerializerContext?.CanUseSerializationLogic == true);
+                Debug.Assert(CanUseSerializeHandler);
                 Debug.Assert(Converter is JsonMetadataServicesConverter<T>);
 
                 Utf8JsonWriter writer = Utf8JsonWriterCache.RentWriterAndBuffer(Options, out PooledByteBufferWriter bufferWriter);
index 9435d71..87d917c 100644 (file)
@@ -11,6 +11,8 @@ namespace System.Text.Json.Serialization.Tests
     /// </summary>
     public abstract partial class JsonSerializerWrapper
     {
+        public abstract JsonSerializerOptions DefaultOptions { get; }
+
         /// <summary>
         /// Do the deserialize methods allow a value of 'null'.
         /// For example, deserializing JSON to a String supports null by returning a 'null' String reference from a literal value of "null".
@@ -36,5 +38,44 @@ namespace System.Text.Json.Serialization.Tests
         public abstract Task<object> DeserializeWrapper(string value, JsonTypeInfo jsonTypeInfo);
 
         public abstract Task<object> DeserializeWrapper(string json, Type type, JsonSerializerContext context);
+
+        public JsonSerializerOptions GetDefaultOptionsWithMetadataModifier(Action<JsonTypeInfo> modifier)
+        {
+            JsonSerializerOptions defaultOptions = DefaultOptions;
+            return new JsonSerializerOptions(defaultOptions)
+            {
+                TypeInfoResolver = defaultOptions.TypeInfoResolver.WithModifier(modifier)
+            };
+        }
+    }
+
+    public static class JsonTypeInfoResolverExtensions
+    {
+        public static IJsonTypeInfoResolver WithModifier(this IJsonTypeInfoResolver resolver, Action<JsonTypeInfo> modifier)
+            => new JsonTypeInfoResolverWithModifier(resolver, modifier);
+
+        private class JsonTypeInfoResolverWithModifier : IJsonTypeInfoResolver
+        {
+            private readonly IJsonTypeInfoResolver _source;
+            private readonly Action<JsonTypeInfo> _modifier;
+
+            public JsonTypeInfoResolverWithModifier(IJsonTypeInfoResolver source, Action<JsonTypeInfo> modifier)
+            {
+                _source = source;
+                _modifier = modifier;
+            }
+
+            public JsonTypeInfo? GetTypeInfo(Type type, JsonSerializerOptions options)
+            {
+                JsonTypeInfo? typeInfo = _source.GetTypeInfo(type, options);
+
+                if (typeInfo != null)
+                {
+                    _modifier(typeInfo);
+                }
+
+                return typeInfo;
+            }
+        }
     }
 }
index 83790f7..ce6194b 100644 (file)
@@ -243,13 +243,13 @@ namespace System.Text.Json.Serialization.Tests
             await Task.WhenAll(tasks);
         }
 
-        private async void TestIdTask()
+        private async Task TestIdTask()
         {
             JsonException ex = await Assert.ThrowsAsync<JsonException>(async () => await Serializer.DeserializeWrapper<Employee>(@"{""$id"":1}", s_deserializerOptionsPreserve));
             Assert.Equal("$.$id", ex.Path);
         }
 
-        private async void TestRefTask()
+        private async Task TestRefTask()
         {
             JsonException ex = await Assert.ThrowsAsync<JsonException>(async () => await Serializer.DeserializeWrapper<Employee>(@"{""$ref"":1}", s_deserializerOptionsPreserve));
             Assert.Equal("$.$ref", ex.Path);
index 9e613a0..3bcf7a9 100644 (file)
@@ -383,7 +383,7 @@ namespace System.Text.Json.Serialization.Tests
         }
 
         [Fact] // https://github.com/dotnet/runtime/issues/51837
-        public async void IgnoreCycles_StringShouldNotBeIgnored()
+        public async Task IgnoreCycles_StringShouldNotBeIgnored()
         {
             var stringReference = "John";
             
@@ -400,7 +400,7 @@ namespace System.Text.Json.Serialization.Tests
         }
 
         [Fact]
-        public async void IgnoreCycles_BoxedValueShouldNotBeIgnored()
+        public async Task IgnoreCycles_BoxedValueShouldNotBeIgnored()
         {
             object dayOfBirthAsObject = 15;
 
diff --git a/src/libraries/System.Text.Json/tests/Common/RequiredKeywordTests.cs b/src/libraries/System.Text.Json/tests/Common/RequiredKeywordTests.cs
new file mode 100644 (file)
index 0000000..4af913c
--- /dev/null
@@ -0,0 +1,597 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using System.Collections.Generic;
+using System.Diagnostics.CodeAnalysis;
+using System.Text.Json.Serialization.Metadata;
+using System.Threading.Tasks;
+using Xunit;
+
+namespace System.Text.Json.Serialization.Tests
+{
+    public abstract partial class RequiredKeywordTests : SerializerTests
+    {
+        public RequiredKeywordTests(JsonSerializerWrapper serializer) : base(serializer)
+        {
+        }
+
+        [Theory]
+        [InlineData(true)]
+        [InlineData(false)]
+        public async Task ClassWithRequiredKeywordDeserialization(bool ignoreNullValues)
+        {
+            JsonSerializerOptions options = new()
+            {
+                IgnoreNullValues = ignoreNullValues
+            };
+
+            AssertJsonTypeInfoHasRequiredProperties(GetTypeInfo<PersonWithRequiredMembers>(options),
+                nameof(PersonWithRequiredMembers.FirstName),
+                nameof(PersonWithRequiredMembers.LastName));
+
+            var obj = new PersonWithRequiredMembers()
+            {
+                FirstName = "foo",
+                LastName = "bar"
+            };
+
+            string json = await Serializer.SerializeWrapper(obj, options);
+            Assert.Equal("""{"FirstName":"foo","MiddleName":"","LastName":"bar"}""", json);
+
+            PersonWithRequiredMembers deserialized = await Serializer.DeserializeWrapper<PersonWithRequiredMembers>(json, options);
+            Assert.Equal(obj.FirstName, deserialized.FirstName);
+            Assert.Equal(obj.MiddleName, deserialized.MiddleName);
+            Assert.Equal(obj.LastName, deserialized.LastName);
+
+            json = """{"LastName":"bar"}""";
+            JsonException exception = await Assert.ThrowsAsync<JsonException>(async () => await Serializer.DeserializeWrapper<PersonWithRequiredMembers>(json, options));
+            Assert.Contains("FirstName", exception.Message);
+            Assert.DoesNotContain("LastName", exception.Message);
+            Assert.DoesNotContain("MiddleName", exception.Message);
+
+            json = """{"LastName":null}""";
+            exception = await Assert.ThrowsAsync<JsonException>(async () => await Serializer.DeserializeWrapper<PersonWithRequiredMembers>(json, options));
+            Assert.Contains("FirstName", exception.Message);
+            Assert.DoesNotContain("LastName", exception.Message);
+            Assert.DoesNotContain("MiddleName", exception.Message);
+
+            json = "{}";
+            exception = await Assert.ThrowsAsync<JsonException>(async () => await Serializer.DeserializeWrapper<PersonWithRequiredMembers>(json, options));
+            Assert.Contains("FirstName", exception.Message);
+            Assert.Contains("LastName", exception.Message);
+            Assert.DoesNotContain("MiddleName", exception.Message);
+        }
+
+        [Fact]
+        public async Task RequiredPropertyOccuringTwiceInThePayloadWorksAsExpected()
+        {
+            string json = """{"FirstName":"foo","MiddleName":"","LastName":"bar","FirstName":"newfoo"}""";
+            PersonWithRequiredMembers deserialized = await Serializer.DeserializeWrapper<PersonWithRequiredMembers>(json);
+            Assert.Equal("newfoo", deserialized.FirstName);
+            Assert.Equal("", deserialized.MiddleName);
+            Assert.Equal("bar", deserialized.LastName);
+        }
+
+        public class PersonWithRequiredMembers
+        {
+            public required string FirstName { get; set; }
+            public string MiddleName { get; set; } = "";
+            public required string LastName { get; set; }
+        }
+
+        [Theory]
+        [InlineData(true)]
+        [InlineData(false)]
+        public async Task ClassWithRequiredKeywordAndSmallParametrizedCtorFailsDeserialization(bool ignoreNullValues)
+        {
+            JsonSerializerOptions options = new()
+            {
+                IgnoreNullValues = ignoreNullValues
+            };
+
+            AssertJsonTypeInfoHasRequiredProperties(GetTypeInfo<PersonWithRequiredMembersAndSmallParametrizedCtor>(options),
+                nameof(PersonWithRequiredMembersAndSmallParametrizedCtor.FirstName),
+                nameof(PersonWithRequiredMembersAndSmallParametrizedCtor.LastName),
+                nameof(PersonWithRequiredMembersAndSmallParametrizedCtor.Info1),
+                nameof(PersonWithRequiredMembersAndSmallParametrizedCtor.Info2));
+
+            var obj = new PersonWithRequiredMembersAndSmallParametrizedCtor("badfoo", "badbar")
+            {
+                // note: these must be set during initialize or otherwise we get compiler errors
+                FirstName = "foo",
+                LastName = "bar",
+                Info1 = "info1",
+                Info2 = "info2",
+            };
+
+            string json = await Serializer.SerializeWrapper(obj, options);
+            Assert.Equal("""{"FirstName":"foo","MiddleName":"","LastName":"bar","Info1":"info1","Info2":"info2"}""", json);
+
+            var deserialized = await Serializer.DeserializeWrapper<PersonWithRequiredMembersAndSmallParametrizedCtor>(json, options);
+            Assert.Equal(obj.FirstName, deserialized.FirstName);
+            Assert.Equal(obj.MiddleName, deserialized.MiddleName);
+            Assert.Equal(obj.LastName, deserialized.LastName);
+            Assert.Equal(obj.Info1, deserialized.Info1);
+            Assert.Equal(obj.Info2, deserialized.Info2);
+
+            json = """{"FirstName":"foo","MiddleName":"","LastName":null,"Info1":null,"Info2":"info2"}""";
+            deserialized = await Serializer.DeserializeWrapper<PersonWithRequiredMembersAndSmallParametrizedCtor>(json, options);
+            Assert.Equal(obj.FirstName, deserialized.FirstName);
+            Assert.Equal(obj.MiddleName, deserialized.MiddleName);
+            Assert.Null(deserialized.LastName);
+            Assert.Null(deserialized.Info1);
+            Assert.Equal(obj.Info2, deserialized.Info2);
+
+            json = """{"LastName":"bar","Info1":"info1"}""";
+            JsonException exception = await Assert.ThrowsAsync<JsonException>(async () => await Serializer.DeserializeWrapper<PersonWithRequiredMembersAndSmallParametrizedCtor>(json, options));
+            Assert.Contains("FirstName", exception.Message);
+            Assert.DoesNotContain("LastName", exception.Message);
+            Assert.DoesNotContain("MiddleName", exception.Message);
+            Assert.DoesNotContain("Info1", exception.Message);
+            Assert.Contains("Info2", exception.Message);
+
+            json = """{"LastName":null,"Info1":null}""";
+            exception = await Assert.ThrowsAsync<JsonException>(async () => await Serializer.DeserializeWrapper<PersonWithRequiredMembersAndSmallParametrizedCtor>(json, options));
+            Assert.Contains("FirstName", exception.Message);
+            Assert.DoesNotContain("LastName", exception.Message);
+            Assert.DoesNotContain("MiddleName", exception.Message);
+            Assert.DoesNotContain("Info1", exception.Message);
+            Assert.Contains("Info2", exception.Message);
+
+            json = "{}";
+            exception = await Assert.ThrowsAsync<JsonException>(async () => await Serializer.DeserializeWrapper<PersonWithRequiredMembersAndSmallParametrizedCtor>(json, options));
+            Assert.Contains("FirstName", exception.Message);
+            Assert.Contains("LastName", exception.Message);
+            Assert.DoesNotContain("MiddleName", exception.Message);
+            Assert.Contains("Info1", exception.Message);
+            Assert.Contains("Info2", exception.Message);
+        }
+
+        public class PersonWithRequiredMembersAndSmallParametrizedCtor
+        {
+            public required string FirstName { get; set; }
+            public string MiddleName { get; set; } = "";
+            public required string LastName { get; set; }
+            public required string Info1 { get; set; }
+            public required string Info2 { get; set; }
+
+            public PersonWithRequiredMembersAndSmallParametrizedCtor(string firstName, string lastName)
+            {
+                FirstName = firstName;
+                LastName = lastName;
+            }
+        }
+
+        [Theory]
+        [InlineData(true)]
+        [InlineData(false)]
+        public async Task ClassWithRequiredKeywordAndLargeParametrizedCtorFailsDeserialization(bool ignoreNullValues)
+        {
+            JsonSerializerOptions options = new()
+            {
+                IgnoreNullValues = ignoreNullValues
+            };
+
+            AssertJsonTypeInfoHasRequiredProperties(GetTypeInfo<PersonWithRequiredMembersAndLargeParametrizedCtor>(options),
+                nameof(PersonWithRequiredMembersAndLargeParametrizedCtor.AProp),
+                nameof(PersonWithRequiredMembersAndLargeParametrizedCtor.BProp),
+                nameof(PersonWithRequiredMembersAndLargeParametrizedCtor.CProp),
+                nameof(PersonWithRequiredMembersAndLargeParametrizedCtor.DProp),
+                nameof(PersonWithRequiredMembersAndLargeParametrizedCtor.EProp),
+                nameof(PersonWithRequiredMembersAndLargeParametrizedCtor.FProp),
+                nameof(PersonWithRequiredMembersAndLargeParametrizedCtor.GProp),
+                nameof(PersonWithRequiredMembersAndLargeParametrizedCtor.HProp),
+                nameof(PersonWithRequiredMembersAndLargeParametrizedCtor.IProp));
+
+            var obj = new PersonWithRequiredMembersAndLargeParametrizedCtor("bada", "badb", "badc", "badd", "bade", "badf", "badg")
+            {
+                // note: these must be set during initialize or otherwise we get compiler errors
+                AProp = "a",
+                BProp = "b",
+                CProp = "c",
+                DProp = "d",
+                EProp = "e",
+                FProp = "f",
+                GProp = "g",
+                HProp = "h",
+                IProp = "i",
+            };
+
+            string json = await Serializer.SerializeWrapper(obj, options);
+            Assert.Equal("""{"AProp":"a","BProp":"b","CProp":"c","DProp":"d","EProp":"e","FProp":"f","GProp":"g","HProp":"h","IProp":"i"}""", json);
+
+            var deserialized = await Serializer.DeserializeWrapper<PersonWithRequiredMembersAndLargeParametrizedCtor>(json, options);
+            Assert.Equal(obj.AProp, deserialized.AProp);
+            Assert.Equal(obj.BProp, deserialized.BProp);
+            Assert.Equal(obj.CProp, deserialized.CProp);
+            Assert.Equal(obj.DProp, deserialized.DProp);
+            Assert.Equal(obj.EProp, deserialized.EProp);
+            Assert.Equal(obj.FProp, deserialized.FProp);
+            Assert.Equal(obj.GProp, deserialized.GProp);
+            Assert.Equal(obj.HProp, deserialized.HProp);
+            Assert.Equal(obj.IProp, deserialized.IProp);
+
+            json = """{"AProp":"a","BProp":"b","CProp":"c","DProp":"d","EProp":null,"FProp":"f","GProp":"g","HProp":null,"IProp":"i"}""";
+            deserialized = await Serializer.DeserializeWrapper<PersonWithRequiredMembersAndLargeParametrizedCtor>(json, options);
+            Assert.Equal(obj.AProp, deserialized.AProp);
+            Assert.Equal(obj.BProp, deserialized.BProp);
+            Assert.Equal(obj.CProp, deserialized.CProp);
+            Assert.Equal(obj.DProp, deserialized.DProp);
+            Assert.Null(deserialized.EProp);
+            Assert.Equal(obj.FProp, deserialized.FProp);
+            Assert.Equal(obj.GProp, deserialized.GProp);
+            Assert.Null(deserialized.HProp);
+            Assert.Equal(obj.IProp, deserialized.IProp);
+
+            json = """{"AProp":"a","IProp":"i"}""";
+            JsonException exception = await Assert.ThrowsAsync<JsonException>(async () => await Serializer.DeserializeWrapper<PersonWithRequiredMembersAndLargeParametrizedCtor>(json, options));
+            Assert.DoesNotContain("AProp", exception.Message);
+            Assert.Contains("BProp", exception.Message);
+            Assert.Contains("CProp", exception.Message);
+            Assert.Contains("DProp", exception.Message);
+            Assert.Contains("EProp", exception.Message);
+            Assert.Contains("FProp", exception.Message);
+            Assert.Contains("GProp", exception.Message);
+            Assert.Contains("HProp", exception.Message);
+            Assert.DoesNotContain("IProp", exception.Message);
+
+            json = """{"AProp":null,"IProp":null}""";
+            exception = await Assert.ThrowsAsync<JsonException>(async () => await Serializer.DeserializeWrapper<PersonWithRequiredMembersAndLargeParametrizedCtor>(json, options));
+            Assert.DoesNotContain("AProp", exception.Message);
+            Assert.Contains("BProp", exception.Message);
+            Assert.Contains("CProp", exception.Message);
+            Assert.Contains("DProp", exception.Message);
+            Assert.Contains("EProp", exception.Message);
+            Assert.Contains("FProp", exception.Message);
+            Assert.Contains("GProp", exception.Message);
+            Assert.Contains("HProp", exception.Message);
+            Assert.DoesNotContain("IProp", exception.Message);
+
+            json = """{"BProp":"b","CProp":"c","DProp":"d","EProp":"e","FProp":"f","HProp":"h"}""";
+            exception = await Assert.ThrowsAsync<JsonException>(async () => await Serializer.DeserializeWrapper<PersonWithRequiredMembersAndLargeParametrizedCtor>(json, options));
+            Assert.Contains("AProp", exception.Message);
+            Assert.DoesNotContain("BProp", exception.Message);
+            Assert.DoesNotContain("CProp", exception.Message);
+            Assert.DoesNotContain("DProp", exception.Message);
+            Assert.DoesNotContain("EProp", exception.Message);
+            Assert.DoesNotContain("FProp", exception.Message);
+            Assert.Contains("GProp", exception.Message);
+            Assert.DoesNotContain("HProp", exception.Message);
+            Assert.Contains("IProp", exception.Message);
+        }
+
+        public class PersonWithRequiredMembersAndLargeParametrizedCtor
+        {
+            // Using suffix for names so that checking if required property is missing can be done with simple string.Contains without false positives
+            public required string AProp { get; set; }
+            public required string BProp { get; set; }
+            public required string CProp { get; set; }
+            public required string DProp { get; set; }
+            public required string EProp { get; set; }
+            public required string FProp { get; set; }
+            public required string GProp { get; set; }
+            public required string HProp { get; set; }
+            public required string IProp { get; set; }
+
+            public PersonWithRequiredMembersAndLargeParametrizedCtor(string aprop, string bprop, string cprop, string dprop, string eprop, string fprop, string gprop)
+            {
+                AProp = aprop;
+                BProp = bprop;
+                CProp = cprop;
+                DProp = dprop;
+                EProp = eprop;
+                FProp = fprop;
+                GProp = gprop;
+            }
+        }
+
+        [Fact]
+        public async Task ClassWithRequiredKeywordAndSetsRequiredMembersOnCtorWorks()
+        {
+            AssertJsonTypeInfoHasRequiredProperties(GetTypeInfo<PersonWithRequiredMembersAndSetsRequiredMembers>(Serializer.DefaultOptions)
+                /* no required members */);
+
+            var obj = new PersonWithRequiredMembersAndSetsRequiredMembers()
+            {
+                FirstName = "foo",
+                LastName = "bar"
+            };
+
+            string json = await Serializer.SerializeWrapper(obj);
+            Assert.Equal("""{"FirstName":"foo","MiddleName":"","LastName":"bar"}""", json);
+
+            json = """{"LastName":"bar"}""";
+            var deserialized = await Serializer.DeserializeWrapper<PersonWithRequiredMembersAndSetsRequiredMembers>(json);
+            Assert.Equal("", deserialized.FirstName);
+            Assert.Equal("", deserialized.MiddleName);
+            Assert.Equal("bar", deserialized.LastName);
+        }
+
+        public class PersonWithRequiredMembersAndSetsRequiredMembers
+        {
+            public required string FirstName { get; set; }
+            public string MiddleName { get; set; } = "";
+            public required string LastName { get; set; }
+
+            [SetsRequiredMembers]
+            public PersonWithRequiredMembersAndSetsRequiredMembers()
+            {
+                FirstName = "";
+                LastName = "";
+            }
+        }
+
+        [Fact]
+        public async Task ClassWithRequiredKeywordSmallParametrizedCtorAndSetsRequiredMembersOnCtorWorks()
+        {
+            AssertJsonTypeInfoHasRequiredProperties(GetTypeInfo<PersonWithRequiredMembersAndSmallParametrizedCtorAndSetsRequiredMembers>(Serializer.DefaultOptions)
+                /* no required members */);
+
+            var obj = new PersonWithRequiredMembersAndSmallParametrizedCtorAndSetsRequiredMembers("foo", "bar");
+
+            string json = await Serializer.SerializeWrapper(obj);
+            Assert.Equal("""{"FirstName":"foo","MiddleName":"","LastName":"bar"}""", json);
+
+            var deserialized = await Serializer.DeserializeWrapper<PersonWithRequiredMembersAndSmallParametrizedCtorAndSetsRequiredMembers>(json);
+            Assert.Equal("foo", deserialized.FirstName);
+            Assert.Equal("", deserialized.MiddleName);
+            Assert.Equal("bar", deserialized.LastName);
+        }
+
+        public class PersonWithRequiredMembersAndSmallParametrizedCtorAndSetsRequiredMembers
+        {
+            public required string FirstName { get; set; }
+            public string MiddleName { get; set; } = "";
+            public required string LastName { get; set; }
+
+            [SetsRequiredMembers]
+            public PersonWithRequiredMembersAndSmallParametrizedCtorAndSetsRequiredMembers(string firstName, string lastName)
+            {
+                FirstName = firstName;
+                LastName = lastName;
+            }
+        }
+
+        [Fact]
+        public async Task ClassWithRequiredKeywordLargeParametrizedCtorAndSetsRequiredMembersOnCtorWorks()
+        {
+            AssertJsonTypeInfoHasRequiredProperties(GetTypeInfo<PersonWithRequiredMembersAndLargeParametrizedCtorAndSetsRequiredMembers>(Serializer.DefaultOptions)
+                /* no required members */);
+
+            var obj = new PersonWithRequiredMembersAndLargeParametrizedCtorAndSetsRequiredMembers("a", "b", "c", "d", "e", "f", "g");
+
+            string json = await Serializer.SerializeWrapper(obj);
+            Assert.Equal("""{"A":"a","B":"b","C":"c","D":"d","E":"e","F":"f","G":"g"}""", json);
+
+            var deserialized = await Serializer.DeserializeWrapper<PersonWithRequiredMembersAndLargeParametrizedCtorAndSetsRequiredMembers>(json);
+            Assert.Equal("a", deserialized.A);
+            Assert.Equal("b", deserialized.B);
+            Assert.Equal("c", deserialized.C);
+            Assert.Equal("d", deserialized.D);
+            Assert.Equal("e", deserialized.E);
+            Assert.Equal("f", deserialized.F);
+            Assert.Equal("g", deserialized.G);
+        }
+
+        public class PersonWithRequiredMembersAndLargeParametrizedCtorAndSetsRequiredMembers
+        {
+            public required string A { get; set; }
+            public required string B { get; set; }
+            public required string C { get; set; }
+            public required string D { get; set; }
+            public required string E { get; set; }
+            public required string F { get; set; }
+            public required string G { get; set; }
+
+            [SetsRequiredMembers]
+            public PersonWithRequiredMembersAndLargeParametrizedCtorAndSetsRequiredMembers(string a, string b, string c, string d, string e, string f, string g)
+            {
+                A = a;
+                B = b;
+                C = c;
+                D = d;
+                E = e;
+                F = f;
+                G = g;
+            }
+        }
+
+        [Fact]
+        public async Task ClassWithRequiredFieldWorksAsExpected()
+        {
+            var options = new JsonSerializerOptions(Serializer.DefaultOptions) { IncludeFields = true };
+            options.MakeReadOnly();
+
+            JsonTypeInfo typeInfo = options.GetTypeInfo(typeof(ClassWithRequiredField));
+            Assert.Equal(1, typeInfo.Properties.Count);
+
+            JsonPropertyInfo jsonPropertyInfo = typeInfo.Properties[0];
+            Assert.True(jsonPropertyInfo.IsRequired);
+
+            await Assert.ThrowsAsync<JsonException>(() => Serializer.DeserializeWrapper("{}", typeInfo));
+        }
+
+        public class ClassWithRequiredField
+        {
+            public required int RequiredField;
+        }
+
+        [Fact]
+        public async Task RemovingPropertiesWithRequiredKeywordAllowsDeserialization()
+        {
+            JsonSerializerOptions options = Serializer.GetDefaultOptionsWithMetadataModifier(static ti =>
+            {
+                for (int i = 0; i < ti.Properties.Count; i++)
+                {
+                    if (ti.Properties[i].Name == nameof(PersonWithRequiredMembers.FirstName))
+                    {
+                        Assert.True(ti.Properties[i].IsRequired);
+                        JsonPropertyInfo property = ti.CreateJsonPropertyInfo(typeof(string), nameof(PersonWithRequiredMembers.FirstName));
+                        property.Get = (obj) => ((PersonWithRequiredMembers)obj).FirstName;
+                        property.Set = (obj, val) => ((PersonWithRequiredMembers)obj).FirstName = (string)val;
+                        ti.Properties[i] = property;
+                    }
+                    else if (ti.Properties[i].Name == nameof(PersonWithRequiredMembers.LastName))
+                    {
+                        Assert.True(ti.Properties[i].IsRequired);
+                        JsonPropertyInfo property = ti.CreateJsonPropertyInfo(typeof(string), nameof(PersonWithRequiredMembers.LastName));
+                        property.Get = (obj) => ((PersonWithRequiredMembers)obj).LastName;
+                        property.Set = (obj, val) => ((PersonWithRequiredMembers)obj).LastName = (string)val;
+                        ti.Properties[i] = property;
+                    }
+                    else
+                    {
+                        Assert.False(ti.Properties[i].IsRequired);
+                    }
+                }
+            });
+
+            var obj = new PersonWithRequiredMembers()
+            {
+                FirstName = "foo",
+                LastName = "bar"
+            };
+
+            string json = await Serializer.SerializeWrapper(obj, options);
+            Assert.Equal("""{"FirstName":"foo","MiddleName":"","LastName":"bar"}""", json);
+
+            json = """{"LastName":"bar"}""";
+            PersonWithRequiredMembers deserialized = await Serializer.DeserializeWrapper<PersonWithRequiredMembers>(json, options);
+            Assert.Null(deserialized.FirstName);
+            Assert.Equal("", deserialized.MiddleName);
+            Assert.Equal("bar", deserialized.LastName);
+        }
+
+        [Fact]
+        public async Task ChangingPropertiesWithRequiredKeywordToNotBeRequiredAllowsDeserialization()
+        {
+            JsonSerializerOptions options = Serializer.GetDefaultOptionsWithMetadataModifier(static ti =>
+            {
+                for (int i = 0; i < ti.Properties.Count; i++)
+                {
+                    ti.Properties[i].IsRequired = false;
+                }
+            });
+
+            var obj = new PersonWithRequiredMembers()
+            {
+                FirstName = "foo",
+                LastName = "bar"
+            };
+
+            string json = await Serializer.SerializeWrapper(obj, options);
+            Assert.Equal("""{"FirstName":"foo","MiddleName":"","LastName":"bar"}""", json);
+
+            json = """{"LastName":"bar"}""";
+            PersonWithRequiredMembers deserialized = await Serializer.DeserializeWrapper<PersonWithRequiredMembers>(json, options);
+            Assert.Null(deserialized.FirstName);
+            Assert.Equal("", deserialized.MiddleName);
+            Assert.Equal("bar", deserialized.LastName);
+        }
+
+        [Fact]
+        public async Task RequiredNonDeserializablePropertyThrows()
+        {
+            JsonSerializerOptions options = Serializer.GetDefaultOptionsWithMetadataModifier(static ti =>
+            {
+                for (int i = 0; i < ti.Properties.Count; i++)
+                {
+                    if (ti.Properties[i].Name == nameof(PersonWithRequiredMembers.FirstName))
+                    {
+                        ti.Properties[i].Set = null;
+                    }
+                }
+            });
+
+            string json = """{"FirstName":"foo","MiddleName":"","LastName":"bar"}""";
+            InvalidOperationException exception = await Assert.ThrowsAsync<InvalidOperationException>(async () => await Serializer.DeserializeWrapper<PersonWithRequiredMembers>(json, options));
+            Assert.Contains(nameof(PersonWithRequiredMembers.FirstName), exception.Message);
+        }
+
+        [Fact]
+        public async Task RequiredInitOnlyPropertyDoesNotThrow()
+        {
+            string json = """{"Prop":"foo"}""";
+            ClassWithInitOnlyRequiredProperty deserialized = await Serializer.DeserializeWrapper<ClassWithInitOnlyRequiredProperty>(json);
+            Assert.Equal("foo", deserialized.Prop);
+        }
+
+        public class ClassWithInitOnlyRequiredProperty
+        {
+            public required string Prop { get; init; }
+        }
+
+        [Fact]
+        public async Task RequiredExtensionDataPropertyThrows()
+        {
+            string json = """{"Foo":"foo","Bar":"bar"}""";
+            InvalidOperationException exception = await Assert.ThrowsAsync<InvalidOperationException>(
+                async () => await Serializer.DeserializeWrapper<ClassWithRequiredExtensionDataProperty>(json));
+            Assert.Contains(nameof(ClassWithRequiredExtensionDataProperty.TestExtensionData), exception.Message);
+        }
+
+        public class ClassWithRequiredExtensionDataProperty
+        {
+            [JsonExtensionData]
+            public required Dictionary<string, JsonElement>? TestExtensionData { get; set; }
+        }
+
+        [Fact]
+        public async Task RequiredKeywordAndJsonRequiredCustomAttributeWorkCorrectlyTogether()
+        {
+            JsonSerializerOptions options = JsonSerializerOptions.Default;
+            JsonTypeInfo typeInfo = GetTypeInfo<ClassWithRequiredKeywordAndJsonRequiredCustomAttribute>(options);
+            AssertJsonTypeInfoHasRequiredProperties(typeInfo,
+                nameof(ClassWithRequiredKeywordAndJsonRequiredCustomAttribute.SomeProperty));
+
+            ClassWithRequiredKeywordAndJsonRequiredCustomAttribute obj = new()
+            {
+                SomeProperty = "foo"
+            };
+
+            string json = await Serializer.SerializeWrapper(obj, options);
+            Assert.Equal("""{"SomeProperty":"foo"}""", json);
+
+            var deserialized = await Serializer.DeserializeWrapper<ClassWithRequiredKeywordAndJsonRequiredCustomAttribute>(json, options);
+            Assert.Equal(obj.SomeProperty, deserialized.SomeProperty);
+
+            json = "{}";
+            JsonException exception = await Assert.ThrowsAsync<JsonException>(
+                async () => await Serializer.DeserializeWrapper<ClassWithRequiredKeywordAndJsonRequiredCustomAttribute>(json, options));
+
+            Assert.Contains(nameof(ClassWithRequiredKeywordAndJsonRequiredCustomAttribute.SomeProperty), exception.Message);
+        }
+
+        public class ClassWithRequiredKeywordAndJsonRequiredCustomAttribute
+        {
+            [JsonRequired]
+            public required string SomeProperty { get; set; }
+        }
+
+        private static JsonTypeInfo GetTypeInfo<T>(JsonSerializerOptions options)
+        {
+            options.TypeInfoResolver ??= JsonSerializerOptions.Default.TypeInfoResolver;
+            options.MakeReadOnly();
+            return options.GetTypeInfo(typeof(T));
+        }
+
+        private static void AssertJsonTypeInfoHasRequiredProperties(JsonTypeInfo typeInfo, params string[] requiredProperties)
+        {
+            HashSet<string> requiredPropertiesSet = new(requiredProperties);
+
+            foreach (var property in typeInfo.Properties)
+            {
+                if (requiredPropertiesSet.Remove(property.Name))
+                {
+                    Assert.True(property.IsRequired);
+                }
+                else
+                {
+                    Assert.False(property.IsRequired);
+                }
+            }
+
+            Assert.Empty(requiredPropertiesSet);
+        }
+    }
+}
@@ -3,6 +3,6 @@
   <Import Project="System.Text.Json.TestLibrary.targets" />
 
   <ItemGroup>
-    <ProjectReference Include="..\..\gen\System.Text.Json.SourceGeneration.Roslyn4.0.csproj" OutputItemType="Analyzer" ReferenceOutputAssembly="false" />
+    <ProjectReference Include="..\..\gen\System.Text.Json.SourceGeneration.Roslyn4.4.csproj" OutputItemType="Analyzer" ReferenceOutputAssembly="false" />
   </ItemGroup>
 </Project>
index 5f6d9aa..595d1e0 100644 (file)
@@ -18,7 +18,7 @@ namespace System.Text.Json.SourceGeneration.Tests
     public sealed class CollectionTests_Metadata_String : CollectionTests_Metadata
     {
         public CollectionTests_Metadata_String()
-            : base(new StringSerializerWrapper(CollectionTestsContext_Metadata.Default, (options) => new CollectionTestsContext_Metadata(options)))
+            : base(new StringSerializerWrapper(CollectionTestsContext_Metadata.Default))
         {
         }
     }
@@ -26,7 +26,7 @@ namespace System.Text.Json.SourceGeneration.Tests
     public sealed class CollectionTests_Metadata_AsyncStream : CollectionTests_Metadata
     {
         public CollectionTests_Metadata_AsyncStream()
-            : base(new AsyncStreamSerializerWrapper(CollectionTestsContext_Metadata.Default, (options) => new CollectionTestsContext_Metadata(options)))
+            : base(new AsyncStreamSerializerWrapper(CollectionTestsContext_Metadata.Default))
         {
         }
     }
@@ -440,7 +440,7 @@ namespace System.Text.Json.SourceGeneration.Tests
     public partial class CollectionTests_Default : CollectionTests
     {
         public CollectionTests_Default()
-            : base(new StringSerializerWrapper(CollectionTestsContext_Default.Default, (options) => new CollectionTestsContext_Default(options)))
+            : base(new StringSerializerWrapper(CollectionTestsContext_Default.Default))
         {
         }
 
index c76cad3..5c5d2e7 100644 (file)
@@ -11,7 +11,7 @@ namespace System.Text.Json.SourceGeneration.Tests
     public sealed class ConstructorTests_Metadata_String : ConstructorTests_Metadata
     {
         public ConstructorTests_Metadata_String()
-            : base(new StringSerializerWrapper(ConstructorTestsContext_Metadata.Default, (options) => new ConstructorTestsContext_Metadata(options)))
+            : base(new StringSerializerWrapper(ConstructorTestsContext_Metadata.Default))
         {
         }
     }
@@ -19,7 +19,7 @@ namespace System.Text.Json.SourceGeneration.Tests
     public sealed class ConstructorTests_Metadata_AsyncStream : ConstructorTests_Metadata
     {
         public ConstructorTests_Metadata_AsyncStream()
-            : base(new AsyncStreamSerializerWrapper(ConstructorTestsContext_Metadata.Default, (options) => new ConstructorTestsContext_Metadata(options)))
+            : base(new AsyncStreamSerializerWrapper(ConstructorTestsContext_Metadata.Default))
         {
         }
     }
@@ -149,7 +149,7 @@ namespace System.Text.Json.SourceGeneration.Tests
     public sealed class ConstructorTests_Default_String : ConstructorTests_Default
     {
         public ConstructorTests_Default_String()
-            : base(new StringSerializerWrapper(ConstructorTestsContext_Default.Default, (options) => new ConstructorTestsContext_Default(options)))
+            : base(new StringSerializerWrapper(ConstructorTestsContext_Default.Default))
         {
         }
     }
@@ -157,7 +157,7 @@ namespace System.Text.Json.SourceGeneration.Tests
     public sealed class ConstructorTests_Default_AsyncStream : ConstructorTests_Default
     {
         public ConstructorTests_Default_AsyncStream()
-            : base(new AsyncStreamSerializerWrapper(ConstructorTestsContext_Default.Default, (options) => new ConstructorTestsContext_Default(options)))
+            : base(new AsyncStreamSerializerWrapper(ConstructorTestsContext_Default.Default))
         {
         }
     }
index 22b67e7..6d5d898 100644 (file)
@@ -11,7 +11,7 @@ namespace System.Text.Json.SourceGeneration.Tests
     public sealed partial class ExtensionDataTests_Metadata : ExtensionDataTests
     {
         public ExtensionDataTests_Metadata()
-            : base(new StringSerializerWrapper(ExtensionDataTestsContext_Metadata.Default, (options) => new ExtensionDataTestsContext_Metadata(options)))
+            : base(new StringSerializerWrapper(ExtensionDataTestsContext_Metadata.Default))
         {
         }
 
@@ -72,7 +72,7 @@ namespace System.Text.Json.SourceGeneration.Tests
     public sealed partial class ExtensionDataTests_Default : ExtensionDataTests
     {
         public ExtensionDataTests_Default()
-            : base(new StringSerializerWrapper(ExtensionDataTestsContext_Default.Default, (options) => new ExtensionDataTestsContext_Default(options)))
+            : base(new StringSerializerWrapper(ExtensionDataTestsContext_Default.Default))
         {
         }
 
index baa7377..6a81883 100644 (file)
@@ -12,144 +12,125 @@ namespace System.Text.Json.SourceGeneration.Tests
     internal sealed class StringSerializerWrapper : JsonSerializerWrapper
     {
         private readonly JsonSerializerContext _defaultContext;
-        private readonly Func<JsonSerializerOptions, JsonSerializerContext> _customContextCreator;
 
-        public StringSerializerWrapper(JsonSerializerContext defaultContext, Func<JsonSerializerOptions, JsonSerializerContext> customContextCreator)
+        public StringSerializerWrapper(JsonSerializerContext defaultContext)
         {
             _defaultContext = defaultContext ?? throw new ArgumentNullException(nameof(defaultContext));
-            _customContextCreator = customContextCreator ?? throw new ArgumentNullException(nameof(customContextCreator));
         }
 
+        public override JsonSerializerOptions DefaultOptions => _defaultContext.Options;
+
         public override Task<string> SerializeWrapper(object value, Type type, JsonSerializerOptions? options = null)
-        {
-            JsonSerializerContext context = GetJsonSerializerContext(options);
-            return Task.FromResult(JsonSerializer.Serialize(value, type, context));
-        }
+            => Task.FromResult(JsonSerializer.Serialize(value, type, GetOptions(options)));
 
         public override Task<string> SerializeWrapper<T>(T value, JsonSerializerOptions? options = null)
-        {
-            Type runtimeType = GetRuntimeType(value);
-
-            if (runtimeType != typeof(T))
-            {
-                return SerializeWrapper(value, runtimeType, options);
-            }
-
-            JsonSerializerContext context = GetJsonSerializerContext(options);
-            JsonTypeInfo<T> typeInfo = (JsonTypeInfo<T>)context.GetTypeInfo(typeof(T));
-            return Task.FromResult(JsonSerializer.Serialize(value, typeInfo));
-        }
+            => Task.FromResult(JsonSerializer.Serialize(value, GetOptions(options)));
 
         public override Task<string> SerializeWrapper(object value, Type inputType, JsonSerializerContext context)
-            => throw new NotImplementedException();
+            => Task.FromResult(JsonSerializer.Serialize(value, inputType, context));
 
         public override Task<string> SerializeWrapper<T>(T value, JsonTypeInfo<T> jsonTypeInfo)
-            => throw new NotImplementedException();
+            => Task.FromResult(JsonSerializer.Serialize(value, jsonTypeInfo));
 
         public override Task<string> SerializeWrapper(object value, JsonTypeInfo jsonTypeInfo)
-            => throw new NotImplementedException();
+            => Task.FromResult(JsonSerializer.Serialize(value, jsonTypeInfo));
 
         public override Task<T> DeserializeWrapper<T>(string json, JsonSerializerOptions? options = null)
-        {
-            JsonSerializerContext context = GetJsonSerializerContext(options);
-            JsonTypeInfo<T> typeInfo = (JsonTypeInfo<T>)context.GetTypeInfo(typeof(T));
-            return Task.FromResult(JsonSerializer.Deserialize<T>(json, typeInfo));
-        }
+            => Task.FromResult(JsonSerializer.Deserialize<T>(json, GetOptions(options)));
 
         public override Task<object> DeserializeWrapper(string json, Type type, JsonSerializerOptions? options = null)
-        {
-            JsonSerializerContext context = GetJsonSerializerContext(options);
-            return Task.FromResult(JsonSerializer.Deserialize(json, type, context));
-        }
+            => Task.FromResult(JsonSerializer.Deserialize(json, type, GetOptions(options)));
 
         public override Task<T> DeserializeWrapper<T>(string json, JsonTypeInfo<T> jsonTypeInfo)
-            => throw new NotImplementedException();
+            => Task.FromResult(JsonSerializer.Deserialize(json, jsonTypeInfo));
 
         public override Task<object> DeserializeWrapper(string json, JsonTypeInfo jsonTypeInfo)
-            => throw new NotImplementedException();
+            => Task.FromResult(JsonSerializer.Deserialize(json, jsonTypeInfo));
 
         public override Task<object> DeserializeWrapper(string json, Type type, JsonSerializerContext context)
-            => throw new NotImplementedException();
-
-        private JsonSerializerContext GetJsonSerializerContext(JsonSerializerOptions? options)
-             => options is null ? _defaultContext : _customContextCreator(new JsonSerializerOptions(options));
+            => Task.FromResult(JsonSerializer.Deserialize(json, type, context));
 
-        private Type GetRuntimeType<TValue>(in TValue value)
+        private JsonSerializerOptions GetOptions(JsonSerializerOptions? options = null)
         {
-            if (typeof(TValue) == typeof(object) && value != null)
+            if (options is null)
+            {
+                return _defaultContext.Options;
+            }
+
+            if (options.TypeInfoResolver is null or DefaultJsonTypeInfoResolver { Modifiers.Count: 0 })
             {
-                return value.GetType();
+                return new JsonSerializerOptions(options) { TypeInfoResolver = _defaultContext };
             }
 
-            return typeof(TValue);
+            return options;
         }
     }
 
     internal sealed class AsyncStreamSerializerWrapper : StreamingJsonSerializerWrapper
     {
         private readonly JsonSerializerContext _defaultContext;
-        private readonly Func<JsonSerializerOptions, JsonSerializerContext> _customContextCreator;
 
+        public override JsonSerializerOptions DefaultOptions => _defaultContext.Options;
         public override bool IsAsyncSerializer => true;
 
-        public AsyncStreamSerializerWrapper(JsonSerializerContext defaultContext, Func<JsonSerializerOptions, JsonSerializerContext> customContextCreator)
+        public AsyncStreamSerializerWrapper(JsonSerializerContext defaultContext)
         {
             _defaultContext = defaultContext ?? throw new ArgumentNullException(nameof(defaultContext));
-            _customContextCreator = customContextCreator ?? throw new ArgumentNullException(nameof(customContextCreator));
         }
 
         public override async Task<T> DeserializeWrapper<T>(Stream utf8Json, JsonSerializerOptions? options = null)
         {
-            JsonSerializerContext context = GetJsonSerializerContext(options);
-            JsonTypeInfo<T> typeInfo = (JsonTypeInfo<T>)context.GetTypeInfo(typeof(T));
-            return await JsonSerializer.DeserializeAsync<T>(utf8Json, typeInfo);
+            return await JsonSerializer.DeserializeAsync<T>(utf8Json, GetOptions(options));
         }
 
         public override async Task<object> DeserializeWrapper(Stream utf8Json, Type returnType, JsonSerializerOptions options = null)
         {
-            JsonSerializerContext context = GetJsonSerializerContext(options);
-            return await JsonSerializer.DeserializeAsync(utf8Json, returnType, context);
+            return await JsonSerializer.DeserializeAsync(utf8Json, returnType, GetOptions(options));
         }
 
-        public override Task<T> DeserializeWrapper<T>(Stream utf8Json, JsonTypeInfo<T> jsonTypeInfo) => throw new NotImplementedException();
-        public override Task<object> DeserializeWrapper(Stream utf8Json, JsonTypeInfo jsonTypeInfo) => throw new NotImplementedException();
-
-        public override async Task SerializeWrapper<T>(Stream stream, T value, JsonSerializerOptions options = null)
+        public override async Task<T> DeserializeWrapper<T>(Stream utf8Json, JsonTypeInfo<T> jsonTypeInfo)
         {
-            Type runtimeType = GetRuntimeType(value);
-            if (runtimeType != typeof(T))
-            {
-                await JsonSerializer.SerializeAsync(stream, value, runtimeType, options);
-                return;
-            }
+            return await JsonSerializer.DeserializeAsync<T>(utf8Json, jsonTypeInfo);
+        }
 
-            JsonSerializerContext context = GetJsonSerializerContext(options);
-            JsonTypeInfo<T> typeInfo = (JsonTypeInfo<T>)context.GetTypeInfo(typeof(T));
-            await JsonSerializer.SerializeAsync<T>(stream, value, typeInfo);
+        public override async Task<object> DeserializeWrapper(Stream utf8Json, JsonTypeInfo jsonTypeInfo)
+        {
+            return await JsonSerializer.DeserializeAsync(utf8Json, jsonTypeInfo);
         }
 
-        public override async Task SerializeWrapper(Stream stream, object value, Type inputType, JsonSerializerOptions options = null)
+        public override async Task<object> DeserializeWrapper(Stream utf8Json, Type returnType, JsonSerializerContext context)
         {
-            JsonSerializerContext context = GetJsonSerializerContext(options);
-            await JsonSerializer.SerializeAsync(stream, value, inputType, context);
+            return await JsonSerializer.DeserializeAsync(utf8Json, returnType, context);
         }
 
-        public override Task SerializeWrapper<T>(Stream stream, T value, JsonTypeInfo<T> jsonTypeInfo) => throw new NotImplementedException();
-        public override Task SerializeWrapper(Stream stream, object value, JsonTypeInfo jsonTypeInfo) => throw new NotImplementedException();
-        public override Task SerializeWrapper(Stream stream, object value, Type inputType, JsonSerializerContext context) => throw new NotImplementedException();
-        public override Task<object> DeserializeWrapper(Stream utf8Json, Type returnType, JsonSerializerContext context) => throw new NotImplementedException();
+        public override Task SerializeWrapper<T>(Stream stream, T value, JsonSerializerOptions options = null)
+            => JsonSerializer.SerializeAsync<T>(stream, value, GetOptions(options));
+
+        public override Task SerializeWrapper(Stream stream, object value, Type inputType, JsonSerializerOptions options = null)
+            => JsonSerializer.SerializeAsync(stream, value, inputType, GetOptions(options));
+
+        public override Task SerializeWrapper<T>(Stream stream, T value, JsonTypeInfo<T> jsonTypeInfo)
+            => JsonSerializer.SerializeAsync(stream, value, jsonTypeInfo);
+
+        public override Task SerializeWrapper(Stream stream, object value, JsonTypeInfo jsonTypeInfo)
+            => JsonSerializer.SerializeAsync(stream, value, jsonTypeInfo);
 
-        private JsonSerializerContext GetJsonSerializerContext(JsonSerializerOptions? options)
-            => options is null ? _defaultContext : _customContextCreator(new JsonSerializerOptions(options));
+        public override Task SerializeWrapper(Stream stream, object value, Type inputType, JsonSerializerContext context)
+            => JsonSerializer.SerializeAsync(stream, value, inputType, context);
 
-        private Type GetRuntimeType<TValue>(in TValue value)
+        private JsonSerializerOptions GetOptions(JsonSerializerOptions? options = null)
         {
-            if (typeof(TValue) == typeof(object) && value != null)
+            if (options is null)
+            {
+                return _defaultContext.Options;
+            }
+
+            if (options.TypeInfoResolver is null or DefaultJsonTypeInfoResolver { Modifiers.Count: 0 })
             {
-                return value.GetType();
+                return new JsonSerializerOptions(options) { TypeInfoResolver = _defaultContext };
             }
 
-            return typeof(TValue);
+            return options;
         }
     }
 }
index 4a27daa..2028547 100644 (file)
@@ -12,7 +12,7 @@ namespace System.Text.Json.SourceGeneration.Tests
     public sealed partial class NodeInteropTests_Metadata : NodeInteropTests
     {
         public NodeInteropTests_Metadata()
-            : base(new StringSerializerWrapper(NodeInteropTestsContext_Metadata.Default, (options) => new NodeInteropTestsContext_Metadata(options)))
+            : base(new StringSerializerWrapper(NodeInteropTestsContext_Metadata.Default))
         {
         }
 
@@ -29,7 +29,7 @@ namespace System.Text.Json.SourceGeneration.Tests
     public sealed partial class NodeInteropTests_Default : NodeInteropTests
     {
         public NodeInteropTests_Default()
-            : base(new StringSerializerWrapper(NodeInteropTestsContext_Default.Default, (options) => new NodeInteropTestsContext_Default(options)))
+            : base(new StringSerializerWrapper(NodeInteropTestsContext_Default.Default))
         {
         }
 
index 1beb220..82566bf 100644 (file)
@@ -10,7 +10,7 @@ namespace System.Text.Json.SourceGeneration.Tests
     public sealed partial class PropertyNameTests_Metadata : PropertyNameTests
     {
         public PropertyNameTests_Metadata()
-            : base(new StringSerializerWrapper(PropertyNameTestsContext_Metadata.Default, (options) => new PropertyNameTestsContext_Metadata(options)))
+            : base(new StringSerializerWrapper(PropertyNameTestsContext_Metadata.Default))
         {
         }
 
@@ -36,7 +36,7 @@ namespace System.Text.Json.SourceGeneration.Tests
     public sealed partial class PropertyNameTests_Default : PropertyNameTests
     {
         public PropertyNameTests_Default()
-            : base(new StringSerializerWrapper(PropertyNameTestsContext_Default.Default, (options) => new PropertyNameTestsContext_Default(options)))
+            : base(new StringSerializerWrapper(PropertyNameTestsContext_Default.Default))
         {
         }
 
index f43228d..b63c3ea 100644 (file)
@@ -14,7 +14,7 @@ namespace System.Text.Json.SourceGeneration.Tests
     public partial class PropertyVisibilityTests_Metadata : PropertyVisibilityTests
     {
         public PropertyVisibilityTests_Metadata()
-            : this(new StringSerializerWrapper(PropertyVisibilityTestsContext_Metadata.Default, (options) => new PropertyVisibilityTestsContext_Metadata(options)))
+            : this(new StringSerializerWrapper(PropertyVisibilityTestsContext_Metadata.Default))
         {
         }
 
@@ -61,27 +61,13 @@ namespace System.Text.Json.SourceGeneration.Tests
         }
 
         [Theory]
-        [InlineData(typeof(ClassWithInitOnlyProperty))]
-        [InlineData(typeof(StructWithInitOnlyProperty))]
-        public override async Task InitOnlyProperties(Type type)
-        {
-            PropertyInfo property = type.GetProperty("MyInt");
-
-            // Init-only properties can be serialized.
-            object obj = Activator.CreateInstance(type);
-            property.SetValue(obj, 1);
-            Assert.Equal(@"{""MyInt"":1}", await Serializer.SerializeWrapper(obj, type));
-
-            // Deserializing init-only properties is not supported.
-            await Assert.ThrowsAsync<InvalidOperationException>(async () => await Serializer.DeserializeWrapper(@"{""MyInt"":1}", type));
-        }
-
-        [Theory]
         [InlineData(typeof(Class_PropertyWith_PrivateInitOnlySetter_WithAttribute))]
         [InlineData(typeof(Class_PropertyWith_InternalInitOnlySetter_WithAttribute))]
         [InlineData(typeof(Class_PropertyWith_ProtectedInitOnlySetter_WithAttribute))]
         public override async Task NonPublicInitOnlySetter_With_JsonInclude(Type type)
         {
+            bool isDeserializationSupported = type == typeof(Class_PropertyWith_InternalInitOnlySetter_WithAttribute);
+
             PropertyInfo property = type.GetProperty("MyInt");
 
             // Init-only properties can be serialized.
@@ -89,8 +75,16 @@ namespace System.Text.Json.SourceGeneration.Tests
             property.SetValue(obj, 1);
             Assert.Equal(@"{""MyInt"":1}", await Serializer.SerializeWrapper(obj, type));
 
-            // Deserializing init-only properties is not supported.
-            await Assert.ThrowsAsync<InvalidOperationException>(async () => await Serializer.DeserializeWrapper(@"{""MyInt"":1}", type));
+            // Deserializing JsonInclude is only supported for internal properties
+            if (isDeserializationSupported)
+            {
+                obj = await Serializer.DeserializeWrapper(@"{""MyInt"":1}", type);
+                Assert.Equal(1, (int)type.GetProperty("MyInt").GetValue(obj));
+            }
+            else
+            {
+                await Assert.ThrowsAsync<InvalidOperationException>(async () => await Serializer.DeserializeWrapper(@"{""MyInt"":1}", type));
+            }
         }
 
         [Fact]
@@ -294,7 +288,7 @@ namespace System.Text.Json.SourceGeneration.Tests
     public partial class PropertyVisibilityTests_Default : PropertyVisibilityTests_Metadata
     {
         public PropertyVisibilityTests_Default()
-            : base(new StringSerializerWrapper(PropertyVisibilityTestsContext_Default.Default, (options) => new PropertyVisibilityTestsContext_Default(options)))
+            : base(new StringSerializerWrapper(PropertyVisibilityTestsContext_Default.Default))
         {
         }
 
index de4c408..8c073e1 100644 (file)
@@ -13,7 +13,7 @@ namespace System.Text.Json.SourceGeneration.Tests
     public sealed class ReferenceHandlerTests_IgnoreCycles_Metadata_String : ReferenceHandlerTests_IgnoreCycles_Metadata
     {
         public ReferenceHandlerTests_IgnoreCycles_Metadata_String()
-            : base(new StringSerializerWrapper(ReferenceHandlerTests_IgnoreCyclesContext_Metadata.Default, (options) => new ReferenceHandlerTests_IgnoreCyclesContext_Metadata(options)))
+            : base(new StringSerializerWrapper(ReferenceHandlerTests_IgnoreCyclesContext_Metadata.Default))
         {
         }
     }
@@ -21,7 +21,7 @@ namespace System.Text.Json.SourceGeneration.Tests
     public sealed class ReferenceHandlerTests_IgnoreCycles_Metadata_AsyncStream : ReferenceHandlerTests_IgnoreCycles_Metadata
     {
         public ReferenceHandlerTests_IgnoreCycles_Metadata_AsyncStream()
-            : base(new AsyncStreamSerializerWrapper(ReferenceHandlerTests_IgnoreCyclesContext_Metadata.Default, (options) => new ReferenceHandlerTests_IgnoreCyclesContext_Metadata(options)))
+            : base(new AsyncStreamSerializerWrapper(ReferenceHandlerTests_IgnoreCyclesContext_Metadata.Default))
         {
         }
     }
@@ -97,7 +97,7 @@ namespace System.Text.Json.SourceGeneration.Tests
     public sealed class ReferenceHandlerTests_IgnoreCycles_Default_String : ReferenceHandlerTests_IgnoreCycles_Default
     {
         public ReferenceHandlerTests_IgnoreCycles_Default_String()
-            : base(new StringSerializerWrapper(ReferenceHandlerTests_IgnoreCyclesContext_Default.Default, (options) => new ReferenceHandlerTests_IgnoreCyclesContext_Default(options)))
+            : base(new StringSerializerWrapper(ReferenceHandlerTests_IgnoreCyclesContext_Default.Default))
         {
         }
     }
@@ -105,7 +105,7 @@ namespace System.Text.Json.SourceGeneration.Tests
     public sealed class ReferenceHandlerTests_IgnoreCycles_Default_AsyncStream : ReferenceHandlerTests_IgnoreCycles_Default
     {
         public ReferenceHandlerTests_IgnoreCycles_Default_AsyncStream()
-            : base(new AsyncStreamSerializerWrapper(ReferenceHandlerTests_IgnoreCyclesContext_Default.Default, (options) => new ReferenceHandlerTests_IgnoreCyclesContext_Default(options)))
+            : base(new AsyncStreamSerializerWrapper(ReferenceHandlerTests_IgnoreCyclesContext_Default.Default))
         {
         }
     }
index 9c2996b..001ecf9 100644 (file)
@@ -13,7 +13,7 @@ namespace System.Text.Json.SourceGeneration.Tests
     public sealed class ReferenceHandlerTests_Metadata_String : ReferenceHandlerTests_Metadata
     {
         public ReferenceHandlerTests_Metadata_String()
-            : base(new StringSerializerWrapper(ReferenceHandlerTestsContext_Metadata.Default, (options) => new ReferenceHandlerTestsContext_Metadata(options)))
+            : base(new StringSerializerWrapper(ReferenceHandlerTestsContext_Metadata.Default))
         {
         }
     }
@@ -21,7 +21,7 @@ namespace System.Text.Json.SourceGeneration.Tests
     public sealed class ReferenceHandlerTests_Metadata_AsyncStream : ReferenceHandlerTests_Metadata
     {
         public ReferenceHandlerTests_Metadata_AsyncStream()
-            : base(new AsyncStreamSerializerWrapper(ReferenceHandlerTestsContext_Metadata.Default, (options) => new ReferenceHandlerTestsContext_Metadata(options)))
+            : base(new AsyncStreamSerializerWrapper(ReferenceHandlerTestsContext_Metadata.Default))
         {
         }
     }
@@ -143,7 +143,7 @@ namespace System.Text.Json.SourceGeneration.Tests
     public sealed class ReferenceHandlerTests_Default_String : ReferenceHandlerTests_Default
     {
         public ReferenceHandlerTests_Default_String()
-            : base(new StringSerializerWrapper(ReferenceHandlerTestsContext_Default.Default, (options) => new ReferenceHandlerTestsContext_Default(options)))
+            : base(new StringSerializerWrapper(ReferenceHandlerTestsContext_Default.Default))
         {
         }
 
@@ -160,7 +160,7 @@ namespace System.Text.Json.SourceGeneration.Tests
     public sealed class ReferenceHandlerTests_Default_AsyncStream : ReferenceHandlerTests_Default
     {
         public ReferenceHandlerTests_Default_AsyncStream()
-            : base(new AsyncStreamSerializerWrapper(ReferenceHandlerTestsContext_Default.Default, (options) => new ReferenceHandlerTestsContext_Default(options)))
+            : base(new AsyncStreamSerializerWrapper(ReferenceHandlerTestsContext_Default.Default))
         {
         }
     }
diff --git a/src/libraries/System.Text.Json/tests/System.Text.Json.SourceGeneration.Tests/Serialization/RequiredKeywordTests.cs b/src/libraries/System.Text.Json/tests/System.Text.Json.SourceGeneration.Tests/Serialization/RequiredKeywordTests.cs
new file mode 100644 (file)
index 0000000..64a946c
--- /dev/null
@@ -0,0 +1,35 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using System.Collections.Generic;
+using System.Reflection;
+using System.Text.Json.Serialization;
+using System.Text.Json.Serialization.Metadata;
+using System.Text.Json.Serialization.Tests;
+using System.Threading.Tasks;
+using Xunit;
+
+namespace System.Text.Json.SourceGeneration.Tests
+{
+    public partial class RequiredKeywordTests_SourceGen : RequiredKeywordTests
+    {
+        public RequiredKeywordTests_SourceGen()
+            : base(new StringSerializerWrapper(RequiredKeywordTestsContext.Default))
+        {
+        }
+
+        [JsonSerializable(typeof(PersonWithRequiredMembers))]
+        [JsonSerializable(typeof(PersonWithRequiredMembersAndSmallParametrizedCtor))]
+        [JsonSerializable(typeof(PersonWithRequiredMembersAndLargeParametrizedCtor))]
+        [JsonSerializable(typeof(PersonWithRequiredMembersAndSetsRequiredMembers))]
+        [JsonSerializable(typeof(PersonWithRequiredMembersAndSmallParametrizedCtorAndSetsRequiredMembers))]
+        [JsonSerializable(typeof(PersonWithRequiredMembersAndLargeParametrizedCtorAndSetsRequiredMembers))]
+        [JsonSerializable(typeof(ClassWithInitOnlyRequiredProperty))]
+        [JsonSerializable(typeof(ClassWithRequiredField))]
+        [JsonSerializable(typeof(ClassWithRequiredExtensionDataProperty))]
+        [JsonSerializable(typeof(ClassWithRequiredKeywordAndJsonRequiredCustomAttribute))]
+        internal sealed partial class RequiredKeywordTestsContext : JsonSerializerContext
+        {
+        }
+    }
+}
index c54ad18..5a8d889 100644 (file)
@@ -12,9 +12,7 @@ namespace System.Text.Json.SourceGeneration.Tests
     public sealed partial class UnsupportedTypesTests_Metadata : UnsupportedTypesTests
     {
         public UnsupportedTypesTests_Metadata() : base(
-            new StringSerializerWrapper(
-                UnsupportedTypesTestsContext_Metadata.Default,
-                (options) => new UnsupportedTypesTestsContext_Metadata(options)),
+            new StringSerializerWrapper(UnsupportedTypesTestsContext_Metadata.Default),
             supportsJsonPathOnSerialize: true)
         {
         }
@@ -52,9 +50,7 @@ namespace System.Text.Json.SourceGeneration.Tests
     public sealed partial class UnsupportedTypesTests_Default : UnsupportedTypesTests
     {
         public UnsupportedTypesTests_Default() : base(
-            new StringSerializerWrapper(
-                UnsupportedTypesTestsContext_Default.Default,
-                (options) => new UnsupportedTypesTestsContext_Default(options)),
+            new StringSerializerWrapper(UnsupportedTypesTestsContext_Default.Default),
             supportsJsonPathOnSerialize: false)
         {
         }
@@ -1,14 +1,18 @@
 <Project Sdk="Microsoft.NET.Sdk">
 
+  <PropertyGroup>
+    <RoslynSupportsRequiredAttributes>true</RoslynSupportsRequiredAttributes>
+  </PropertyGroup>
+
   <ItemGroup Condition="'$(ContinuousIntegrationBuild)' == 'true'">
     <HighAotMemoryUsageAssembly Include="Microsoft.CodeAnalysis.CSharp.dll" />
-    <HighAotMemoryUsageAssembly Include="System.Text.Json.SourceGeneration.Roslyn4.0.Tests.dll"/>
+    <HighAotMemoryUsageAssembly Include="System.Text.Json.SourceGeneration.Roslyn4.4.Tests.dll" />
   </ItemGroup>
 
   <Import Project="System.Text.Json.SourceGeneration.Tests.targets" />
 
   <ItemGroup>
-    <ProjectReference Include="..\..\gen\System.Text.Json.SourceGeneration.Roslyn4.0.csproj" OutputItemType="Analyzer" ReferenceOutputAssembly="false" />
-    <ProjectReference Include="..\System.Text.Json.SourceGeneration.TestLibrary\System.Text.Json.TestLibrary.Roslyn4.0.csproj" />
+    <ProjectReference Include="..\..\gen\System.Text.Json.SourceGeneration.Roslyn4.4.csproj" OutputItemType="Analyzer" ReferenceOutputAssembly="false" SetTargetFramework="TargetFramework=netstandard2.0" SkipGetTargetFrameworkProperties="true" />
+    <ProjectReference Include="..\System.Text.Json.SourceGeneration.TestLibrary\System.Text.Json.TestLibrary.Roslyn4.4.csproj" />
   </ItemGroup>
 </Project>
index 1b9e0d1..cce8110 100644 (file)
@@ -16,6 +16,9 @@
 
   <ItemGroup Condition="'$(TargetFrameworkIdentifier)' != '.NETCoreApp'">
     <Compile Include="$(CoreLibSharedDir)System\Runtime\Versioning\RequiresPreviewFeaturesAttribute.cs" Link="System\Runtime\Versioning\RequiresPreviewFeaturesAttribute.cs" />
+    <Compile Include="$(CoreLibSharedDir)System\Runtime\CompilerServices\CompilerFeatureRequiredAttribute.cs" Link="Common\System\Runtime\CompilerServices\CompilerFeatureRequiredAttribute.cs" />
+    <Compile Include="$(CoreLibSharedDir)System\Runtime\CompilerServices\RequiredMemberAttribute.cs" Link="Common\System\Runtime\CompilerServices\RequiredMemberAttribute.cs" />
+    <Compile Include="$(CoreLibSharedDir)System\Diagnostics\CodeAnalysis\SetsRequiredMembersAttribute.cs" Link="Common\System\Diagnostics\CodeAnalysis\SetsRequiredMembersAttribute.cs" />
   </ItemGroup>
 
   <ItemGroup>
     <Compile Include="TestClasses.CustomConverters.cs" />
   </ItemGroup>
 
+  <ItemGroup Condition="'$(RoslynSupportsRequiredAttributes)' == 'true'">
+   <Compile Include="..\Common\RequiredKeywordTests.cs" Link="CommonTest\System\Text\Json\Tests\Serialization\RequiredKeywordTests.cs" /> 
+   <Compile Include="Serialization\RequiredKeywordTests.cs" />
+  </ItemGroup>
+
   <ItemGroup Condition="'$(TargetFrameworkIdentifier)' == '.NETFramework'">
     <ProjectReference Include="..\..\src\System.Text.Json.csproj" />
     <ProjectReference Include="$(LibrariesProjectRoot)System.Collections.Immutable\src\System.Collections.Immutable.csproj" />
index 7121ed7..c46a991 100644 (file)
@@ -365,6 +365,34 @@ namespace System.Text.Json.SourceGeneration.UnitTests
             return CreateCompilation(source);
         }
 
+        public static Compilation CreateCompilationWithRequiredProperties()
+        {
+            string source = @"
+            using System;
+            using System.Text.Json.Serialization;
+
+            namespace HelloWorld
+            {
+                public class MyClass
+                {
+                    public required string Required1 { get; set; }
+                    public required string Required2 { get; set; }
+
+                    public MyClass(string required1)
+                    {
+                        Required1 = required1;
+                    }
+                }
+
+                [JsonSerializable(typeof(MyClass))]
+                public partial class MyJsonContext : JsonSerializerContext
+                {
+                }
+            }";
+
+            return CreateCompilation(source);
+        }
+
         public static Compilation CreateCompilationWithRecordPositionalParameters()
         {
             string source = @"
index c24261e..7f6d1b4 100644 (file)
@@ -2,6 +2,7 @@
 // The .NET Foundation licenses this file to you under the MIT license.
 
 using System.Collections.Generic;
+using System.Diagnostics;
 using System.IO;
 using System.Linq;
 using Microsoft.CodeAnalysis;
@@ -291,21 +292,14 @@ namespace System.Text.Json.SourceGeneration.UnitTests
         }
 
         [Fact]
-        public void WarnOnClassesWithInitOnlyProperties()
+        public void DoNotWarnOnClassesWithInitOnlyProperties()
         {
             Compilation compilation = CompilationHelper.CreateCompilationWithInitOnlyProperties();
             JsonSourceGenerator generator = new JsonSourceGenerator();
             CompilationHelper.RunGenerators(compilation, out var generatorDiags, generator);
 
-            Location location = compilation.GetSymbolsWithName("Id").First().Locations[0];
-
-            (Location, string)[] expectedWarningDiagnostics = new (Location, string)[]
-            {
-                (location, "The type 'Location' defines init-only properties, deserialization of which is currently not supported in source generation mode.")
-            };
-
             CompilationHelper.CheckDiagnosticMessages(DiagnosticSeverity.Info, generatorDiags, Array.Empty<(Location, string)>());
-            CompilationHelper.CheckDiagnosticMessages(DiagnosticSeverity.Warning, generatorDiags, expectedWarningDiagnostics);
+            CompilationHelper.CheckDiagnosticMessages(DiagnosticSeverity.Warning, generatorDiags, Array.Empty<(Location, string)>());
             CompilationHelper.CheckDiagnosticMessages(DiagnosticSeverity.Error, generatorDiags, Array.Empty<(Location, string)>());
         }
 
@@ -322,21 +316,14 @@ namespace System.Text.Json.SourceGeneration.UnitTests
         }
         
         [Fact]
-        public void WarnOnClassesWithMixedInitOnlyProperties()
+        public void DoNotWarnOnClassesWithMixedInitOnlyProperties()
         {
             Compilation compilation = CompilationHelper.CreateCompilationWithMixedInitOnlyProperties();
             JsonSourceGenerator generator = new JsonSourceGenerator();
             CompilationHelper.RunGenerators(compilation, out var generatorDiags, generator);
 
-            Location location = compilation.GetSymbolsWithName("Orphaned").First().Locations[0];
-
-            (Location, string)[] expectedWarningDiagnostics = new (Location, string)[]
-            {
-                (location, "The type 'MyClass' defines init-only properties, deserialization of which is currently not supported in source generation mode.")
-            };
-
             CompilationHelper.CheckDiagnosticMessages(DiagnosticSeverity.Info, generatorDiags, Array.Empty<(Location, string)>());
-            CompilationHelper.CheckDiagnosticMessages(DiagnosticSeverity.Warning, generatorDiags, expectedWarningDiagnostics);
+            CompilationHelper.CheckDiagnosticMessages(DiagnosticSeverity.Warning, generatorDiags, Array.Empty<(Location, string)>());
             CompilationHelper.CheckDiagnosticMessages(DiagnosticSeverity.Error, generatorDiags, Array.Empty<(Location, string)>());
         }
 
@@ -353,6 +340,18 @@ namespace System.Text.Json.SourceGeneration.UnitTests
         }
 
         [Fact]
+        public void DoNotWarnOnClassesWithRequiredProperties()
+        {
+            Compilation compilation = CompilationHelper.CreateCompilationWithRequiredProperties();
+            JsonSourceGenerator generator = new JsonSourceGenerator();
+            Compilation newCompilation = CompilationHelper.RunGenerators(compilation, out var generatorDiags, generator);
+
+            CompilationHelper.CheckDiagnosticMessages(DiagnosticSeverity.Info, generatorDiags, Array.Empty<(Location, string)>());
+            CompilationHelper.CheckDiagnosticMessages(DiagnosticSeverity.Warning, generatorDiags, Array.Empty<(Location, string)>());
+            CompilationHelper.CheckDiagnosticMessages(DiagnosticSeverity.Error, generatorDiags, Array.Empty<(Location, string)>());
+        }
+
+        [Fact]
         public void WarnOnClassesWithInaccessibleJsonIncludeProperties()
         {
             Compilation compilation = CompilationHelper.CreateCompilationWithInaccessibleJsonIncludeProperties();
@@ -1,7 +1,7 @@
 <Project Sdk="Microsoft.NET.Sdk">
   <PropertyGroup>
-    <RoslynApiVersion>$(MicrosoftCodeAnalysisVersion_4_0)</RoslynApiVersion>
-    <DefineConstants>$(DefineConstants);ROSLYN4_0_OR_GREATER</DefineConstants>
+    <RoslynApiVersion>$(MicrosoftCodeAnalysisVersion_4_4)</RoslynApiVersion>
+    <DefineConstants>$(DefineConstants);ROSLYN4_0_OR_GREATER;ROSLYN4_4_OR_GREATER</DefineConstants>
     <IsHighAotMemoryUsageTest>true</IsHighAotMemoryUsageTest>
   </PropertyGroup>
 
@@ -12,6 +12,6 @@
   <Import Project="System.Text.Json.SourceGeneration.Unit.Tests.targets" />
 
   <ItemGroup>
-    <ProjectReference Include="..\..\gen\System.Text.Json.SourceGeneration.Roslyn4.0.csproj" />
+    <ProjectReference Include="..\..\gen\System.Text.Json.SourceGeneration.Roslyn4.4.csproj" />
   </ItemGroup>
 </Project>
index 886f96f..6a59bf1 100644 (file)
@@ -26,6 +26,7 @@ namespace System.Text.Json.Serialization.Tests
 
         private class SpanSerializerWrapper : JsonSerializerWrapper
         {
+            public override JsonSerializerOptions DefaultOptions => JsonSerializerOptions.Default;
             public override bool SupportsNullValueOnDeserialize => true; // a 'null' value is supported via implicit operator.
 
             public override Task<string> SerializeWrapper(object value, Type inputType, JsonSerializerOptions options = null)
@@ -86,6 +87,7 @@ namespace System.Text.Json.Serialization.Tests
 
         private class StringSerializerWrapper : JsonSerializerWrapper
         {
+            public override JsonSerializerOptions DefaultOptions => JsonSerializerOptions.Default;
             public override bool SupportsNullValueOnDeserialize => true;
 
             public override Task<string> SerializeWrapper(object value, Type inputType, JsonSerializerOptions options = null)
@@ -144,6 +146,7 @@ namespace System.Text.Json.Serialization.Tests
             private readonly bool _forceSmallBufferInOptions;
             private readonly bool _forceBomInsertions;
 
+            public override JsonSerializerOptions DefaultOptions => JsonSerializerOptions.Default;
             public override bool IsAsyncSerializer => true;
             public override bool ForceSmallBufferInOptions => _forceSmallBufferInOptions;
 
@@ -215,6 +218,7 @@ namespace System.Text.Json.Serialization.Tests
             private readonly bool _forceSmallBufferInOptions;
             private readonly bool _forceBomInsertions;
 
+            public override JsonSerializerOptions DefaultOptions => JsonSerializerOptions.Default;
             public override bool IsAsyncSerializer => false;
             public override bool ForceSmallBufferInOptions => _forceSmallBufferInOptions;
 
@@ -293,6 +297,7 @@ namespace System.Text.Json.Serialization.Tests
 
         private class ReaderWriterSerializerWrapper : JsonSerializerWrapper
         {
+            public override JsonSerializerOptions DefaultOptions => JsonSerializerOptions.Default;
             public override Task<string> SerializeWrapper(object value, Type inputType, JsonSerializerOptions options = null)
             {
                 using MemoryStream stream = new MemoryStream();
@@ -381,6 +386,7 @@ namespace System.Text.Json.Serialization.Tests
 
         private class DocumentSerializerWrapper : JsonSerializerWrapper
         {
+            public override JsonSerializerOptions DefaultOptions => JsonSerializerOptions.Default;
             public override Task<string> SerializeWrapper(object value, Type inputType, JsonSerializerOptions options = null)
             {
                 JsonDocument document = JsonSerializer.SerializeToDocument(value, inputType, options);
@@ -491,6 +497,7 @@ namespace System.Text.Json.Serialization.Tests
 
         private class ElementSerializerWrapper : JsonSerializerWrapper
         {
+            public override JsonSerializerOptions DefaultOptions => JsonSerializerOptions.Default;
             public override Task<string> SerializeWrapper(object value, Type inputType, JsonSerializerOptions options = null)
             {
                 JsonElement element = JsonSerializer.SerializeToElement(value, inputType, options);
@@ -564,6 +571,7 @@ namespace System.Text.Json.Serialization.Tests
 
         private class NodeSerializerWrapper : JsonSerializerWrapper
         {
+            public override JsonSerializerOptions DefaultOptions => JsonSerializerOptions.Default;
             public override bool SupportsNullValueOnDeserialize => true;
 
             public override Task<string> SerializeWrapper(object value, Type inputType, JsonSerializerOptions options = null)
index 82c776c..41dd4f6 100644 (file)
@@ -1,13 +1,9 @@
 // Licensed to the .NET Foundation under one or more agreements.
 // The .NET Foundation licenses this file to you under the MIT license.
 
-using System;
 using System.Collections.Generic;
 using System.Diagnostics.CodeAnalysis;
-using System.Linq;
 using System.Text.Json.Serialization.Metadata;
-using Microsoft.Diagnostics.Runtime.ICorDebug;
-using Newtonsoft.Json;
 using Xunit;
 
 namespace System.Text.Json.Serialization.Tests
@@ -56,614 +52,4 @@ namespace System.Text.Json.Serialization.Tests
     {
         public RequiredKeywordTests_Node() : base(JsonSerializerWrapper.NodeSerializer) { }
     }
-
-    public abstract partial class RequiredKeywordTests : SerializerTests
-    {
-        public RequiredKeywordTests(JsonSerializerWrapper serializer) : base(serializer)
-        {
-        }
-
-        [Theory]
-        [InlineData(true)]
-        [InlineData(false)]
-        public async void ClassWithRequiredKeywordDeserialization(bool ignoreNullValues)
-        {
-            JsonSerializerOptions options = new()
-            {
-                IgnoreNullValues = ignoreNullValues
-            };
-
-            AssertJsonTypeInfoHasRequiredProperties(GetTypeInfo<PersonWithRequiredMembers>(options),
-                nameof(PersonWithRequiredMembers.FirstName),
-                nameof(PersonWithRequiredMembers.LastName));
-
-            var obj = new PersonWithRequiredMembers()
-            {
-                FirstName = "foo",
-                LastName = "bar"
-            };
-
-            string json = await Serializer.SerializeWrapper(obj, options);
-            Assert.Equal("""{"FirstName":"foo","MiddleName":"","LastName":"bar"}""", json);
-
-            PersonWithRequiredMembers deserialized = await Serializer.DeserializeWrapper<PersonWithRequiredMembers>(json, options);
-            Assert.Equal(obj.FirstName, deserialized.FirstName);
-            Assert.Equal(obj.MiddleName, deserialized.MiddleName);
-            Assert.Equal(obj.LastName, deserialized.LastName);
-
-            json = """{"LastName":"bar"}""";
-            JsonException exception = await Assert.ThrowsAsync<JsonException>(async () => await Serializer.DeserializeWrapper<PersonWithRequiredMembers>(json, options));
-            Assert.Contains("FirstName", exception.Message);
-            Assert.DoesNotContain("LastName", exception.Message);
-            Assert.DoesNotContain("MiddleName", exception.Message);
-
-            json = """{"LastName":null}""";
-            exception = await Assert.ThrowsAsync<JsonException>(async () => await Serializer.DeserializeWrapper<PersonWithRequiredMembers>(json, options));
-            Assert.Contains("FirstName", exception.Message);
-            Assert.DoesNotContain("LastName", exception.Message);
-            Assert.DoesNotContain("MiddleName", exception.Message);
-
-            json = "{}";
-            exception = await Assert.ThrowsAsync<JsonException>(async () => await Serializer.DeserializeWrapper<PersonWithRequiredMembers>(json, options));
-            Assert.Contains("FirstName", exception.Message);
-            Assert.Contains("LastName", exception.Message);
-            Assert.DoesNotContain("MiddleName", exception.Message);
-        }
-
-        [Fact]
-        public async void RequiredPropertyOccuringTwiceInThePayloadWorksAsExpected()
-        {
-            string json = """{"FirstName":"foo","MiddleName":"","LastName":"bar","FirstName":"newfoo"}""";
-            PersonWithRequiredMembers deserialized = await Serializer.DeserializeWrapper<PersonWithRequiredMembers>(json);
-            Assert.Equal("newfoo", deserialized.FirstName);
-            Assert.Equal("", deserialized.MiddleName);
-            Assert.Equal("bar", deserialized.LastName);
-        }
-
-        private class PersonWithRequiredMembers
-        {
-            public required string FirstName { get; set; }
-            public string MiddleName { get; set; } = "";
-            public required string LastName { get; set; }
-        }
-
-        [Theory]
-        [InlineData(true)]
-        [InlineData(false)]
-        public async void ClassWithRequiredKeywordAndSmallParametrizedCtorFailsDeserialization(bool ignoreNullValues)
-        {
-            JsonSerializerOptions options = new()
-            {
-                IgnoreNullValues = ignoreNullValues
-            };
-
-            AssertJsonTypeInfoHasRequiredProperties(GetTypeInfo<PersonWithRequiredMembersAndSmallParametrizedCtor>(options),
-                nameof(PersonWithRequiredMembersAndSmallParametrizedCtor.FirstName),
-                nameof(PersonWithRequiredMembersAndSmallParametrizedCtor.LastName),
-                nameof(PersonWithRequiredMembersAndSmallParametrizedCtor.Info1),
-                nameof(PersonWithRequiredMembersAndSmallParametrizedCtor.Info2));
-
-            var obj = new PersonWithRequiredMembersAndSmallParametrizedCtor("badfoo", "badbar")
-            {
-                // note: these must be set during initialize or otherwise we get compiler errors
-                FirstName = "foo",
-                LastName = "bar",
-                Info1 = "info1",
-                Info2 = "info2",
-            };
-
-            string json = await Serializer.SerializeWrapper(obj, options);
-            Assert.Equal("""{"FirstName":"foo","MiddleName":"","LastName":"bar","Info1":"info1","Info2":"info2"}""", json);
-
-            var deserialized = await Serializer.DeserializeWrapper<PersonWithRequiredMembersAndSmallParametrizedCtor>(json, options);
-            Assert.Equal(obj.FirstName, deserialized.FirstName);
-            Assert.Equal(obj.MiddleName, deserialized.MiddleName);
-            Assert.Equal(obj.LastName, deserialized.LastName);
-            Assert.Equal(obj.Info1, deserialized.Info1);
-            Assert.Equal(obj.Info2, deserialized.Info2);
-
-            json = """{"FirstName":"foo","MiddleName":"","LastName":null,"Info1":null,"Info2":"info2"}""";
-            deserialized = await Serializer.DeserializeWrapper<PersonWithRequiredMembersAndSmallParametrizedCtor>(json, options);
-            Assert.Equal(obj.FirstName, deserialized.FirstName);
-            Assert.Equal(obj.MiddleName, deserialized.MiddleName);
-            Assert.Null(deserialized.LastName);
-            Assert.Null(deserialized.Info1);
-            Assert.Equal(obj.Info2, deserialized.Info2);
-
-            json = """{"LastName":"bar","Info1":"info1"}""";
-            JsonException exception = await Assert.ThrowsAsync<JsonException>(async () => await Serializer.DeserializeWrapper<PersonWithRequiredMembersAndSmallParametrizedCtor>(json, options));
-            Assert.Contains("FirstName", exception.Message);
-            Assert.DoesNotContain("LastName", exception.Message);
-            Assert.DoesNotContain("MiddleName", exception.Message);
-            Assert.DoesNotContain("Info1", exception.Message);
-            Assert.Contains("Info2", exception.Message);
-
-            json = """{"LastName":null,"Info1":null}""";
-            exception = await Assert.ThrowsAsync<JsonException>(async () => await Serializer.DeserializeWrapper<PersonWithRequiredMembersAndSmallParametrizedCtor>(json, options));
-            Assert.Contains("FirstName", exception.Message);
-            Assert.DoesNotContain("LastName", exception.Message);
-            Assert.DoesNotContain("MiddleName", exception.Message);
-            Assert.DoesNotContain("Info1", exception.Message);
-            Assert.Contains("Info2", exception.Message);
-
-            json = "{}";
-            exception = await Assert.ThrowsAsync<JsonException>(async () => await Serializer.DeserializeWrapper<PersonWithRequiredMembersAndSmallParametrizedCtor>(json, options));
-            Assert.Contains("FirstName", exception.Message);
-            Assert.Contains("LastName", exception.Message);
-            Assert.DoesNotContain("MiddleName", exception.Message);
-            Assert.Contains("Info1", exception.Message);
-            Assert.Contains("Info2", exception.Message);
-        }
-
-        private class PersonWithRequiredMembersAndSmallParametrizedCtor
-        {
-            public required string FirstName { get; set; }
-            public string MiddleName { get; set; } = "";
-            public required string LastName { get; set; }
-            public required string Info1 { get; set; }
-            public required string Info2 { get; set; }
-
-            public PersonWithRequiredMembersAndSmallParametrizedCtor(string firstName, string lastName)
-            {
-                FirstName = firstName;
-                LastName = lastName;
-            }
-        }
-
-        [Theory]
-        [InlineData(true)]
-        [InlineData(false)]
-        public async void ClassWithRequiredKeywordAndLargeParametrizedCtorFailsDeserialization(bool ignoreNullValues)
-        {
-            JsonSerializerOptions options = new()
-            {
-                IgnoreNullValues = ignoreNullValues
-            };
-
-            AssertJsonTypeInfoHasRequiredProperties(GetTypeInfo<PersonWithRequiredMembersAndLargeParametrizedCtor>(options),
-                nameof(PersonWithRequiredMembersAndLargeParametrizedCtor.AProp),
-                nameof(PersonWithRequiredMembersAndLargeParametrizedCtor.BProp),
-                nameof(PersonWithRequiredMembersAndLargeParametrizedCtor.CProp),
-                nameof(PersonWithRequiredMembersAndLargeParametrizedCtor.DProp),
-                nameof(PersonWithRequiredMembersAndLargeParametrizedCtor.EProp),
-                nameof(PersonWithRequiredMembersAndLargeParametrizedCtor.FProp),
-                nameof(PersonWithRequiredMembersAndLargeParametrizedCtor.GProp),
-                nameof(PersonWithRequiredMembersAndLargeParametrizedCtor.HProp),
-                nameof(PersonWithRequiredMembersAndLargeParametrizedCtor.IProp));
-
-            var obj = new PersonWithRequiredMembersAndLargeParametrizedCtor("bada", "badb", "badc", "badd", "bade", "badf", "badg")
-            {
-                // note: these must be set during initialize or otherwise we get compiler errors
-                AProp = "a",
-                BProp = "b",
-                CProp = "c",
-                DProp = "d",
-                EProp = "e",
-                FProp = "f",
-                GProp = "g",
-                HProp = "h",
-                IProp = "i",
-            };
-
-            string json = await Serializer.SerializeWrapper(obj, options);
-            Assert.Equal("""{"AProp":"a","BProp":"b","CProp":"c","DProp":"d","EProp":"e","FProp":"f","GProp":"g","HProp":"h","IProp":"i"}""", json);
-
-            var deserialized = await Serializer.DeserializeWrapper<PersonWithRequiredMembersAndLargeParametrizedCtor>(json, options);
-            Assert.Equal(obj.AProp, deserialized.AProp);
-            Assert.Equal(obj.BProp, deserialized.BProp);
-            Assert.Equal(obj.CProp, deserialized.CProp);
-            Assert.Equal(obj.DProp, deserialized.DProp);
-            Assert.Equal(obj.EProp, deserialized.EProp);
-            Assert.Equal(obj.FProp, deserialized.FProp);
-            Assert.Equal(obj.GProp, deserialized.GProp);
-            Assert.Equal(obj.HProp, deserialized.HProp);
-            Assert.Equal(obj.IProp, deserialized.IProp);
-
-            json = """{"AProp":"a","BProp":"b","CProp":"c","DProp":"d","EProp":null,"FProp":"f","GProp":"g","HProp":null,"IProp":"i"}""";
-            deserialized = await Serializer.DeserializeWrapper<PersonWithRequiredMembersAndLargeParametrizedCtor>(json, options);
-            Assert.Equal(obj.AProp, deserialized.AProp);
-            Assert.Equal(obj.BProp, deserialized.BProp);
-            Assert.Equal(obj.CProp, deserialized.CProp);
-            Assert.Equal(obj.DProp, deserialized.DProp);
-            Assert.Null(deserialized.EProp);
-            Assert.Equal(obj.FProp, deserialized.FProp);
-            Assert.Equal(obj.GProp, deserialized.GProp);
-            Assert.Null(deserialized.HProp);
-            Assert.Equal(obj.IProp, deserialized.IProp);
-
-            json = """{"AProp":"a","IProp":"i"}""";
-            JsonException exception = await Assert.ThrowsAsync<JsonException>(async () => await Serializer.DeserializeWrapper<PersonWithRequiredMembersAndLargeParametrizedCtor>(json, options));
-            Assert.DoesNotContain("AProp", exception.Message);
-            Assert.Contains("BProp", exception.Message);
-            Assert.Contains("CProp", exception.Message);
-            Assert.Contains("DProp", exception.Message);
-            Assert.Contains("EProp", exception.Message);
-            Assert.Contains("FProp", exception.Message);
-            Assert.Contains("GProp", exception.Message);
-            Assert.Contains("HProp", exception.Message);
-            Assert.DoesNotContain("IProp", exception.Message);
-
-            json = """{"AProp":null,"IProp":null}""";
-            exception = await Assert.ThrowsAsync<JsonException>(async () => await Serializer.DeserializeWrapper<PersonWithRequiredMembersAndLargeParametrizedCtor>(json, options));
-            Assert.DoesNotContain("AProp", exception.Message);
-            Assert.Contains("BProp", exception.Message);
-            Assert.Contains("CProp", exception.Message);
-            Assert.Contains("DProp", exception.Message);
-            Assert.Contains("EProp", exception.Message);
-            Assert.Contains("FProp", exception.Message);
-            Assert.Contains("GProp", exception.Message);
-            Assert.Contains("HProp", exception.Message);
-            Assert.DoesNotContain("IProp", exception.Message);
-
-            json = """{"BProp":"b","CProp":"c","DProp":"d","EProp":"e","FProp":"f","HProp":"h"}""";
-            exception = await Assert.ThrowsAsync<JsonException>(async () => await Serializer.DeserializeWrapper<PersonWithRequiredMembersAndLargeParametrizedCtor>(json, options));
-            Assert.Contains("AProp", exception.Message);
-            Assert.DoesNotContain("BProp", exception.Message);
-            Assert.DoesNotContain("CProp", exception.Message);
-            Assert.DoesNotContain("DProp", exception.Message);
-            Assert.DoesNotContain("EProp", exception.Message);
-            Assert.DoesNotContain("FProp", exception.Message);
-            Assert.Contains("GProp", exception.Message);
-            Assert.DoesNotContain("HProp", exception.Message);
-            Assert.Contains("IProp", exception.Message);
-        }
-
-        private class PersonWithRequiredMembersAndLargeParametrizedCtor
-        {
-            // Using suffix for names so that checking if required property is missing can be done with simple string.Contains without false positives
-            public required string AProp { get; set; }
-            public required string BProp { get; set; }
-            public required string CProp { get; set; }
-            public required string DProp { get; set; }
-            public required string EProp { get; set; }
-            public required string FProp { get; set; }
-            public required string GProp { get; set; }
-            public required string HProp { get; set; }
-            public required string IProp { get; set; }
-
-            public PersonWithRequiredMembersAndLargeParametrizedCtor(string aprop, string bprop, string cprop, string dprop, string eprop, string fprop, string gprop)
-            {
-                AProp = aprop;
-                BProp = bprop;
-                CProp = cprop;
-                DProp = dprop;
-                EProp = eprop;
-                FProp = fprop;
-                GProp = gprop;
-            }
-        }
-
-        [Theory]
-        [InlineData(false)]
-        [InlineData(true)]
-        public async void ClassWithRequiredKeywordAndSetsRequiredMembersOnCtorWorks(bool useContext)
-        {
-            JsonSerializerOptions options = useContext ? SetsRequiredMembersTestsContext.Default.Options : JsonSerializerOptions.Default;
-            AssertJsonTypeInfoHasRequiredProperties(GetTypeInfo<PersonWithRequiredMembersAndSetsRequiredMembers>(options)
-                /* no required members */);
-
-            var obj = new PersonWithRequiredMembersAndSetsRequiredMembers()
-            {
-                FirstName = "foo",
-                LastName = "bar"
-            };
-
-            string json = await Serializer.SerializeWrapper(obj, options);
-            Assert.Equal("""{"FirstName":"foo","MiddleName":"","LastName":"bar"}""", json);
-
-            json = """{"LastName":"bar"}""";
-            var deserialized = await Serializer.DeserializeWrapper<PersonWithRequiredMembersAndSetsRequiredMembers>(json, options);
-            Assert.Equal("", deserialized.FirstName);
-            Assert.Equal("", deserialized.MiddleName);
-            Assert.Equal("bar", deserialized.LastName);
-        }
-
-        private class PersonWithRequiredMembersAndSetsRequiredMembers
-        {
-            public required string FirstName { get; set; }
-            public string MiddleName { get; set; } = "";
-            public required string LastName { get; set; }
-
-            [SetsRequiredMembers]
-            public PersonWithRequiredMembersAndSetsRequiredMembers()
-            {
-                FirstName = "";
-                LastName = "";
-            }
-        }
-
-        [Theory]
-        [InlineData(false)]
-        [InlineData(true)]
-        public async void ClassWithRequiredKeywordSmallParametrizedCtorAndSetsRequiredMembersOnCtorWorks(bool useContext)
-        {
-            JsonSerializerOptions options = useContext ? SetsRequiredMembersTestsContext.Default.Options : JsonSerializerOptions.Default;
-            AssertJsonTypeInfoHasRequiredProperties(GetTypeInfo<PersonWithRequiredMembersAndSmallParametrizedCtorAndSetsRequiredMembers>(options)
-                /* no required members */);
-
-            var obj = new PersonWithRequiredMembersAndSmallParametrizedCtorAndSetsRequiredMembers("foo", "bar");
-
-            string json = await Serializer.SerializeWrapper(obj, options);
-            Assert.Equal("""{"FirstName":"foo","MiddleName":"","LastName":"bar"}""", json);
-
-            var deserialized = await Serializer.DeserializeWrapper<PersonWithRequiredMembersAndSmallParametrizedCtorAndSetsRequiredMembers>(json, options);
-            Assert.Equal("foo", deserialized.FirstName);
-            Assert.Equal("", deserialized.MiddleName);
-            Assert.Equal("bar", deserialized.LastName);
-        }
-
-        private class PersonWithRequiredMembersAndSmallParametrizedCtorAndSetsRequiredMembers
-        {
-            public required string FirstName { get; set; }
-            public string MiddleName { get; set; } = "";
-            public required string LastName { get; set; }
-
-            [SetsRequiredMembers]
-            public PersonWithRequiredMembersAndSmallParametrizedCtorAndSetsRequiredMembers(string firstName, string lastName)
-            {
-                FirstName = firstName;
-                LastName = lastName;
-            }
-        }
-
-        [Theory]
-        [InlineData(false)]
-        [InlineData(true)]
-        public async void ClassWithRequiredKeywordLargeParametrizedCtorAndSetsRequiredMembersOnCtorWorks(bool useContext)
-        {
-            JsonSerializerOptions options = useContext ? SetsRequiredMembersTestsContext.Default.Options : JsonSerializerOptions.Default;
-            AssertJsonTypeInfoHasRequiredProperties(GetTypeInfo<PersonWithRequiredMembersAndLargeParametrizedCtorAndSetsRequiredMembers>(options)
-                /* no required members */);
-
-            var obj = new PersonWithRequiredMembersAndLargeParametrizedCtorAndSetsRequiredMembers("a", "b", "c", "d", "e", "f", "g");
-
-            string json = await Serializer.SerializeWrapper(obj, options);
-            Assert.Equal("""{"A":"a","B":"b","C":"c","D":"d","E":"e","F":"f","G":"g"}""", json);
-
-            var deserialized = await Serializer.DeserializeWrapper<PersonWithRequiredMembersAndLargeParametrizedCtorAndSetsRequiredMembers>(json, options);
-            Assert.Equal("a", deserialized.A);
-            Assert.Equal("b", deserialized.B);
-            Assert.Equal("c", deserialized.C);
-            Assert.Equal("d", deserialized.D);
-            Assert.Equal("e", deserialized.E);
-            Assert.Equal("f", deserialized.F);
-            Assert.Equal("g", deserialized.G);
-        }
-
-        private class PersonWithRequiredMembersAndLargeParametrizedCtorAndSetsRequiredMembers
-        {
-            public required string A { get; set; }
-            public required string B { get; set; }
-            public required string C { get; set; }
-            public required string D { get; set; }
-            public required string E { get; set; }
-            public required string F { get; set; }
-            public required string G { get; set; }
-
-            [SetsRequiredMembers]
-            public PersonWithRequiredMembersAndLargeParametrizedCtorAndSetsRequiredMembers(string a, string b, string c, string d, string e, string f, string g)
-            {
-                A = a;
-                B = b;
-                C = c;
-                D = d;
-                E = e;
-                F = f;
-                G = g;
-            }
-        }
-
-        [JsonSerializable(typeof(PersonWithRequiredMembersAndSetsRequiredMembers))]
-        [JsonSerializable(typeof(PersonWithRequiredMembersAndSmallParametrizedCtorAndSetsRequiredMembers))]
-        [JsonSerializable(typeof(PersonWithRequiredMembersAndLargeParametrizedCtorAndSetsRequiredMembers))]
-        private partial class SetsRequiredMembersTestsContext : JsonSerializerContext { }
-
-        [Fact]
-        public async void RemovingPropertiesWithRequiredKeywordAllowsDeserialization()
-        {
-            JsonSerializerOptions options = new()
-            {
-                TypeInfoResolver = new DefaultJsonTypeInfoResolver()
-                {
-                    Modifiers =
-                    {
-                        (ti) =>
-                        {
-                            for (int i = 0; i < ti.Properties.Count; i++)
-                            {
-                                if (ti.Properties[i].Name == nameof(PersonWithRequiredMembers.FirstName))
-                                {
-                                    Assert.True(ti.Properties[i].IsRequired);
-                                    JsonPropertyInfo property = ti.CreateJsonPropertyInfo(typeof(string), nameof(PersonWithRequiredMembers.FirstName));
-                                    property.Get = (obj) => ((PersonWithRequiredMembers)obj).FirstName;
-                                    property.Set = (obj, val) => ((PersonWithRequiredMembers)obj).FirstName = (string)val;
-                                    ti.Properties[i] = property;
-                                }
-                                else if (ti.Properties[i].Name == nameof(PersonWithRequiredMembers.LastName))
-                                {
-                                    Assert.True(ti.Properties[i].IsRequired);
-                                    JsonPropertyInfo property = ti.CreateJsonPropertyInfo(typeof(string), nameof(PersonWithRequiredMembers.LastName));
-                                    property.Get = (obj) => ((PersonWithRequiredMembers)obj).LastName;
-                                    property.Set = (obj, val) => ((PersonWithRequiredMembers)obj).LastName = (string)val;
-                                    ti.Properties[i] = property;
-                                }
-                                else
-                                {
-                                    Assert.False(ti.Properties[i].IsRequired);
-                                }
-                            }
-                        }
-                    }
-                }
-            };
-
-            var obj = new PersonWithRequiredMembers()
-            {
-                FirstName = "foo",
-                LastName = "bar"
-            };
-
-            string json = await Serializer.SerializeWrapper(obj, options);
-            Assert.Equal("""{"FirstName":"foo","MiddleName":"","LastName":"bar"}""", json);
-
-            json = """{"LastName":"bar"}""";
-            PersonWithRequiredMembers deserialized = await Serializer.DeserializeWrapper<PersonWithRequiredMembers>(json, options);
-            Assert.Null(deserialized.FirstName);
-            Assert.Equal("", deserialized.MiddleName);
-            Assert.Equal("bar", deserialized.LastName);
-        }
-
-        [Fact]
-        public async void ChangingPropertiesWithRequiredKeywordToNotBeRequiredAllowsDeserialization()
-        {
-            JsonSerializerOptions options = new()
-            {
-                TypeInfoResolver = new DefaultJsonTypeInfoResolver()
-                {
-                    Modifiers =
-                    {
-                        (ti) =>
-                        {
-                            for (int i = 0; i < ti.Properties.Count; i++)
-                            {
-                                ti.Properties[i].IsRequired = false;
-                            }
-                        }
-                    }
-                }
-            };
-
-            var obj = new PersonWithRequiredMembers()
-            {
-                FirstName = "foo",
-                LastName = "bar"
-            };
-
-            string json = await Serializer.SerializeWrapper(obj, options);
-            Assert.Equal("""{"FirstName":"foo","MiddleName":"","LastName":"bar"}""", json);
-
-            json = """{"LastName":"bar"}""";
-            PersonWithRequiredMembers deserialized = await Serializer.DeserializeWrapper<PersonWithRequiredMembers>(json, options);
-            Assert.Null(deserialized.FirstName);
-            Assert.Equal("", deserialized.MiddleName);
-            Assert.Equal("bar", deserialized.LastName);
-        }
-
-        [Fact]
-        public async void RequiredNonDeserializablePropertyThrows()
-        {
-            JsonSerializerOptions options = new()
-            {
-                TypeInfoResolver = new DefaultJsonTypeInfoResolver()
-                {
-                    Modifiers =
-                    {
-                        (ti) =>
-                        {
-                            for (int i = 0; i < ti.Properties.Count; i++)
-                            {
-                                if (ti.Properties[i].Name == nameof(PersonWithRequiredMembers.FirstName))
-                                {
-                                    ti.Properties[i].Set = null;
-                                }
-                            }
-                        }
-                    }
-                }
-            };
-
-            string json = """{"FirstName":"foo","MiddleName":"","LastName":"bar"}""";
-            InvalidOperationException exception = await Assert.ThrowsAsync<InvalidOperationException>(async () => await Serializer.DeserializeWrapper<PersonWithRequiredMembers>(json, options));
-            Assert.Contains(nameof(PersonWithRequiredMembers.FirstName), exception.Message);
-        }
-
-        [Fact]
-        public async void RequiredInitOnlyPropertyDoesNotThrow()
-        {
-            string json = """{"Prop":"foo"}""";
-            ClassWithInitOnlyRequiredProperty deserialized = await Serializer.DeserializeWrapper<ClassWithInitOnlyRequiredProperty>(json);
-            Assert.Equal("foo", deserialized.Prop);
-        }
-
-        private class ClassWithInitOnlyRequiredProperty
-        {
-            public required string Prop { get; init; }
-        }
-
-        [Fact]
-        public async void RequiredExtensionDataPropertyThrows()
-        {
-            string json = """{"Foo":"foo","Bar":"bar"}""";
-            InvalidOperationException exception = await Assert.ThrowsAsync<InvalidOperationException>(
-                async () => await Serializer.DeserializeWrapper<ClassWithRequiredExtensionDataProperty>(json));
-            Assert.Contains(nameof(ClassWithRequiredExtensionDataProperty.TestExtensionData), exception.Message);
-        }
-
-        private class ClassWithRequiredExtensionDataProperty
-        {
-            [JsonExtensionData]
-            public required Dictionary<string, JsonElement>? TestExtensionData { get; set; }
-        }
-
-        [Fact]
-        public async void RequiredKeywordAndJsonRequiredCustomAttributeWorkCorrectlyTogether()
-        {
-            JsonSerializerOptions options = JsonSerializerOptions.Default;
-            JsonTypeInfo typeInfo = GetTypeInfo<ClassWithRequiredKeywordAndJsonRequiredCustomAttribute>(options);
-            AssertJsonTypeInfoHasRequiredProperties(typeInfo,
-                nameof(ClassWithRequiredKeywordAndJsonRequiredCustomAttribute.SomeProperty));
-
-            ClassWithRequiredKeywordAndJsonRequiredCustomAttribute obj = new()
-            {
-                SomeProperty = "foo"
-            };
-
-            string json = await Serializer.SerializeWrapper(obj, options);
-            Assert.Equal("""{"SomeProperty":"foo"}""", json);
-
-            var deserialized = await Serializer.DeserializeWrapper<ClassWithRequiredKeywordAndJsonRequiredCustomAttribute>(json, options);
-            Assert.Equal(obj.SomeProperty, deserialized.SomeProperty);
-
-            json = "{}";
-            JsonException exception = await Assert.ThrowsAsync<JsonException>(
-                async () => await Serializer.DeserializeWrapper<ClassWithRequiredKeywordAndJsonRequiredCustomAttribute>(json, options));
-
-            Assert.Contains(nameof(ClassWithRequiredKeywordAndJsonRequiredCustomAttribute.SomeProperty), exception.Message);
-        }
-
-        private class ClassWithRequiredKeywordAndJsonRequiredCustomAttribute
-        {
-            [JsonRequired]
-            public required string SomeProperty { get; set; }
-        }
-
-        private static JsonTypeInfo GetTypeInfo<T>(JsonSerializerOptions options)
-        {
-            // For some variations of test (i.e. AsyncStreamWithSmallBuffer)
-            // we don't use options directly and use copy with changed settings.
-            // Because of that options.GetTypeInfo might throw even when Serializer.(De)SerializeWrapper was called..
-            // We call into Serialize here to ensure that options are locked and options.GetTypeInfo queried.
-            JsonSerializer.Serialize<T>(default(T), options);
-            return options.GetTypeInfo(typeof(T));
-        }
-
-        private static void AssertJsonTypeInfoHasRequiredProperties(JsonTypeInfo typeInfo, params string[] requiredProperties)
-        {
-            HashSet<string> requiredPropertiesSet = new(requiredProperties);
-
-            foreach (var property in typeInfo.Properties)
-            {
-                if (requiredPropertiesSet.Remove(property.Name))
-                {
-                    Assert.True(property.IsRequired);
-                }
-                else
-                {
-                    Assert.False(property.IsRequired);
-                }
-            }
-
-            Assert.Empty(requiredPropertiesSet);
-        }
-    }
 }
index d24b8ea..51fbcbf 100644 (file)
@@ -58,6 +58,7 @@
     <Compile Include="..\Common\ReferenceHandlerTests\ReferenceHandlerTests.Deserialize.cs" Link="CommonTest\System\Text\Json\Tests\Serialization\ReferenceHandlerTests\ReferenceHandlerTests.Deserialize.cs" />
     <Compile Include="..\Common\ReferenceHandlerTests\ReferenceHandlerTests.IgnoreCycles.cs" Link="CommonTest\System\Text\Json\Tests\Serialization\ReferenceHandlerTests\ReferenceHandlerTests.IgnoreCycles.cs" />
     <Compile Include="..\Common\ReferenceHandlerTests\ReferenceHandlerTests.Serialize.cs" Link="CommonTest\System\Text\Json\Tests\Serialization\ReferenceHandlerTests\ReferenceHandlerTests.Serialize.cs" />
+    <Compile Include="..\Common\RequiredKeywordTests.cs" Link="CommonTest\System\Text\Json\Tests\Serialization\RequiredKeywordTests.cs" />
     <Compile Include="..\Common\SampleTestData.OrderPayload.cs" Link="CommonTest\System\Text\Json\Tests\Serialization\SampleTestData.OrderPayload.cs" />
     <Compile Include="..\Common\SerializerTests.cs" Link="CommonTest\System\Text\Json\Tests\Serialization\SerializerTests.cs" />
     <Compile Include="..\Common\Utf8MemoryStream.cs" Link="CommonTest\System\Text\Json\Tests\Serialization\Utf8MemoryStream.cs" />