Skip to content
Merged
Show file tree
Hide file tree
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
Expand Up @@ -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
Expand Up @@ -388,6 +388,7 @@ public enum JsonSerializerDefaults
{
General = 0,
Web = 1,
Strict = 2,
}
public sealed partial class JsonSerializerOptions
{
Expand Down Expand Up @@ -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 { } }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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>
Expand All @@ -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.
Expand Down Expand Up @@ -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));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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()
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down Expand Up @@ -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;
Expand Down
Loading