Dživo Jelić
Dživo Jelić

Reputation: 2143

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

Upvotes: 13

Views: 8195

Answers (2)

dbc
dbc

Reputation: 117036

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:

  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:

Upvotes: 8

The equivalent types in System.Text.Json -- JsonClassInfo and JsonPropertyInfo -- are internal. There is an open enhancement Equivalent of DefaultContractResolver in System.Text.Json #31257 asking for a public equivalent. – dbc Nov 25 at 19:11

Github issues:

Please try this:
I wrote this as an extension to System.Text.Json to offer missing features: https://github.com/dahomey-technologies/Dahomey.Json.

You will find support for programmatic object mapping.

Define your own implementation of IObjectMappingConvention:

public class SelectiveSerializer : IObjectMappingConvention
{
    private readonly IObjectMappingConvention defaultObjectMappingConvention = new DefaultObjectMappingConvention();
    private readonly string[] fields;

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

    public void Apply<T>(JsonSerializerOptions options, ObjectMapping<T> objectMapping) where T : class
    {
        defaultObjectMappingConvention.Apply<T>(options, objectMapping);
        foreach (IMemberMapping memberMapping in objectMapping.MemberMappings)
        {
            if (memberMapping is MemberMapping<T> member)
            {
                member.SetShouldSerializeMethod(o => fields.Contains(member.MemberName.ToLower()));
            }
        }
    }
}

Define your class:

public class Employee
{
    public int Id { get; set; }
    public string FirstName { get; set; }
    public string LastName { get; set; }
    public string Email { get; set; }
}

Setup json extensions by calling on JsonSerializerOptions the extension method SetupExtensions defined in the namespace Dahomey.Json:

JsonSerializerOptions options = new JsonSerializerOptions();
options.SetupExtensions();

Register the new object mapping convention for the class:

options.GetObjectMappingConventionRegistry().RegisterConvention(
    typeof(Employee), new SelectiveSerializer("FirstName,Email,Id"));

Then serialize your class with the regular Sytem.Text.Json API:

Employee employee = new Employee
{
    Id = 12,
    FirstName = "John",
    LastName = "Doe",
    Email = "[email protected]"
};
        
string json = JsonSerializer.Serialize(employee, options);
// {"Id":12,"FirstName":"John","Email":"[email protected]"};

Upvotes: 5

Related Questions