Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

System.Text.Json API is there something like IContractResolver

In the new System.Text.Json; namespace is there something like IContractResolver i am trying to migrate my project away from Newtonsoft.

This is one of the classes i am trying to move:

public class SelectiveSerializer : DefaultContractResolver
{
private readonly string[] fields;

public SelectiveSerializer(string fields)
{
  var fieldColl = fields.Split(',');
  this.fields = fieldColl
      .Select(f => f.ToLower().Trim())
      .ToArray();
}

protected override JsonProperty CreateProperty(MemberInfo member, MemberSerialization memberSerialization)
{
  var property = base.CreateProperty(member, memberSerialization);
  property.ShouldSerialize = o => fields.Contains(member.Name.ToLower());

  return property;
}
}
like image 795
Dživo Jelić Avatar asked Feb 26 '26 03:02

Dživo Jelić


1 Answers

Contract customization has been implemented in .NET 7, starting with 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:

  1. You can subclass DefaultJsonTypeInfoResolver and override GetTypeInfo(Type, JsonSerializerOptions). This resembles overriding Json.NET's DefaultContractResolver.CreateContract().

  2. 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.

  3. 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:

  • JsonPropertyInfo.ShouldSerialize (also new in .NET 7) can be used for conditional serialization of properties.

    In both System.Text.Json and Newtonsoft, returning false from ShouldSerialize prevents the member from being serialized -- but does not prevent it from being deserialized. If you have some member that should be neither serialized nor deserialized (e.g. for security reasons) you must remove it entirely from the Properties list e.g. as shown in How can I exclude properties from JSON serialization using custom logic in System.Text.Json without using attribute?.

  • When a JsonPropertyInfo was created by the reflection or source-gen resolvers, JsonPropertyInfo.AttributeProvider will be the underlying PropertyInfo or FieldInfo.

    For confirmation see this comment by layomia to System.Text.Json: In .NET 7, how can I determine the JsonPropertyInfo created for a specific member, so I can customize the serialization of that member? #77761.

  • All serialization metadata should be constructed using locale-invariant string logic. In your code you use ToLower() but it would have been better to use ToLowerInvariant(). In my modifier action I use StringComparer.OrdinalIgnoreCase which avoids the need to lowercase the strings.

  • System.Text.Json is case-sensitive by default so you might want to use case-sensitive property name matching when filtering selected fields.

Demo fiddle here.

like image 77
dbc Avatar answered Feb 27 '26 18:02

dbc