Skip to content

Investigate thread safety of GetReadOnlyTypeInfo and SystemTextJsonOutputFormatter #49849

@halter73

Description

@halter73

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.

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);
}

// 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?

@eerhardt @eiriktsarpalis @captainsafia

Metadata

Metadata

Assignees

No one assigned

    Labels

    NativeAOTarea-minimalIncludes minimal APIs, endpoint filters, parameter binding, request delegate generator etcarea-mvcIncludes: MVC, Actions and Controllers, Localization, CORS, most templates

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions