Skip to content

Add Strict JsonSerializerOptions #116271

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 17 additions & 1 deletion src/libraries/System.Text.Json/Common/JsonSerializerDefaults.cs
Original file line number Diff line number Diff line change
@@ -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.
/// </remarks>
General = 0,

/// <summary>
/// Specifies that values should be used more appropriate to web-based scenarios.
/// </summary>
/// <remarks>
/// This option implies that property names are treated as case-insensitive and that "camelCase" name formatting should be employed.
/// </remarks>
Web = 1
Web = 1,

/// <summary>
/// Specifies that stricter policies should be applied when deserializing from JSON.
/// </summary>
/// <remarks>
/// JSON produced with <see cref="General"/> can be deserialized with <see cref="Strict"/>.
/// The following policies are used:
/// <list type="bullet">
/// <item>Property names are treated as case-sensitive and "PascalCase" name formatting is employed.</item>
/// <item>Properties that cannot be mapped to a .NET member are not allowed.</item>
/// <item>Properties with duplicate names are not allowed.</item>
/// <item>Nullable annotations and required constructor parameters are respected.</item>
/// </list>
/// </remarks>
Strict = 2,
}
}
2 changes: 2 additions & 0 deletions src/libraries/System.Text.Json/ref/System.Text.Json.cs
Original file line number Diff line number Diff line change
@@ -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<System.Text.Json.Serialization.Metadata.IJsonTypeInfoResolver> TypeInfoResolverChain { get { throw null; } }
public System.Text.Json.Serialization.JsonUnknownTypeHandling UnknownTypeHandling { get { throw null; } set { } }
Original file line number Diff line number Diff line change
@@ -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;

/// <summary>
/// Gets a read-only, singleton instance of <see cref="JsonSerializerOptions" /> that uses the web configuration.
/// </summary>
@@ -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;
/// <summary>
/// Gets a read-only, singleton instance of <see cref="JsonSerializerOptions" /> that uses the strict configuration.
/// </summary>
/// <remarks>
/// Each <see cref="JsonSerializerOptions" /> 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.
/// </remarks>
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));
Original file line number Diff line number Diff line change
@@ -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()
{
Original file line number Diff line number Diff line change
@@ -1302,6 +1302,54 @@ public static void JsonSerializerOptions_Web_IsReadOnly()
Assert.Throws<InvalidOperationException>(() => 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<JsonException>(
() => JsonSerializer.Deserialize<Dictionary<string, int>>("""{"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<InvalidOperationException>(() => optionsSingleton.AllowDuplicateProperties = true);
Assert.Throws<InvalidOperationException>(() => optionsSingleton.PropertyNameCaseInsensitive = true);
Assert.Throws<InvalidOperationException>(() => optionsSingleton.RespectNullableAnnotations = false);
Assert.Throws<InvalidOperationException>(() => optionsSingleton.RespectRequiredConstructorParameters = false);
Assert.Throws<InvalidOperationException>(() => optionsSingleton.UnmappedMemberHandling = JsonUnmappedMemberHandling.Skip);

Assert.Throws<InvalidOperationException>(() => 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;
Loading
Oops, something went wrong.