Skip to content

[API Proposal]: Expose JsonConverter.CanHaveMetadata #118900

Description

@jamescourtney

Background and motivation

My project is maintained by ~1500 developers with over 200 C# projects. We rely heavily on:

  • System.Text.Json (with extensive use of polymorphism and source generators)
  • Immutable data types with primary constructors
  • ASP.NET Core
  • HttpClient
  • Nullable static analysis

We have recently become aware of RespectNullableAnnotations and RespectRequiredConstructorParameters in STJ. Our codebase predates .NET 9. Since we do not have these flags turned on today, our nullable analysis is suspect since we cannot trust the annotations on types we deserialize with STJ.

We would like to have a gradual migration to enable RespectNullableAnnotations and RespectRequiredConstructorParameters. Simply enabling them globally is not a good option as this is a production service. Further complicating this is that some of our dependencies such as MVC and CosmosDb simply want a single JsonSerializerOptions without the ability to toggle per-request or per-JSON operation.

Our initial approach for this was to define a RampUpJsonConverter<T> : JsonConverter<T> that used a RNG to try strict serialization/deserialization options at a rate that wouldn't impact our SLA while emitting telemetry on failures. This is appealing because:

  • It allows different owners of different parts of the code to migrate at their own pace
  • It avoids us having to duplicate our code and contracts for both "permissive" and "strict" cases.

However, it seems we are completely blocked by STJ with polymorphic types because JsonConverter.CanHaveMetadata is an internal property that gives us no way to override. The exception in STJ is thrown from here.

The current stack also does not permit any workarounds or other hacks. The current implementation of CanHaveMetadata is just: internal virtual bool CanHaveMetadata => false;. This does not permit any reflection twiddling of a backing field or other workaround to unblock ourselves.

API Proposal

The simplest change is to change CanHaveMetadata from internal to public.

API Usage

public class MyConverter : JsonConverter<PolymorphicType>
{
     public override bool CanHaveMetadata => true;
}

Alternative Designs

Alternatively, STJ could expose a new property on JsonConverterFactory that disables caching of the returned converter instances.

public abstract class JsonConverterFactory
{
    public virtual bool EnableCaching => true;
   
    public abstract JsonConverter? GetConverter(...);
}

This would allow a custom factory to implement a delegation pattern while owning caching itself internally. This doesn't work today because the output from JsonConverterFactory is always cached, which forces the RNG and strict/loose decision to be made in a custom JsonConverter, which then hits the blocker around CanHaveMetadata.

Risks

No response

Activity

  1. added
    api-suggestionEarly API idea and discussion, it is NOT ready for implementation
    on Aug 19, 2025
  2. dotnet-policy-service commented on Aug 19, 2025

    @dotnet-policy-service
    Contributor

    Tagging subscribers to this area: @dotnet/area-system-text-json, @gregsdennis
    See info in area-owners.md if you want to be subscribed.

  3. eiriktsarpalis commented on Aug 20, 2025

    @eiriktsarpalis
    Member

    Note that the CanHaveMetadata property is simply an internal marker meant for validation; changing it value has no bearing on whether the underlying converter actually supports polymorphism.

    Today, user-defined converters do not support polymorphism because the publicly available Write and Read methods do not accept serialization state parameters. Sharing serialization state is important in the context of polymorphism because you have two converters writing or reading from the same JSON object, so coordination becomes essential. Built-in converters work because they implement internal APIs to achieve this, however this capability is lost when these converters are wrapped inside a custom converter. There's an issue tracking support for making state-passing converter methods accessible to users, however we had the chance to prioritise this work so far.

    Our initial approach for this was to define a RampUpJsonConverter : JsonConverter that used a RNG to try strict serialization/deserialization options at a rate that wouldn't impact our SLA while emitting telemetry on failures.

    Why not create two separate JsonSerializerOptions instances with separate configurations and use an RNG to pick one of those? It sounds like it might achieve the same effect without faffing about with custom converters.

  4. PranavSenthilnathan commented on Aug 20, 2025

    @PranavSenthilnathan
    Member

    I think the issue that @jamescourtney wants to avoid is having to change all the places where they currently cache a JsonSerializerOptions to store some other JsonSerializerOptionsFactory (which may not even be effective if someone then tries to cache the option they get from it instead of getting a randomized option every time).

    Is it possible for us to just remove the polymorphism restriction during configuration time and make it throw when it's used in an unsupported way? In the mentioned use case, since you're just forwarding to an internal converter at the top level, the wrapping converter won't ever need to support the internal stack based calls. But if someone does try to use it to serialize a nested polymorphic type then we can throw at that point.

  5. eiriktsarpalis commented on Aug 21, 2025

    @eiriktsarpalis
    Member

    I think the issue that @jamescourtney wants to avoid is having to change all the places where they currently cache a JsonSerializerOptions to store some other JsonSerializerOptionsFactory (which may not even be effective if someone then tries to cache the option they get from it instead of getting a randomized option every time).

    I still think that it's a less risky solution compared to injecting custom converters when previously built-in converters were being used. Because of how STJ works, changing from a built-in converter to a custom converter that wraps said built-in converter changes serialization behavior in subtle and multi-faceted ways. The CanHaveMetadata issue being cited here is just the tip of the iceberg.

  6. added
    needs-further-triageIssue has been initially triaged, but needs deeper consideration or reconsideration
    and removed
    untriagedNew issue has not been triaged by the area owner
    on Aug 31, 2025
  7. added this to the Future milestone on Aug 31, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    api-suggestionEarly API idea and discussion, it is NOT ready for implementationarea-System.Text.Jsonneeds-further-triageIssue has been initially triaged, but needs deeper consideration or reconsideration

    Type

    No type

    Projects

    No projects

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions