Reputation: 2143
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
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 usingJsonTypeInfo<T>
, which in previous versions served as an opaque token used exclusively in source generator APIs. Starting in .NET 7, most facets of theJsonTypeInfo
contract metadata have been exposed and made user-modifiable. Contract customization allows users to write their own JSON contract resolution logic using implementations of theIJsonTypeInfoResolver
interface:public interface IJsonTypeInfoResolver { JsonTypeInfo? GetTypeInfo(Type type, JsonSerializerOptions options); }
A contract resolver returns a configured
JsonTypeInfo
instance for the givenType
andJsonSerializerOptions
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 implementsIJsonTypeInfoResolver
.Starting from .NET 7 the
JsonSerializerContext
class used in source generation also implementsIJsonTypeInfoResolver
.
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:
JsonPropertyInfo.ShouldSerialize
(also new in .NET 7) can be used for conditional serialization of properties.
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.
Upvotes: 8
Reputation: 636
The equivalent types in System.Text.Json --
JsonClassInfo
andJsonPropertyInfo
-- 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