diff --git a/src/libraries/System.Text.Json/Common/JsonSerializerDefaults.cs b/src/libraries/System.Text.Json/Common/JsonSerializerDefaults.cs index fe762976f8a05d..7f7b609865badd 100644 --- a/src/libraries/System.Text.Json/Common/JsonSerializerDefaults.cs +++ b/src/libraries/System.Text.Json/Common/JsonSerializerDefaults.cs @@ -15,12 +15,28 @@ public enum JsonSerializerDefaults /// This option implies that property names are treated as case-sensitive and that "PascalCase" name formatting should be employed. /// General = 0, + /// /// Specifies that values should be used more appropriate to web-based scenarios. /// /// /// This option implies that property names are treated as case-insensitive and that "camelCase" name formatting should be employed. /// - Web = 1 + Web = 1, + + /// + /// Specifies that stricter policies should be applied when deserializing from JSON. + /// + /// + /// JSON produced with can be deserialized with . + /// The following policies are used: + /// + /// Property names are treated as case-sensitive and "PascalCase" name formatting is employed. + /// Properties that cannot be mapped to a .NET member are not allowed. + /// Properties with duplicate names are not allowed. + /// Nullable annotations and required constructor parameters are respected. + /// + /// + Strict = 2, } } 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 616934dc9ac456..04378981084d2c 100644 --- a/src/libraries/System.Text.Json/ref/System.Text.Json.cs +++ b/src/libraries/System.Text.Json/ref/System.Text.Json.cs @@ -388,6 +388,7 @@ public enum JsonSerializerDefaults { General = 0, Web = 1, + Strict = 2, } public sealed partial class JsonSerializerOptions { @@ -421,6 +422,7 @@ public JsonSerializerOptions(System.Text.Json.JsonSerializerOptions options) { } 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 static System.Text.Json.JsonSerializerOptions Strict { [System.Diagnostics.CodeAnalysis.RequiresDynamicCodeAttribute("JSON serialization and deserialization might require types that cannot be statically analyzed and might need runtime code generation. Use System.Text.Json source generation for native AOT applications."), System.Diagnostics.CodeAnalysis.RequiresUnreferencedCodeAttribute("JSON serialization and deserialization might require types that cannot be statically analyzed. Use the overload that takes a JsonTypeInfo or JsonSerializerContext, or make sure all of the required types are preserved.")] get { throw null; } } 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 { } } 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 01e426eb1aaaea..00f913e068ee38 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 @@ -39,14 +39,9 @@ public static JsonSerializerOptions Default { [RequiresUnreferencedCode(JsonSerializer.SerializationUnreferencedCodeMessage)] [RequiresDynamicCode(JsonSerializer.SerializationRequiresDynamicCodeMessage)] - get - { - return s_defaultOptions ?? GetOrCreateSingleton(ref s_defaultOptions, JsonSerializerDefaults.General); - } + get => field ?? GetOrCreateSingleton(ref field, JsonSerializerDefaults.General); } - private static JsonSerializerOptions? s_defaultOptions; - /// /// Gets a read-only, singleton instance of that uses the web configuration. /// @@ -59,13 +54,23 @@ public static JsonSerializerOptions Web { [RequiresUnreferencedCode(JsonSerializer.SerializationUnreferencedCodeMessage)] [RequiresDynamicCode(JsonSerializer.SerializationRequiresDynamicCodeMessage)] - get - { - return s_webOptions ?? GetOrCreateSingleton(ref s_webOptions, JsonSerializerDefaults.Web); - } + get => field ?? GetOrCreateSingleton(ref field, JsonSerializerDefaults.Web); } - private static JsonSerializerOptions? s_webOptions; + /// + /// Gets a read-only, singleton instance of that uses the strict configuration. + /// + /// + /// Each instance encapsulates its own serialization metadata caches, + /// so using fresh default instances every time one is needed can result in redundant recomputation of converters. + /// This property provides a shared instance that can be consumed by any number of components without necessitating any converter recomputation. + /// + public static JsonSerializerOptions Strict + { + [RequiresUnreferencedCode(JsonSerializer.SerializationUnreferencedCodeMessage)] + [RequiresDynamicCode(JsonSerializer.SerializationRequiresDynamicCodeMessage)] + get => field ?? GetOrCreateSingleton(ref field, JsonSerializerDefaults.Strict); + } // For any new option added, consider adding it to the options copied in the copy constructor below // and consider updating the EqualtyComparer used for comparing CachingContexts. @@ -172,6 +177,13 @@ public JsonSerializerOptions(JsonSerializerDefaults defaults) : this() _jsonPropertyNamingPolicy = JsonNamingPolicy.CamelCase; _numberHandling = JsonNumberHandling.AllowReadingFromString; } + else if (defaults == JsonSerializerDefaults.Strict) + { + _unmappedMemberHandling = JsonUnmappedMemberHandling.Disallow; + _allowDuplicateProperties = false; + _respectNullableAnnotations = true; + _respectRequiredConstructorParameters = true; + } else if (defaults != JsonSerializerDefaults.General) { throw new ArgumentOutOfRangeException(nameof(defaults)); diff --git a/src/libraries/System.Text.Json/tests/System.Text.Json.SourceGeneration.Tests/JsonSourceGenerationOptionsTests.cs b/src/libraries/System.Text.Json/tests/System.Text.Json.SourceGeneration.Tests/JsonSourceGenerationOptionsTests.cs index 17f5e125a84c1b..5c4cc405d9c463 100644 --- a/src/libraries/System.Text.Json/tests/System.Text.Json.SourceGeneration.Tests/JsonSourceGenerationOptionsTests.cs +++ b/src/libraries/System.Text.Json/tests/System.Text.Json.SourceGeneration.Tests/JsonSourceGenerationOptionsTests.cs @@ -37,6 +37,20 @@ public static void ContextWithWebSerializerDefaults_GeneratesExpectedOptions() public partial class ContextWithWebSerializerDefaults : JsonSerializerContext { } + [Fact] + public static void ContextWithStrictSerializerDefaults_GeneratesExpectedOptions() + { + JsonSerializerOptions expected = new(JsonSerializerDefaults.Strict) { TypeInfoResolver = ContextWithStrictSerializerDefaults.Default }; + JsonSerializerOptions options = ContextWithStrictSerializerDefaults.Default.Options; + + JsonTestHelper.AssertOptionsEqual(expected, options); + } + + [JsonSourceGenerationOptions(JsonSerializerDefaults.Strict)] + [JsonSerializable(typeof(PersonStruct))] + public partial class ContextWithStrictSerializerDefaults : JsonSerializerContext + { } + [Fact] public static void ContextWithWebDefaultsAndOverriddenPropertyNamingPolicy_GeneratesExpectedOptions() { 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 840a5e1079e50f..17141dabfd8771 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 @@ -1302,6 +1302,54 @@ public static void JsonSerializerOptions_Web_IsReadOnly() Assert.Throws(() => new JsonContext(optionsSingleton)); } + [Fact] + public static void JsonSerializerOptions_Strict_MatchesConstructorWithJsonSerializerDefaults() + { + var options = new JsonSerializerOptions(JsonSerializerDefaults.Strict) + { + TypeInfoResolver = JsonSerializerOptions.Default.TypeInfoResolver + }; + + JsonSerializerOptions optionsSingleton = JsonSerializerOptions.Strict; + + AssertExtensions.FalseExpression(options.AllowDuplicateProperties); + AssertExtensions.FalseExpression(options.PropertyNameCaseInsensitive); + AssertExtensions.TrueExpression(options.RespectNullableAnnotations); + AssertExtensions.TrueExpression(options.RespectRequiredConstructorParameters); + Assert.Equal(JsonUnmappedMemberHandling.Disallow, options.UnmappedMemberHandling); + + JsonTestHelper.AssertOptionsEqual(options, optionsSingleton); + } + + [Fact] + public static void JsonSerializerOptions_Strict_SerializesWithExpectedSettings() + { + JsonSerializerOptions options = JsonSerializerOptions.Strict; + AssertExtensions.ThrowsContains( + () => JsonSerializer.Deserialize>("""{"foo":1, "foo":2}""", options), + "Duplicate"); + } + + [Fact] + public static void JsonSerializerOptions_Strict_ReturnsSameInstance() + { + Assert.Same(JsonSerializerOptions.Strict, JsonSerializerOptions.Strict); + } + + [Fact] + public static void JsonSerializerOptions_Strict_IsReadOnly() + { + var optionsSingleton = JsonSerializerOptions.Strict; + Assert.True(optionsSingleton.IsReadOnly); + Assert.Throws(() => optionsSingleton.AllowDuplicateProperties = true); + Assert.Throws(() => optionsSingleton.PropertyNameCaseInsensitive = true); + Assert.Throws(() => optionsSingleton.RespectNullableAnnotations = false); + Assert.Throws(() => optionsSingleton.RespectRequiredConstructorParameters = false); + Assert.Throws(() => optionsSingleton.UnmappedMemberHandling = JsonUnmappedMemberHandling.Skip); + + Assert.Throws(() => new JsonContext(optionsSingleton)); + } + [Theory] [MemberData(nameof(GetInitialTypeInfoResolversAndExpectedChains))] public static void TypeInfoResolverChain_SetTypeInfoResolver_ReturnsExpectedChain( @@ -1543,7 +1591,7 @@ public static void PredefinedSerializerOptions_Web() [Theory] [InlineData(-1)] - [InlineData(2)] + [InlineData(3)] public static void PredefinedSerializerOptions_UnhandledDefaults(int enumValue) { var outOfRangeSerializerDefaults = (JsonSerializerDefaults)enumValue;