-
Notifications
You must be signed in to change notification settings - Fork 10.4k
Description
It came up in API review for MakeReadOnly(bool populateMissingResolver) that the pattern of conditionally setting JsonSerializerOptions.TypeInfoResolver
before calling MakeReadOnly
is not thread safe and can lead to InvalidOperationExceptions like those in dotnet/runtime#89830 when there's a race.
At first blush, GetReadOnlyTypeInfo and the SystemTextJsonOutputFormatter ctor appear to have the same thread safety issue. Of course, if we can guarantee these never run concurrently, there might not be an issue. That could explain why our tests haven't caught this, but I think it's more likely that we just don't have any functional tests where TypeInfoResolver
would be null by the time it gets to this code.
aspnetcore/src/Shared/Json/JsonSerializerExtensions.cs
Lines 19 to 25 in f1cfa8e
public static JsonTypeInfo GetReadOnlyTypeInfo(this JsonSerializerOptions options, Type type) | |
{ | |
// Use JsonTypeInfoResolver.Combine() to produce an empty TypeInfoResolver | |
options.TypeInfoResolver ??= JsonTypeInfoResolver.Combine(); | |
options.MakeReadOnly(); | |
return options.GetTypeInfo(type); | |
} |
aspnetcore/src/Mvc/Mvc.Core/src/Formatters/SystemTextJsonOutputFormatter.cs
Lines 26 to 28 in f1cfa8e
// Use JsonTypeInfoResolver.Combine() to produce an empty TypeInfoResolver | |
jsonSerializerOptions.TypeInfoResolver ??= JsonTypeInfoResolver.Combine(); | |
jsonSerializerOptions.MakeReadOnly(); |
It appears we won't be able to use the newly approved thread-safe MakeReadOnly(bool populateMissingResolver)
API because we try to set an empty rather than the default reflection-based TypeInfoResolver
. I'm not sure why that is. Is it an attempt to reduce the need for suppressions when we're doing serialization that is not statically verifiable to be linker safe?
I get that it's probably unusual to get a null TypeInfoResolver
unless reflection is really not supported given who JsonOptions.DefaultSerializerOptions
gets configured, but it still seems nicer to at least try to fallback to reflection rather than just fail to serialize or deserialize if there's even a small chance that it might work. Wouldn't it effectively just change the exception that's thrown if the reflection fallback fails? Probably to something with a clearer error message?