diff --git a/src/libraries/System.Text.Json/Common/JsonSourceGenerationOptionsAttribute.cs b/src/libraries/System.Text.Json/Common/JsonSourceGenerationOptionsAttribute.cs index 8fd1e3353dc4f..12035eb34c14c 100644 --- a/src/libraries/System.Text.Json/Common/JsonSourceGenerationOptionsAttribute.cs +++ b/src/libraries/System.Text.Json/Common/JsonSourceGenerationOptionsAttribute.cs @@ -120,6 +120,11 @@ public JsonSourceGenerationOptionsAttribute(JsonSerializerDefaults defaults) /// public bool RespectNullableAnnotations { get; set; } + /// + /// Specifies the default value of when set. + /// + public bool RespectRequiredConstructorParameters { get; set; } + /// /// Specifies the default value of when set. /// diff --git a/src/libraries/System.Text.Json/gen/JsonSourceGenerator.Emitter.cs b/src/libraries/System.Text.Json/gen/JsonSourceGenerator.Emitter.cs index 62e9fd57d8299..1ed487e6bbd73 100644 --- a/src/libraries/System.Text.Json/gen/JsonSourceGenerator.Emitter.cs +++ b/src/libraries/System.Text.Json/gen/JsonSourceGenerator.Emitter.cs @@ -1207,6 +1207,9 @@ private static void GetLogicForDefaultSerializerOptionsInit(SourceGenerationOpti if (optionsSpec.RespectNullableAnnotations is bool respectNullableAnnotations) writer.WriteLine($"RespectNullableAnnotations = {FormatBoolLiteral(respectNullableAnnotations)},"); + if (optionsSpec.RespectRequiredConstructorParameters is bool respectRequiredConstructorParameters) + writer.WriteLine($"RespectRequiredConstructorParameters = {FormatBoolLiteral(respectRequiredConstructorParameters)},"); + if (optionsSpec.IgnoreReadOnlyFields is bool ignoreReadOnlyFields) writer.WriteLine($"IgnoreReadOnlyFields = {FormatBoolLiteral(ignoreReadOnlyFields)},"); diff --git a/src/libraries/System.Text.Json/gen/JsonSourceGenerator.Parser.cs b/src/libraries/System.Text.Json/gen/JsonSourceGenerator.Parser.cs index 66d40939ca423..b32a3f95e929d 100644 --- a/src/libraries/System.Text.Json/gen/JsonSourceGenerator.Parser.cs +++ b/src/libraries/System.Text.Json/gen/JsonSourceGenerator.Parser.cs @@ -270,6 +270,7 @@ private SourceGenerationOptionsSpec ParseJsonSourceGenerationOptionsAttribute(IN JsonKnownNamingPolicy? dictionaryKeyPolicy = null; bool? respectNullableAnnotations = null; bool? ignoreReadOnlyFields = null; + bool? respectRequiredConstructorParameters = null; bool? ignoreReadOnlyProperties = null; bool? includeFields = null; int? maxDepth = null; @@ -334,6 +335,10 @@ private SourceGenerationOptionsSpec ParseJsonSourceGenerationOptionsAttribute(IN respectNullableAnnotations = (bool)namedArg.Value.Value!; break; + case nameof(JsonSourceGenerationOptionsAttribute.RespectRequiredConstructorParameters): + respectRequiredConstructorParameters = (bool)namedArg.Value.Value!; + break; + case nameof(JsonSourceGenerationOptionsAttribute.IgnoreReadOnlyFields): ignoreReadOnlyFields = (bool)namedArg.Value.Value!; break; @@ -418,6 +423,7 @@ private SourceGenerationOptionsSpec ParseJsonSourceGenerationOptionsAttribute(IN DefaultIgnoreCondition = defaultIgnoreCondition, DictionaryKeyPolicy = dictionaryKeyPolicy, RespectNullableAnnotations = respectNullableAnnotations, + RespectRequiredConstructorParameters = respectRequiredConstructorParameters, IgnoreReadOnlyFields = ignoreReadOnlyFields, IgnoreReadOnlyProperties = ignoreReadOnlyProperties, IncludeFields = includeFields, diff --git a/src/libraries/System.Text.Json/gen/Model/SourceGenerationOptionsSpec.cs b/src/libraries/System.Text.Json/gen/Model/SourceGenerationOptionsSpec.cs index 9b7ecc8ea6920..5c4d250af188f 100644 --- a/src/libraries/System.Text.Json/gen/Model/SourceGenerationOptionsSpec.cs +++ b/src/libraries/System.Text.Json/gen/Model/SourceGenerationOptionsSpec.cs @@ -30,6 +30,8 @@ public sealed record SourceGenerationOptionsSpec public required bool? RespectNullableAnnotations { get; init; } + public required bool? RespectRequiredConstructorParameters { get; init; } + public required bool? IgnoreReadOnlyFields { get; init; } public required bool? IgnoreReadOnlyProperties { get; init; } diff --git a/src/libraries/System.Text.Json/ref/System.Text.Json.cs b/src/libraries/System.Text.Json/ref/System.Text.Json.cs index 6ce961115720b..2dbe6cad3e5eb 100644 --- a/src/libraries/System.Text.Json/ref/System.Text.Json.cs +++ b/src/libraries/System.Text.Json/ref/System.Text.Json.cs @@ -402,6 +402,7 @@ public JsonSerializerOptions(System.Text.Json.JsonSerializerOptions options) { } public System.Text.Json.JsonCommentHandling ReadCommentHandling { get { throw null; } set { } } public System.Text.Json.Serialization.ReferenceHandler? ReferenceHandler { get { throw null; } set { } } public bool RespectNullableAnnotations { get { throw null; } set { } } + public bool RespectRequiredConstructorParameters { get { throw null; } set { } } public System.Text.Json.Serialization.Metadata.IJsonTypeInfoResolver? TypeInfoResolver { get { throw null; } set { } } public System.Collections.Generic.IList TypeInfoResolverChain { get { throw null; } } public System.Text.Json.Serialization.JsonUnknownTypeHandling UnknownTypeHandling { get { throw null; } set { } } @@ -1093,6 +1094,7 @@ public JsonSourceGenerationOptionsAttribute(System.Text.Json.JsonSerializerDefau public System.Text.Json.Serialization.JsonKnownNamingPolicy PropertyNamingPolicy { get { throw null; } set { } } public System.Text.Json.JsonCommentHandling ReadCommentHandling { get { throw null; } set { } } public bool RespectNullableAnnotations { get { throw null; } set { } } + public bool RespectRequiredConstructorParameters { get { throw null; } set { } } public System.Text.Json.Serialization.JsonUnknownTypeHandling UnknownTypeHandling { get { throw null; } set { } } public System.Text.Json.Serialization.JsonUnmappedMemberHandling UnmappedMemberHandling { get { throw null; } set { } } public bool UseStringEnumConverter { get { throw null; } set { } } diff --git a/src/libraries/System.Text.Json/src/Resources/Strings.resx b/src/libraries/System.Text.Json/src/Resources/Strings.resx index e65049fbb87a0..19ebcbd47137a 100644 --- a/src/libraries/System.Text.Json/src/Resources/Strings.resx +++ b/src/libraries/System.Text.Json/src/Resources/Strings.resx @@ -693,7 +693,7 @@ JsonPropertyInfo '{0}' defined in type '{1}' is marked both as required and as an extension data property. This combination is not supported. - JSON deserialization for type '{0}' was missing required properties, including the following: {1} + JSON deserialization for type '{0}' was missing required properties including: {1}. Property '{0}' on type '{1}' is marked with JsonObjectCreationHandling.Populate but it doesn't support populating. This can be either because the property type is immutable or it could use a custom converter. diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/AppContextSwitchHelper.cs b/src/libraries/System.Text.Json/src/System/Text/Json/AppContextSwitchHelper.cs index f2333a19c315f..01ce7b72e6e9c 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/AppContextSwitchHelper.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/AppContextSwitchHelper.cs @@ -16,5 +16,11 @@ internal static class AppContextSwitchHelper switchName: "System.Text.Json.Serialization.RespectNullableAnnotationsDefault", isEnabled: out bool value) ? value : false; + + public static bool RespectRequiredConstructorParametersDefault { get; } = + AppContext.TryGetSwitch( + switchName: "System.Text.Json.Serialization.RespectRequiredConstructorParametersDefault", + isEnabled: out bool value) + ? value : false; } } diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializerOptions.Caching.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializerOptions.Caching.cs index f8add81e50d9d..a9633d439bcae 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializerOptions.Caching.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializerOptions.Caching.cs @@ -508,6 +508,7 @@ public bool Equals(JsonSerializerOptions? left, JsonSerializerOptions? right) left._allowOutOfOrderMetadataProperties == right._allowOutOfOrderMetadataProperties && left._allowTrailingCommas == right._allowTrailingCommas && left._respectNullableAnnotations == right._respectNullableAnnotations && + left._respectRequiredConstructorParameters == right._respectRequiredConstructorParameters && left._ignoreNullValues == right._ignoreNullValues && left._ignoreReadOnlyProperties == right._ignoreReadOnlyProperties && left._ignoreReadonlyFields == right._ignoreReadonlyFields && @@ -567,6 +568,7 @@ public int GetHashCode(JsonSerializerOptions options) AddHashCode(ref hc, options._allowOutOfOrderMetadataProperties); AddHashCode(ref hc, options._allowTrailingCommas); AddHashCode(ref hc, options._respectNullableAnnotations); + AddHashCode(ref hc, options._respectRequiredConstructorParameters); AddHashCode(ref hc, options._ignoreNullValues); AddHashCode(ref hc, options._ignoreReadOnlyProperties); AddHashCode(ref hc, options._ignoreReadonlyFields); diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializerOptions.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializerOptions.cs index acee88788c3a7..1a482bbe24455 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializerOptions.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializerOptions.cs @@ -87,6 +87,7 @@ public static JsonSerializerOptions Web private bool _allowOutOfOrderMetadataProperties; private bool _allowTrailingCommas; private bool _respectNullableAnnotations = AppContextSwitchHelper.RespectNullableAnnotationsDefault; + private bool _respectRequiredConstructorParameters = AppContextSwitchHelper.RespectRequiredConstructorParametersDefault; private bool _ignoreNullValues; private bool _ignoreReadOnlyProperties; private bool _ignoreReadonlyFields; @@ -141,6 +142,7 @@ public JsonSerializerOptions(JsonSerializerOptions options) _allowOutOfOrderMetadataProperties = options._allowOutOfOrderMetadataProperties; _allowTrailingCommas = options._allowTrailingCommas; _respectNullableAnnotations = options._respectNullableAnnotations; + _respectRequiredConstructorParameters = options._respectRequiredConstructorParameters; _ignoreNullValues = options._ignoreNullValues; _ignoreReadOnlyProperties = options._ignoreReadOnlyProperties; _ignoreReadonlyFields = options._ignoreReadonlyFields; @@ -797,6 +799,9 @@ public string NewLine /// Due to restrictions in how nullable reference types are represented at run time, /// this setting only governs nullability annotations of non-generic properties and fields. /// It cannot be used to enforce nullability annotations of root-level types or generic parameters. + /// + /// The default setting for this property can be toggled application-wide using the + /// "System.Text.Json.Serialization.RespectNullableAnnotationsDefault" feature switch. /// public bool RespectNullableAnnotations { @@ -808,6 +813,29 @@ public bool RespectNullableAnnotations } } + /// + /// Gets or sets a value that indicates whether non-optional constructor parameters should be specified during deserialization. + /// + /// + /// Thrown if this property is set after serialization or deserialization has occurred. + /// + /// + /// For historical reasons constructor-based deserialization treats all constructor parameters as optional by default. + /// This flag allows users to toggle that behavior as necessary for each instance. + /// + /// The default setting for this property can be toggled application-wide using the + /// "System.Text.Json.Serialization.RespectRequiredConstructorParametersDefault" feature switch. + /// + public bool RespectRequiredConstructorParameters + { + get => _respectRequiredConstructorParameters; + set + { + VerifyMutable(); + _respectRequiredConstructorParameters = value; + } + } + /// /// Returns true if options uses compatible built-in resolvers or a combination of compatible built-in resolvers. /// diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/JsonParameterInfo.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/JsonParameterInfo.cs index 6adb7a7d8fed3..4db70c6694e22 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/JsonParameterInfo.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/JsonParameterInfo.cs @@ -123,5 +123,9 @@ public ICustomAttributeProvider? AttributeProvider internal JsonNumberHandling? NumberHandling => MatchingProperty.EffectiveNumberHandling; internal JsonTypeInfo JsonTypeInfo => MatchingProperty.JsonTypeInfo; internal bool ShouldDeserialize => !MatchingProperty.IsIgnored; + internal bool IsRequiredParameter => + Options.RespectRequiredConstructorParameters && + !HasDefaultValue && + !IsMemberInitializer; } } diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/JsonPropertyInfo.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/JsonPropertyInfo.cs index 6a805e80162bf..cb7975b38c99c 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/JsonPropertyInfo.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/JsonPropertyInfo.cs @@ -356,7 +356,7 @@ public bool IsRequired } } - private bool _isRequired; + private protected bool _isRequired; /// /// Gets the constructor parameter associated with the current property. @@ -448,7 +448,7 @@ internal void Configure() if (IsRequired) { - if (!CanDeserialize) + if (!CanDeserialize && AssociatedParameter?.IsRequiredParameter != true) { ThrowHelper.ThrowInvalidOperationException_JsonPropertyRequiredAndNotDeserializable(this); } diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/JsonPropertyInfoOfT.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/JsonPropertyInfoOfT.cs index ff31cf97a353d..a0219eafa1622 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/JsonPropertyInfoOfT.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/JsonPropertyInfoOfT.cs @@ -120,6 +120,8 @@ internal override void AddJsonParameterInfo(JsonParameterInfoValues parameterInf AssociatedParameter = new JsonParameterInfo(parameterInfoValues, this); // Overwrite the nullability annotation of property setter with the parameter. _isSetNullable = parameterInfoValues.IsNullable; + // If the property has been associated with a non-optional parameter, mark it as required. + _isRequired |= AssociatedParameter.IsRequiredParameter; } internal new JsonConverter EffectiveConverter diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/ThrowHelper.Serialization.cs b/src/libraries/System.Text.Json/src/System/Text/Json/ThrowHelper.Serialization.cs index 110dcba34fa2c..5b16af8de3eec 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/ThrowHelper.Serialization.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/ThrowHelper.Serialization.cs @@ -296,7 +296,7 @@ public static void ThrowJsonException_JsonRequiredPropertyMissing(JsonTypeInfo p Debug.Assert(parent.PropertyCache != null); // Soft cut-off length - once message becomes longer than that we won't be adding more elements - const int CutOffLength = 50; + const int CutOffLength = 60; foreach (KeyValuePair kvp in parent.PropertyCache.List) { @@ -313,7 +313,9 @@ public static void ThrowJsonException_JsonRequiredPropertyMissing(JsonTypeInfo p listOfMissingPropertiesBuilder.Append(' '); } + listOfMissingPropertiesBuilder.Append('\''); listOfMissingPropertiesBuilder.Append(property.Name); + listOfMissingPropertiesBuilder.Append('\''); first = false; if (listOfMissingPropertiesBuilder.Length >= CutOffLength) diff --git a/src/libraries/System.Text.Json/tests/Common/ConstructorTests/ConstructorTests.ParameterMatching.cs b/src/libraries/System.Text.Json/tests/Common/ConstructorTests/ConstructorTests.ParameterMatching.cs index 2e382dbe75399..e134d42286de9 100644 --- a/src/libraries/System.Text.Json/tests/Common/ConstructorTests/ConstructorTests.ParameterMatching.cs +++ b/src/libraries/System.Text.Json/tests/Common/ConstructorTests/ConstructorTests.ParameterMatching.cs @@ -12,6 +12,8 @@ namespace System.Text.Json.Serialization.Tests { public abstract partial class ConstructorTests : SerializerTests { + private static readonly JsonSerializerOptions s_respectRequiredParamsOptions = new() { RespectRequiredConstructorParameters = true }; + public ConstructorTests(JsonSerializerWrapper stringSerializer) : base(stringSerializer) { @@ -1636,5 +1638,35 @@ public class CustomCtorParameterConverter : JsonConverter public override void Write(Utf8JsonWriter writer, string value, JsonSerializerOptions options) => writer.WriteStringValue(value); } + + [Fact] + public async Task RespectRequiredConstructorParameters_RequiredParameterMissing_ThrowsJsonException() + { + string json = """{"X":1,"Z":3}"""; + JsonException ex = await Assert.ThrowsAsync(() => Serializer.DeserializeWrapper(json, s_respectRequiredParamsOptions)); + Assert.DoesNotContain("'X'", ex.Message); + Assert.Contains("'Y'", ex.Message); + Assert.DoesNotContain("'Z'", ex.Message); + } + + [Fact] + public async Task RespectRequiredConstructorParameters_OptionalParameterMissing_Succeeds() + { + string json = """{"X":1,"Y":2}"""; + Point_3D result = await Serializer.DeserializeWrapper(json, s_respectRequiredParamsOptions); + Assert.Equal(1, result.X); + Assert.Equal(2, result.Y); + Assert.Equal(50, result.Z); + } + + [Fact] + public async Task RespectRequiredConstructorParameters_NoParameterMissing_Succeeds() + { + string json = """{"X":1,"Y":2,"Z":3}"""; + Point_3D result = await Serializer.DeserializeWrapper(json, s_respectRequiredParamsOptions); + Assert.Equal(1, result.X); + Assert.Equal(2, result.Y); + Assert.Equal(3, result.Z); + } } } diff --git a/src/libraries/System.Text.Json/tests/Common/MetadataTests.cs b/src/libraries/System.Text.Json/tests/Common/MetadataTests.cs index f25c5fe80a5be..967a3f4366a13 100644 --- a/src/libraries/System.Text.Json/tests/Common/MetadataTests.cs +++ b/src/libraries/System.Text.Json/tests/Common/MetadataTests.cs @@ -271,6 +271,39 @@ public void JsonTypeInfo_ElementType_ReturnsExpectedValue(Type type, Type? expec Assert.Equal(expectedKeyType, typeInfo.ElementType); } + [Theory] + [InlineData(typeof(ClassWithParameterizedCtor))] + [InlineData(typeof(StructWithParameterizedCtor))] + [InlineData(typeof(ClassWithRequiredAndOptionalConstructorParameters))] + public void RespectRequiredConstructorParameters_false_ReportsCorrespondingPropertiesAsNotRequired(Type type) + { + var options = new JsonSerializerOptions { RespectRequiredConstructorParameters = false }; + JsonTypeInfo typeInfo = Serializer.GetTypeInfo(type, options); + + Assert.NotEmpty(typeInfo.Properties); + Assert.All(typeInfo.Properties, property => + { + Assert.False(property.IsRequired); + }); + } + + [Theory] + [InlineData(typeof(ClassWithParameterizedCtor))] + [InlineData(typeof(StructWithParameterizedCtor))] + [InlineData(typeof(ClassWithRequiredAndOptionalConstructorParameters))] + public void RespectRequiredConstructorParameters_true_ReportsCorrespondingPropertiesAsRequired(Type type) + { + var options = new JsonSerializerOptions { RespectRequiredConstructorParameters = true }; + JsonTypeInfo typeInfo = Serializer.GetTypeInfo(type, options); + + Assert.NotEmpty(typeInfo.Properties); + Assert.All(typeInfo.Properties, property => + { + bool isRequiredParam = property.AssociatedParameter is { HasDefaultValue: false, IsMemberInitializer: false }; + Assert.Equal(isRequiredParam, property.IsRequired); + }); + } + private static object? GetDefaultValue(ParameterInfo parameterInfo) { Type parameterType = parameterInfo.ParameterType; @@ -506,6 +539,18 @@ public sealed class CustomConverter : JsonConverter throw new NotImplementedException(); } } + + internal class ClassWithRequiredAndOptionalConstructorParameters + { + [JsonConstructor] + public ClassWithRequiredAndOptionalConstructorParameters(string? x, string? y = null) + { + X = x; + Y = y; + } + public string? X { get; } + public string? Y { get; } + } } internal class WeatherForecastWithPOCOs diff --git a/src/libraries/System.Text.Json/tests/System.Text.Json.SourceGeneration.Tests/Serialization/MetadataTests.cs b/src/libraries/System.Text.Json/tests/System.Text.Json.SourceGeneration.Tests/Serialization/MetadataTests.cs index 2dee7e6da78f4..f7030eb7c45e9 100644 --- a/src/libraries/System.Text.Json/tests/System.Text.Json.SourceGeneration.Tests/Serialization/MetadataTests.cs +++ b/src/libraries/System.Text.Json/tests/System.Text.Json.SourceGeneration.Tests/Serialization/MetadataTests.cs @@ -39,6 +39,7 @@ public partial class MetadataTests_SourceGen() : MetadataTests(new StringSeriali [JsonSerializable(typeof(ClassWithMultipleConstructors))] [JsonSerializable(typeof(DerivedClassWithShadowingProperties))] [JsonSerializable(typeof(IDerivedInterface))] + [JsonSerializable(typeof(ClassWithRequiredAndOptionalConstructorParameters))] partial class Context : JsonSerializerContext; } } diff --git a/src/libraries/System.Text.Json/tests/System.Text.Json.Tests/Serialization/CacheTests.cs b/src/libraries/System.Text.Json/tests/System.Text.Json.Tests/Serialization/CacheTests.cs index 739be45931b9e..5a75a784cd3e4 100644 --- a/src/libraries/System.Text.Json/tests/System.Text.Json.Tests/Serialization/CacheTests.cs +++ b/src/libraries/System.Text.Json/tests/System.Text.Json.Tests/Serialization/CacheTests.cs @@ -374,6 +374,7 @@ public static void JsonSerializerOptions_EqualityComparer_ChangingAnySettingShou yield return (GetProp(nameof(JsonSerializerOptions.UnknownTypeHandling)), JsonUnknownTypeHandling.JsonNode); yield return (GetProp(nameof(JsonSerializerOptions.WriteIndented)), true); yield return (GetProp(nameof(JsonSerializerOptions.RespectNullableAnnotations)), !JsonSerializerOptions.Default.RespectNullableAnnotations); + yield return (GetProp(nameof(JsonSerializerOptions.RespectRequiredConstructorParameters)), !JsonSerializerOptions.Default.RespectRequiredConstructorParameters); yield return (GetProp(nameof(JsonSerializerOptions.IndentCharacter)), '\t'); yield return (GetProp(nameof(JsonSerializerOptions.IndentSize)), 1); yield return (GetProp(nameof(JsonSerializerOptions.ReferenceHandler)), ReferenceHandler.Preserve); diff --git a/src/libraries/System.Text.Json/tests/System.Text.Json.Tests/Serialization/OptionsTests.cs b/src/libraries/System.Text.Json/tests/System.Text.Json.Tests/Serialization/OptionsTests.cs index f5ef9964d8f1c..f0b9b5ad0ca63 100644 --- a/src/libraries/System.Text.Json/tests/System.Text.Json.Tests/Serialization/OptionsTests.cs +++ b/src/libraries/System.Text.Json/tests/System.Text.Json.Tests/Serialization/OptionsTests.cs @@ -67,6 +67,7 @@ public static void SetOptionsFail() Assert.Equal(JsonUnmappedMemberHandling.Skip, options.UnmappedMemberHandling); Assert.False(options.WriteIndented); Assert.False(options.RespectNullableAnnotations); + Assert.False(options.RespectRequiredConstructorParameters); TestIListNonThrowingOperationsWhenImmutable(options.Converters, tc); TestIListNonThrowingOperationsWhenImmutable(options.TypeInfoResolverChain, options.TypeInfoResolver); @@ -87,6 +88,7 @@ public static void SetOptionsFail() Assert.Throws(() => options.WriteIndented = options.WriteIndented); Assert.Throws(() => options.TypeInfoResolver = options.TypeInfoResolver); Assert.Throws(() => options.RespectNullableAnnotations = options.RespectNullableAnnotations); + Assert.Throws(() => options.RespectRequiredConstructorParameters = options.RespectRequiredConstructorParameters); TestIListThrowingOperationsWhenImmutable(options.Converters, tc); TestIListThrowingOperationsWhenImmutable(options.TypeInfoResolverChain, options.TypeInfoResolver); @@ -986,6 +988,46 @@ public static void Options_NullabilityInfoFeatureSwitchDisabled_ReportsPropertie }, options).Dispose(); } + [SkipOnTargetFramework(TargetFrameworkMonikers.NetFramework)] + [ConditionalTheory(typeof(RemoteExecutor), nameof(RemoteExecutor.IsSupported))] + [InlineData(null)] + [InlineData(false)] + [InlineData(true)] + public static void Options_RespectRequiredConstructorParameters_FeatureSwitch(bool? state) + { + var options = new RemoteInvokeOptions(); + if (state.HasValue) + { + options.RuntimeConfigurationOptions["System.Text.Json.Serialization.RespectRequiredConstructorParametersDefault"] = state.Value; + } + + string arg = state ?? false ? "true" : "false"; + RemoteExecutor.Invoke(static arg => + { + bool shouldRespectRequiredConstructorParameters = bool.Parse(arg); + + var jsonOptions = new JsonSerializerOptions(); + Assert.Equal(shouldRespectRequiredConstructorParameters, jsonOptions.RespectRequiredConstructorParameters); + Assert.Equal(shouldRespectRequiredConstructorParameters, JsonSerializerOptions.Default.RespectRequiredConstructorParameters); + + string json = """{"X":1,"Z":3}"""; + + if (shouldRespectRequiredConstructorParameters) + { + JsonException ex = Assert.Throws(() => JsonSerializer.Deserialize(json)); + Assert.Contains("'Y'", ex.Message); + } + else + { + Point_3D result = JsonSerializer.Deserialize(json); + Assert.Equal(1, result.X); + Assert.Equal(0, result.Y); + Assert.Equal(3, result.Z); + } + + }, arg, options).Dispose(); + } + [SkipOnTargetFramework(TargetFrameworkMonikers.NetFramework)] [ConditionalFact(typeof(RemoteExecutor), nameof(RemoteExecutor.IsSupported))] public static void Options_NullabilityInfoFeatureSwitchDisabled_RespectNullabilityAnnotationsEnabled_ThrowsInvalidOperationException()