-
Notifications
You must be signed in to change notification settings - Fork 10.3k
Tweak configuration of JsonSerializerOptions #49875
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
Conversation
...DelegateGenerator/Baselines/MapAction_ExplicitBodyParam_ComplexReturn_Snapshot.generated.txt
Outdated
Show resolved
Hide resolved
@@ -506,6 +506,7 @@ namespace Microsoft.AspNetCore.Http.Generated | |||
{{GeneratedCodeAttribute}} | |||
file static class GeneratedRouteBuilderExtensionsCore | |||
{ | |||
private static readonly JsonOptions FallbackJsonOptions = new(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You should try setting a resolver here, otherwise the singleton itself would be susceptible to races as multiple threads attempt to initialize it.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We shouldn't need to set the resolver here.
The JsonSerializerOptions and JsonTypeInfoResolver get lazy initialized in JsonOptions here:
aspnetcore/src/Http/Http.Extensions/src/JsonOptions.cs
Lines 27 to 34 in 9427854
TypeInfoResolver = JsonSerializer.IsReflectionEnabledByDefault ? CreateDefaultTypeResolver() : null | |
}; | |
// Use a copy so the defaults are not modified. | |
/// <summary> | |
/// Gets the <see cref="JsonSerializerOptions"/>. | |
/// </summary> | |
public JsonSerializerOptions SerializerOptions { get; } = new JsonSerializerOptions(DefaultSerializerOptions); |
when IsReflectionEnabledByDeafult=false
, the Resolver may be null. We should fix the thread safety concerns all up (RDG, RDF, and MVC) with #49849.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It would depend on how DefaultSerializerOptions
is defined. If it contains a resolver it would get picked up by the copy constructor.
Apropos, does the singleton need to be JsonOptions
here. Seems it might as well be a JsonSerializerOptions
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It would depend on how DefaultSerializerOptions is defined. If it contains a resolver it would get picked up by the copy constructor.
DefaultSerializerOptions
doesn't initialize a resolver AFAICT (it's just new JsonSerlizerOptions(JsonDefaults.Web
) so I think being explicit here helps.
Apropos, does the singleton need to be JsonOptions here. Seems it might as well be a JsonSerializerOptions.
True.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It would depend on how DefaultSerializerOptions is defined.
It is part of the above linked code. I missed a couple lines.
aspnetcore/src/Http/Http.Extensions/src/JsonOptions.cs
Lines 16 to 34 in 9427854
internal static readonly JsonSerializerOptions DefaultSerializerOptions = new JsonSerializerOptions(JsonSerializerDefaults.Web) | |
{ | |
// Web defaults don't use the relaxed JSON escaping encoder. | |
// | |
// Because these options are for producing content that is written directly to the request | |
// (and not embedded in an HTML page for example), we can use UnsafeRelaxedJsonEscaping. | |
Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping, | |
// The JsonSerializerOptions.GetTypeInfo method is called directly and needs a defined resolver | |
// setting the default resolver (reflection-based) but the user can overwrite it directly or by modifying | |
// the TypeInfoResolverChain | |
TypeInfoResolver = JsonSerializer.IsReflectionEnabledByDefault ? CreateDefaultTypeResolver() : null | |
}; | |
// Use a copy so the defaults are not modified. | |
/// <summary> | |
/// Gets the <see cref="JsonSerializerOptions"/>. | |
/// </summary> | |
public JsonSerializerOptions SerializerOptions { get; } = new JsonSerializerOptions(DefaultSerializerOptions); |
Apropos, does the singleton need to be JsonOptions here. Seems it might as well be a JsonSerializerOptions.
It should be using the DefaultSerializerOptions
that is in JsonOptions because this is where our default JsonSerializerOptions is defined. We shouldn't be defining a new one here in generated code.
var jsonOptions = serviceProvider?.GetService<IOptions<JsonOptions>>()?.Value ?? FallbackJsonOptions; | ||
var jsonSerializerOptions = jsonOptions.SerializerOptions; | ||
jsonSerializerOptions.TypeInfoResolver ??= JsonTypeInfoResolver.Combine(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Assuming the singleton is fully configured, couldn't this be written without any mutations as follows?
var jsonOptions = serviceProvider?.GetService<IOptions<JsonOptions>>()?.Value ?? FallbackJsonOptions; | |
var jsonSerializerOptions = jsonOptions.SerializerOptions; | |
jsonSerializerOptions.TypeInfoResolver ??= JsonTypeInfoResolver.Combine(); | |
var jsonSerializerOptions = serviceProvider?.GetService<IOptions<JsonOptions>>()?.Value?.SerializerOptions; | |
if (jsonSerializerOptions is null or { TypeInfoResolver: null }) | |
{ | |
// FallbackJsonOptions configured with the empty resolver. | |
jsonSerializerOptions = FallbackJsonOptions.Value.SerializerOptions; | |
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Note that JsonOptions FallbackJsonOptions = new();
does NOT have an empty resolver when JsonSerializer.IsReflectionEnabledByDefault == false
.
TypeInfoResolver = JsonSerializer.IsReflectionEnabledByDefault ? CreateDefaultTypeResolver() : null |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That would be great...wouldn't we run the risk of overriding scenarios where the user set the serializer options without an explicit type info resolver but other properties set?
builder.Services.ConfigureHttpJsonOptions(o => o.SerializerOptions.AllowTrailingCommas = true);
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Note that JsonOptions FallbackJsonOptions = new(); does NOT have an empty resolver when JsonSerializer.IsReflectionEnabledByDefault == false.
Correct! One implication that this has for our tests is the fact that our RDG tests are implicitly using the reflection-based type resolver for scenarios where we don't define JsonOptions
. I realized this after trying to initialize a JsonSerializerOptions from generated code and discovering the implications of not using the one defined in JsonOptions
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Note that
JsonOptions FallbackJsonOptions = new();
does NOT have an empty resolver whenJsonSerializer.IsReflectionEnabledByDefault == false
.
That's true for JsonOptions FallbackJsonOptions = new();
, but that's not necessarily true for serviceProvider?.GetService<IOptions<JsonOptions>>()?.Value
.
Someone could call JsonOptions.SerializerOptions.TypeInfoResolver = null
, but I see we've added a test for this. Now that we no longer assign to JsonOptions.SerializerOptions.TypeInfoResolver
when its null like the code originally referenced in this comment, I think we're fine.
src/Http/Http.Extensions/gen/StaticRouteHandlerModel/Emitters/EndpointJsonPreparationEmitter.cs
Outdated
Show resolved
Hide resolved
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This LGTM. I'm slightly nervous about changing the TypeInfoResolver to be non-null so late, but I think this is the best solution to this problem.
Note that this change also fixes #49849. That should be closed when this is merged.
@@ -262,8 +262,8 @@ public void WriteResponseBodyAsync_Works_WhenTypeResolverIsNull() | |||
var jsonOptions = new JsonOptions(); | |||
jsonOptions.JsonSerializerOptions.TypeInfoResolver = null; | |||
|
|||
var stjOutputFormatter = SystemTextJsonOutputFormatter.CreateFormatter(jsonOptions); | |||
Assert.IsAssignableFrom<IJsonTypeInfoResolver>(stjOutputFormatter.SerializerOptions.TypeInfoResolver); | |||
var exception = Assert.Throws<InvalidOperationException>(() => SystemTextJsonOutputFormatter.CreateFormatter(jsonOptions)); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
(nit) the test name should probably be updated since this scenario no longer "works".
// the TypeInfoResolverChain | ||
TypeInfoResolver = JsonSerializer.IsReflectionEnabledByDefault ? CreateDefaultTypeResolver() : null | ||
// the TypeInfoResolverChain. Use JsonTypeInfoResolver.Combine() to produce an empty TypeInfoResolver. | ||
TypeInfoResolver = JsonSerializer.IsReflectionEnabledByDefault ? CreateDefaultTypeResolver() : JsonTypeInfoResolver.Combine() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
When trying to serialize with PublishTrimmed == true
this will throw a "No metadata for type 'Foo'" error message. Consider authoring a custom resolver that throws a desirable error message. You could also have STJ do it for you by setting the value to JsonSerializerOptions.Default.TypeInfoResolver
(although that would require a suppresion).
Fixes #49855.
Fixes #49849.