Reputation: 33
I have to change the default json serialization/deserialization of an object following these rules:
I try this:
public class EntityConverter : JsonConverter<EventDefinition>
{
public override EventDefinition Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
EventDefinition result = JsonSerializer.Deserialize<EventDefinition>(ref reader, options);
if (result.EventDefinitionId == 0)
return null;
else return result;
}
public override void Write(Utf8JsonWriter writer, EventDefinition value, JsonSerializerOptions options)
{
if (value == null)
{
value = new EventDefinition();
value.EventDefinitionId = 0;
writer.WriteStringValue(JsonSerializer.Serialize(value));
}
else
writer.WriteStringValue(JsonSerializer.Serialize(value));
}
}
I need to replace writer.WriteStringValue
beacuse it writes the whole object as a string and what I need is to continue with the normal serialization of the object after the modification. How can I achieved this?
Upvotes: 3
Views: 2792
Reputation: 116980
.NET 5 allows custom converters to handle null
if they choose. From How to write custom converters for JSON serialization (marshalling) in .NET: Handle null values:
To enable a custom converter to handle null for a reference or value type, override
JsonConverter<T>.HandleNull
to return true.
Thus in EntityConverter
you need to add
public override bool HandleNull => true;
However, when you do you will encounter a second problem, namely that, in Write()
, you are writing the serialized JSON for EventDefinition
as a double-serialized string value, rather than as an object. How can it be serialized as an object (with null
replaced) instead? If you have applied EntityConverter
as either:
[JsonConverter]
applied to a property.Converters
collection.Then you can enhance the answer from How to use default serialization in a custom System.Text.Json JsonConverter? to include HandleNull
as follows:
public class EntityConverter : DefaultConverterFactory<EventDefinition>
{
protected override bool HandleNull => true;
protected override EventDefinition Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions modifiedOptions, JsonConverter<EventDefinition> defaultConverter)
{
var result = base.Read(ref reader, typeToConvert, modifiedOptions, defaultConverter);
return result?.EventDefinitionId == 0 ? null : result;
}
protected override void Write(Utf8JsonWriter writer, EventDefinition value, JsonSerializerOptions modifiedOptions, JsonConverter<EventDefinition> defaultConverter)
{
value ??= new EventDefinition { EventDefinitionId = 0 };
base.Write(writer, value, modifiedOptions, defaultConverter);
}
}
public abstract class DefaultConverterFactory<T> : JsonConverterFactory
{
class NullHandlingDefaultConverter : DefaultConverter
{
public NullHandlingDefaultConverter(JsonSerializerOptions options, DefaultConverterFactory<T> factory) : base(options, factory) { }
public override bool HandleNull => true;
}
class DefaultConverter : JsonConverter<T>
{
readonly JsonSerializerOptions modifiedOptions;
readonly DefaultConverterFactory<T> factory;
readonly JsonConverter<T> defaultConverter;
public DefaultConverter(JsonSerializerOptions options, DefaultConverterFactory<T> factory)
{
this.factory = factory ?? throw new ArgumentNullException();
this.modifiedOptions = options.CopyAndRemoveConverter(factory.GetType());
this.defaultConverter = (JsonConverter<T>)modifiedOptions.GetConverter(typeof(T));
}
public override void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options) => factory.Write(writer, value, modifiedOptions, defaultConverter);
public override T Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) => factory.Read(ref reader, typeToConvert, modifiedOptions, defaultConverter);
}
protected virtual T Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions modifiedOptions, JsonConverter<T> defaultConverter)
=> defaultConverter.ReadOrSerialize<T>(ref reader, typeToConvert, modifiedOptions);
protected virtual void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions modifiedOptions, JsonConverter<T> defaultConverter)
=> defaultConverter.WriteOrSerialize(writer, value, modifiedOptions);
protected virtual bool HandleNull => false;
public override bool CanConvert(Type typeToConvert) => typeof(T) == typeToConvert;
public override JsonConverter CreateConverter(Type typeToConvert, JsonSerializerOptions options) => HandleNull ? new NullHandlingDefaultConverter(options, this) : 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;
}
public static void WriteOrSerialize<T>(this JsonConverter<T> converter, Utf8JsonWriter writer, T value, JsonSerializerOptions options)
{
if (converter != null)
converter.Write(writer, value, options);
else
JsonSerializer.Serialize(writer, value, options);
}
public static T ReadOrSerialize<T>(this JsonConverter<T> converter, ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
if (converter != null)
return converter.Read(ref reader, typeToConvert, options);
else
return (T)JsonSerializer.Deserialize(ref reader, typeToConvert, options);
}
}
And serialize a List<EventDefinition> events
as follow:
var options = new JsonSerializerOptions
{
Converters = { new EntityConverter() },
WriteIndented = true, // If you want
};
var json = JsonSerializer.Serialize(events, options);
Notes:
In .NET Core 3.x HandleNull
is not available; JsonConverter<T>.Write()
will never be passed a null value in that version. Thus, in that version you would need to adopt a different approach, such as adding a custom converter for the containing type(s) or serializing DTOs instead of "real" objects.
Json.NET also will never call call its JsonConverter.WriteJson()
for a null value so the the two serializers are consistent in that limitation in 3.x, see How to force JsonConverter.WriteJson() to be called for a null value for confirmation. The answer to that question shows a workaround using a custom contract resolver, so reverting to Json.NET might be an option for you in 3.1. System.Text.Json in contrast does not make its contract model public.
If you have applied EntityConverter
directly to EventDefinition
using JsonConverterAttribute
, i.e.:
[JsonConverter(typeof(EntityConverter))]
public class EventDefinition
{
public int EventDefinitionId { get; set; }
}
Then the above approach will not work. In fact there does not appear to be a way to generate a "normal" serialization for an instance of a type to which a converter is applied directly. Instead you would need to manually write and read each required property inside Write()
and Read()
, or write your own reflection code to do so automatically.
Demo fiddle here.
Upvotes: 1