Skip to content

JsonSerializerOptions should provide explicit control over chained IJsonTypeInfoResolvers #83095

Closed
@eiriktsarpalis

Description

@eiriktsarpalis

Background and motivation

When shipped in .NET 7, the contract customization feature added support for chaining resolvers by means of the JsonTypeInfoResolver.Combine method:

var options = new JsonTypeInfoResolver
{
    TypeInfoResolver = JsonTypeInfoResolver.Combine(ContextA.Default, ContextB.Default, ContextC.Default);
};

Based on feedback we've received, this approach has a couple of usability issues:

  1. It necessitates specifying all chained components at one call site -- resolvers cannot be prepended or appended to the chain after the fact.
  2. Because the chaining implementation is abstracted behind a IJsonTypeInfoResolver implementation, there is no way for users to introspect the chain or remove components from it.

API Proposal

namespace System.Text.Json;

public partial class JsonSerializerOptions
{
    public IJsonTypeInfoResolver? TypeInfoResolver { get; set; }
+   public IList<IJsonTypeInfoResolver> TypeInfoResolverChain { get; }
}

With this API added, we should also revert the recent change to AddContext in #80698 since it provided an inadequate attempt to address the same underlying issue:

  1. It only allows appending resolvers, prepending is not possible
  2. It only works with JsonSerializerContext, not resolvers in general.
  3. It introduces a breaking change compared to the AddContext method in .NET 7.

API Usage

The new property should complement and be mutually consistent with the existing TypeInfoResolver property. Here are a few examples:

Setting a resolver to TypeInfoResolver

JsonSerializerOptions options = new();
options.TypeInfoResolver = new DefaultJsonTypeInfoResolver();

Assert.Equal(new IJsonTypeInfoResolver[] { options.TypeInfoResolver }, options.ChainedTypeInfoResolvers);

Setting a combined resolver to TypeInfoResolver

JsonSerializerOptions options = new();
options.TypeInfoResolver = JsonTypeInfoResolver.Combine(CtxA.Default, CtxB.Default, CtxC.Default);

Assert.Equal(new IJsonTypeInfoResolver[] { CtxA.Default, CtxB.Default, CtxC.Default }, options.ChainedTypeInfoResolvers);
Assert.Same(options.TypeInfoResolver, options.ChainedTypeInfoResolvers);

Appending a resolver to ChainedTypeInfoResolvers

JsonSerializerOptions options = new();
options.ChainedTypeInfoResolvers.Add(CtxA.Default);
options.ChainedTypeInfoResolvers.Add(CtxB.Default);
options.ChainedTypeInfoResolvers.Add(CtxC.Default);

Assert.Equal(new IJsonTypeInfoResolver[] { CtxA.Default, CtxB.Default, CtxC.Default }, options.ChainedTypeInfoResolvers);
Assert.Same(options.TypeInfoResolver, options.ChainedTypeInfoResolvers);

Prepending a resolver to ChainedTypeInfoResolvers:

JsonSerializerOptions options = new();
DefaultJsonTypeInfoResolver defaultResolver = new();
options.TypeInfoResolver = defaultResolver ;

Assert.Equal(new IJsonTypeInfoResolver[] { defaultResolver }, options.ChainedTypeInfoResolvers);
options.ChainedTypeInfoResolvers.Insert(0, CtxA.Default);
Assert.Equal(new IJsonTypeInfoResolver[] { CtxA.Default, defaultResolver }, options.ChainedTypeInfoResolvers);

cc @brunolins16 @davidfowl @eerhardt

Metadata

Metadata

Labels

api-approvedAPI was approved in API review, it can be implementedarea-System.Text.JsonblockingMarks issues that we want to fast track in order to unblock other important work

Type

No type

Projects

No projects

Milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions