How to use default serialization in a custom System.Text.Json JsonConverter?
As explained in the docs, converters are chosen with the following precedence:
[JsonConverter]
applied to a property.- A converter added to the
Converters
collection.[JsonConverter]
applied to a custom value type or POCO.
Each case needs to be dealt with separately.
If you have
[JsonConverter]
applied to a property., then simply callingJsonSerializer.Serialize(writer, person, options);
will generate a default serialization.If you have A converter added to the
Converters
collection., then inside theWrite()
(orRead()
) method, you can copy the incomingoptions
using theJsonSerializerOptions
copy constructor, remove the converter from the copy'sConverters
list, and pass the modified copy intoJsonSerializer.Serialize<T>(Utf8JsonWriter, T, JsonSerializerOptions);
This can't be done as easily in .NET Core 3.x because the copy constructor does not exist in that version. Temporarily modifying the
Converters
collection of the incoming options to remove the converter would not be not thread safe and so is not recommended. Instead one would need create new options and manually copy each property as well as theConverters
collection, skipping converts of typeconverterType
.Do note that this will cause problems with serialization of recursive types such as trees, because nested objects of the same type will not be serialized initially using the converter.
If you have
[JsonConverter]
applied to a custom value type or POCO. there does not appear to be a way to generate a default serialization.
Since, in the question, the converter is added to the Converters
list, the following modified version correctly generates a default serialization:
public sealed class PersonConverter : DefaultConverterFactory<Person>
{
record PersonDTO(string FirstName, string LastName, string Name); // A DTO with both the old and new properties.
protected override Person Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions modifiedOptions)
{
var dto = JsonSerializer.Deserialize<PersonDTO>(ref reader, modifiedOptions);
var oldNames = dto?.Name?.Split(' ', StringSplitOptions.RemoveEmptyEntries) ?? Enumerable.Empty<string>();
return new Person(dto.FirstName ?? oldNames.FirstOrDefault(), dto.LastName ?? oldNames.LastOrDefault());
}
}
public abstract class DefaultConverterFactory<T> : JsonConverterFactory
{
class DefaultConverter : JsonConverter<T>
{
readonly JsonSerializerOptions modifiedOptions;
readonly DefaultConverterFactory<T> factory;
public DefaultConverter(JsonSerializerOptions options, DefaultConverterFactory<T> factory)
{
this.factory = factory;
this.modifiedOptions = options.CopyAndRemoveConverter(factory.GetType());
}
public override void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options) => factory.Write(writer, value, modifiedOptions);
public override T Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) => factory.Read(ref reader, typeToConvert, modifiedOptions);
}
protected virtual T Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions modifiedOptions)
=> (T)JsonSerializer.Deserialize(ref reader, typeToConvert, modifiedOptions);
protected virtual void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions modifiedOptions)
=> JsonSerializer.Serialize(writer, value, modifiedOptions);
public override bool CanConvert(Type typeToConvert) => typeof(T) == typeToConvert;
public override JsonConverter CreateConverter(Type typeToConvert, JsonSerializerOptions options) => new DefaultConverter(options, this);
}
public static class JsonSerializerExtensions
{
public static JsonSerializerOptions CopyAndRemoveConverter(this JsonSerializerOptions options, Type converterType)
{
var copy = new JsonSerializerOptions(options);
for (var i = copy.Converters.Count - 1; i >= 0; i--)
if (copy.Converters[i].GetType() == converterType)
copy.Converters.RemoveAt(i);
return copy;
}
}
Notes:
I used a converter factory rather than a converter as the base class for
PersonConverter
because it allowed me to conveniently cache the copied options inside the manufactured converter.If you try to apply a
DefaultConverterFactory<T>
to a custom value type or POCO, e.g.[JsonConverter(typeof(PersonConverter))] public record Person(string FirstName, string LastName);
A nasty stack overflow will occur.
Demo fiddle here.
System.Text.Json how to get converter to utilise default Write() behaviour
I have worked out the answer. :-)
When I request a instance of a Customer
to be serialized...
string json = JsonSerializer.Serialize(customer);
... as the serializer hits the Addresses
sub-class within Customer
, it calls the Write()
method of my custom converter, passing to it the current Json writer instance, the Addresses object instance to be serialized, and any converter options.
Quite simply, since I have deserialized the original quirky Json correctly in the Read()
method of my customer converter and created an appropriate List<>, the Write()
can simply serialize the Addresses
instance using the Addresses class. The code looks like this...
public override void Write(Utf8JsonWriter writer, Addresses value, JsonSerializerOptions options)
{
JsonSerializer.Serialize(writer, value);
}
system text json use standard deserialization
If you'd like to change the property names only, use [JsonPropertyName("Name")]
.
https://learn.microsoft.com/en-us/dotnet/standard/serialization/system-text-json-customize-properties
Custom converter for System.Text.Json when working with generics
Like Newtonsoft.Json, it appears you can recursively call JsonSerializer.Deserialize
and pass the ref Utf8JsonReader
along with the custom serializer as parameters.
Here is the code from within the custom serializer:
if (reader.TokenType == JsonTokenType.StartObject)
{
ret.Right = JsonSerializer.Deserialize<Expression>(ref reader, new JsonSerializerOptions
{
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull,
Converters = { new JsonStringEnumConverter(), new ExpressionConverter() },
});
}
The deserializer will pick up after where the recursion left off. Worked like a charm.
Related Topics
Does Garbage Collection Run During Debug
Practical Applications of Bitwise Operations
Post Build Event Execute Powershell
When Is Finally Run If You Throw an Exception from the Catch Block
How to Define a Method in Razor
Controlling Execution Order of Unit Tests in Visual Studio
Unload a Dll Loaded Using Dllimport
ASP.NET Core Long Running/Background Task
Converting String Expression to Integer Value Using C#
Spawn Multiple Threads for Work Then Wait Until All Finished
Getting Hash of a List of Strings Regardless of Order
Winforms: Application.Exit VS Environment.Exit VS Form.Close
Send Http Post Message in ASP.NET Core Using Httpclient Postasjsonasync