I have a custom attribute [Foo]
implemented as follows:
public class FooAttribute
: Attribute
{
}
Now I want to use the System.Text.Json.JsonSerializer
to step into each field that has that attribute, in order to manipulate how is serialized and deserialized.
For example, if I have the following class
class SampleInt
{
[Foo]
public int Number { get; init; }
public int StandardNumber { get; init; }
public string Text { get; init; }
}
when I serialize an instance of this class, I want a custom int JsonConverter
to apply only for that field.
public class IntJsonConverter
: JsonConverter<int>
{
public override int Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
// do whatever before reading if the text starts with "potato". But this should be triggered only if destination type has the Foo attribute. How?
return reader.GetInt32();
}
public override void Write(Utf8JsonWriter writer, int value, JsonSerializerOptions options)
{
writer.WriteStringValue("potato" + value.ToString());
}
}
so that the serialization for
var sample =
new SampleInt
{
Number = 123,
StandardNumber = 456
Text = "bar"
};
like this
var serializeOptions = new JsonSerializerOptions();
var serializeOptions.Converters.Add(new IntJsonConverter());
var resultJson = JsonSerializer.Serialize(sample, serializeOptions);
results on the following json
{
"number": "potato123",
"standardNumber": 456,
"text": "bar"
}
and not in
{
"number": "potato123",
"standardNumber": "potato456",
"text": "bar"
}
In a similar manner, I want the deserialization to be conditional, and only use the custom converter if the destination field has the [Foo]
attribute.
With Newtonsoft, this is possible using Contract Resolvers and overriding CreateProperties
method like this.
public class SerializationContractResolver
: DefaultContractResolver
{
private readonly ICryptoTransform _encryptor;
private readonly FieldEncryptionDecryption _fieldEncryptionDecryption;
public SerializationContractResolver(
ICryptoTransform encryptor,
FieldEncryptionDecryption fieldEncryptionDecryption)
{
_encryptor = encryptor;
_fieldEncryptionDecryption = fieldEncryptionDecryption;
NamingStrategy = new CamelCaseNamingStrategy();
}
protected override IList<JsonProperty> CreateProperties(Type type, MemberSerialization memberSerialization)
{
var properties = base.CreateProperties(type, memberSerialization);
foreach (var jsonProperty in properties)
{
var hasAttribute = HasAttribute(type, jsonProperty);
if (hasAttribute)
{
var serializationJsonConverter = new MyJsonConverter();
jsonProperty.Converter = serializationJsonConverter;
}
}
return properties;
}
private bool HasAttribute(Type type, JsonProperty jsonProperty)
{
var propertyInfo = type.GetProperty(jsonProperty.UnderlyingName);
if (propertyInfo is null)
{
return false;
}
var hasAttribute =
propertyInfo.CustomAttributes
.Any(x => x.AttributeType == typeof(FooAttribute));
var propertyType = propertyInfo.PropertyType;
var isSimpleValue = propertyType.IsValueType || propertyType == typeof(string);
var isSupportedField = isSimpleValue && hasPersonalDataAttribute;
return isSupportedField;
}
}
But I don't want to use Newtonsoft. I want to use the new dotnet System.Text.Json
serializer. Is it possible to use it in a similar granular way?
As of .NET 7 and later you can use a typeInfo
modifier to customize your type's serialization contract and apply a converter to properties or fields marked with some attribute.
To do this, first define the following generic modifier that sets JsonPropertyInfo.CustomConverter
:
public static partial class JsonExtensions
{
public static Action<JsonTypeInfo> WithPropertyConverterFor<TAttribute>(JsonConverter? converter, bool checkCanConvert = false)
where TAttribute : System.Attribute => typeInfo =>
{
if (typeInfo.Kind != JsonTypeInfoKind.Object)
return;
foreach (var property in typeInfo.Properties)
if (property.AttributeProvider?.GetCustomAttributes(typeof(TAttribute), true).Any() == true
&& (!checkCanConvert || converter == null || converter.CanConvert(property.PropertyType)))
property.CustomConverter = converter;
};
}
Then, when setting up your JsonSerializerOptions
, add the modifier as follows:
JsonSerializerOptions options = new()
{ // Add whatever standard options you need here, e.g.
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
WriteIndented = true,
};
options.TypeInfoResolver =
(options.TypeInfoResolver ?? new DefaultJsonTypeInfoResolver())
.WithAddedModifier(JsonExtensions.WithPropertyConverterFor<FooAttribute>(
new IntJsonConverter()));
Then when you serialize a SampleInt
e.g. as follows:
var sample = new SampleInt { Number = 123, StandardNumber = 456, Text = "bar" };
var json = JsonSerializer.Serialize(sample, options);
The [Foo]
properties will be serialized using the converter:
{
"number": "potato123",
"standardNumber": 456,
"text": "bar"
}
Notes:
Pass null
for converter
to remove an existing converter.
Setting a converter for which CanConvert
returns false
for the property's declared type will cause an exception to be thrown, so if [Foo]
might be applied to properties of type other than int
, pass checkCanConvert : true
to the modifier.
In practice you might want to do this if you have multiple converters for different property types. E.g. if you also have LongJsonConverter
you could do:
options.TypeInfoResolver =
(options.TypeInfoResolver ?? new DefaultJsonTypeInfoResolver())
.WithAddedModifier(JsonExtensions.WithPropertyConverterFor<FooAttribute>(
new IntJsonConverter(), checkCanConvert : true))
.WithAddedModifier(JsonExtensions.WithPropertyConverterFor<FooAttribute>(
new LongJsonConverter(), checkCanConvert : true));
If have properties of many different types marked with [Foo]
, you could use the factory pattern e.g. like so:
public class FooDataConverterFactory : JsonConverterFactory
{
static readonly Dictionary<Type, JsonConverter> wellKnownConverters = new ()
{
[typeof(int)] = new IntJsonConverter(),
[typeof(long)] = new LongJsonConverter(),
[typeof(string)] = new StringJsonConverter(),
// Add others as required or use Type.MakeGenericType() to manufacture them.
};
public override bool CanConvert(Type typeToConvert) => wellKnownConverters.ContainsKey(typeToConvert);
public override JsonConverter CreateConverter(Type typeToConvert, JsonSerializerOptions options) => wellKnownConverters[typeToConvert];
};
options.TypeInfoResolver =
(options.TypeInfoResolver ?? new DefaultJsonTypeInfoResolver())
.WithAddedModifier(JsonExtensions.WithPropertyConverterFor<FooAttribute>(
new FooDataConverterFactory()));
This wasn't relevant back when the question was asked, but if you are now using source generation, then as of .NET 9, it should be populating JsonPropertyInfo.AttributeProvider
(see this issue for confirmation) so the above code should work with native aot in .NET 9 and later. Neverless I haven't tested.
Demo .NET 8 fiddle here.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With