Contract customization will be implemented in .NET 7, and is available in Preview 6.
From the documentation page What’s new in System.Text.Json in .NET 7: Contract Customization by Eirik Tsarpalis, Krzysztof Wicher and Layomi Akinrinade:
The contract metadata for a given type T
is represented using JsonTypeInfo<T>
, which in previous versions served as an opaque token used exclusively in source generator APIs. Starting in .NET 7, most facets of the JsonTypeInfo
contract metadata have been exposed and made user-modifiable. Contract customization allows users to write their own JSON contract resolution logic using implementations of the IJsonTypeInfoResolver
interface:
public interface IJsonTypeInfoResolver
{
JsonTypeInfo? GetTypeInfo(Type type, JsonSerializerOptions options);
}
A contract resolver returns a configured JsonTypeInfo
instance for the given Type
and JsonSerializerOptions
combination. It can return null if the resolver does not support metadata for the specified input type.
Contract resolution performed by the default, reflection-based serializer is now exposed via the DefaultJsonTypeInfoResolver
class, which implements IJsonTypeInfoResolver
.
Starting from .NET 7 the JsonSerializerContext
class used in source generation also implements IJsonTypeInfoResolver
.
You can create your own IJsonTypeInfoResolver
via one of the following methods:
You can subclass DefaultJsonTypeInfoResolver
and override GetTypeInfo(Type, JsonSerializerOptions)
. This resembles overriding Json.NET's DefaultContractResolver.CreateContract()
.
You can add an Action<JsonTypeInfo>
to DefaultJsonTypeInfoResolver.Modifiers
to modify the default JsonTypeInfo
generated for selected types after creation.
Combining multiple customizations looks easier with this approach than with the inheritance approach. However, since the modifier actions are applied in order, there is a chance that later modifiers could conflict with earlier modifiers.
You could create your own IJsonTypeInfoResolver
from scratch that creates contracts only for those types that interest you, and combine it with some other type info resolver via JsonTypeInfoResolver.Combine(IJsonTypeInfoResolver[])
.
JsonTypeInfoResolver.Combine()
is also useful when you want to use compile-time generated JsonSerializerContext
instances with a runtime contract resolver that customizes serialization for certain types only.
Once you have a custom resolver, you can set it via JsonSerializerOptions.TypeInfoResolver
.
Thus your SelectiveSerializer
can be converted to a DefaultJsonTypeInfoResolver
roughly as follows, using modifiers. First define the following fluent extension methods:
public static partial class JsonSerializerExtensions
{
public static DefaultJsonTypeInfoResolver SerializeSelectedFields(this DefaultJsonTypeInfoResolver resolver, string fields) =>
SerializeSelectedFields(resolver, fields?.Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries) ?? throw new ArgumentNullException(nameof(fields)));
public static DefaultJsonTypeInfoResolver SerializeSelectedFields(this DefaultJsonTypeInfoResolver resolver, IEnumerable<string> membersToSerialize)
{
if (resolver == null)
throw new ArgumentNullException(nameof(resolver));
if (membersToSerialize == null)
throw new ArgumentNullException(nameof(membersToSerialize));
var membersToSerializeSet = membersToSerialize.ToHashSet(StringComparer.OrdinalIgnoreCase); // Possibly this should be changed to StringComparer.Ordinal
resolver.Modifiers.Add(typeInfo =>
{
if (typeInfo.Kind == JsonTypeInfoKind.Object)
{
foreach (var property in typeInfo.Properties)
{
if (property.GetMemberName() is {} name && !membersToSerializeSet.Contains(name))
property.ShouldSerialize = static (obj, value) => false;
}
}
});
return resolver;
}
public static string? GetMemberName(this JsonPropertyInfo property) => (property.AttributeProvider as MemberInfo)?.Name;
}
And now you can set up your JsonSerializerOptions
e.g. as follows:
var options = new JsonSerializerOptions
{
TypeInfoResolver = new DefaultJsonTypeInfoResolver()
.SerializeSelectedFields("FirstName,Email,Id"),
// Add other options as required
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
WriteIndented = true,
};
Notes: