JSON.Net Custom Serialization with JSONconverter - How to Get the "Default" Behavior

Json.NET custom serialization with JsonConverter - how to get the default behavior

One easy way to do it is to allocate an instance of your class then use JsonSerializer.Populate(JsonReader, Object). This is the way it is done in the standard CustomCreationConverter<T>:

public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
{
if (reader.Value != null && reader.ValueType == typeof(string))
{
return someSpecialDataTypeInstance;
}
else if (reader.TokenType == JsonToken.StartObject)
{
existingValue = existingValue ?? serializer.ContractResolver.ResolveContract(objectType).DefaultCreator();
serializer.Populate(reader, existingValue);
return existingValue;
}
else if (reader.TokenType == JsonToken.Null)
{
return null;
}
else
{
throw new JsonSerializationException();
}
}

Limitations:

  • This does not handle situations when TypeNameHandling has been enabled and a "$type" property is present specifying a polymorphic subtype.

    In this case you'll need to do some of the tricks use by JsonDerivedTypeConverer<T> in JsonConverter with Interface.

  • The type to be deserialized must have a parameterless constructor accessible to Json.NET. If not, and existingValue is null, it will be necessary to construct it manually, via new DataType(arg1, arg2, ...).

  • Reference preservation via PreserveReferencesHandling is not supported.

    For one approach to handle this situation see How can I choose what type to deserialize at runtime based on the structure of the json?.

Sample fiddle.

How to use default serialization in a custom JsonConverter

In your custom JsonConverter, override CanWrite and return false:

public override bool CanWrite { get { return false; } }

public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
{
throw new NotImplementedException();
}

Then you can just throw an exception from WriteJson, since it won't get called.

(Similarly, to get default behavior during deserialization, override CanRead and return false.)

Note that the same approach can be used for JsonConverter<T> (introduced in Json.NET 11.0.1) since it is just a subclass of JsonConverter that introduces type-safe versions of ReadJson() and WriteJson().

How to get ReadJson to return default behavior - as if CanConvert returned false

From your question and comments, it sounds like you have some situations where you want a converter to read but not write, and others where you want it to write but not read. You've solved the problem by splitting the functionality into two converters and then having each converter's CanConvert method return true or false at the appropriate times. This is certainly a viable approach and seems to be working for you, which is great. However, I wanted to offer an alternative solution.

In addition to the CanConvert method, the base JsonConverter offers two virtual boolean properties which you can override: CanRead and CanWrite. (Both return true by default.) These properties directly control whether ReadJson and WriteJson are called by the serializer for a particular converter. So, for example, if CanRead returns false, then ReadJson will not be called and the default read behavior will be used instead, even though CanConvert returned true. This allows you to set up an asymmetric converter quite neatly. For example, you might have a situation where you want to deserialize a crazy JSON format into a more sane object structure, but when you serialize it again, you don't want to go back to the crazy JSON format-- you just want the default serialization. In that case you would override CanWrite in your converter to always return false. Then you could just leave the implementation of WriteJson blank or have it throw a NotImplementedException; it will never be called.

Your case sounds a little more complicated than that, but you still should be able to manipulate the CanRead and CanWrite properties to achieve your desired results. Below is a contrived example which shows how we can switch the ReadJson and WriteJson methods on and off depending on a situational variable.

public class Program
{
public static void Main(string[] args)
{
string json = @"{""keys"":[""foo"",""fizz""],""values"":[""bar"",""bang""]}";

CustomConverter converter = new CustomConverter();
JsonSerializerSettings settings = new JsonSerializerSettings();
settings.Converters.Add(converter);

// Here we are reading a JSON object containing two arrays into a dictionary
// (custom read) and then writing out the dictionary JSON (standard write)
Console.WriteLine("--- Situation 1 (custom read, standard write) ---");
converter.Behavior = ConverterBehavior.CustomReadStandardWrite;
json = DeserializeThenSerialize(json, settings);

// Here we are reading a simple JSON object into a dictionary (standard read)
// and then writing out a new JSON object containing arrays (custom write)
Console.WriteLine("--- Situation 2 (standard read, custom write) ---");
converter.Behavior = ConverterBehavior.StandardReadCustomWrite;
json = DeserializeThenSerialize(json, settings);
}

private static string DeserializeThenSerialize(string json, JsonSerializerSettings settings)
{
Console.WriteLine("Deserializing...");
Console.WriteLine(json);
var dict = JsonConvert.DeserializeObject<Dictionary<string, string>>(json, settings);
foreach (var kvp in dict)
{
Console.WriteLine(kvp.Key + ": " + kvp.Value);
}

Console.WriteLine("Serializing...");
json = JsonConvert.SerializeObject(dict, settings);
Console.WriteLine(json);
Console.WriteLine();

return json;
}
}

enum ConverterBehavior { CustomReadStandardWrite, StandardReadCustomWrite }

class CustomConverter : JsonConverter
{
public ConverterBehavior Behavior { get; set; }

public override bool CanConvert(Type objectType)
{
return typeof(IDictionary<string, string>).IsAssignableFrom(objectType);
}

public override bool CanRead
{
get { return Behavior == ConverterBehavior.CustomReadStandardWrite; }
}

public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
{
Console.WriteLine("ReadJson was called");

// Converts a JSON object containing a keys array and a values array
// into a Dictionary<string, string>
JObject jo = JObject.Load(reader);
return jo["keys"].Zip(jo["values"], (k, v) => new JProperty((string)k, v))
.ToDictionary(jp => jp.Name, jp => (string)jp.Value);
}

public override bool CanWrite
{
get { return Behavior == ConverterBehavior.StandardReadCustomWrite; }
}

public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
{
Console.WriteLine("WriteJson was called");

// Converts a dictionary to a JSON object containing
// a keys array and a values array from the dictionary
var dict = (Dictionary<string, string>)value;
JObject jo = new JObject(new JProperty("keys", new JArray(dict.Keys)),
new JProperty("values", new JArray(dict.Values)));
jo.WriteTo(writer);
}
}

Output:

--- Situation 1 (custom read, standard write) ---
Deserializing...
{"keys":["foo","fizz"],"values":["bar","bang"]}
ReadJson was called
foo: bar
fizz: bang
Serializing...
{"foo":"bar","fizz":"bang"}

--- Situation 2 (standard read, custom write) ---
Deserializing...
{"foo":"bar","fizz":"bang"}
foo: bar
fizz: bang
Serializing...
WriteJson was called
{"keys":["foo","fizz"],"values":["bar","bang"]}

Fiddle: https://dotnetfiddle.net/BdtSoN

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.

  1. If you have [JsonConverter] applied to a property., then simply calling JsonSerializer.Serialize(writer, person, options); will generate a default serialization.

  2. If you have A converter added to the Converters collection., then inside the Write() (or Read()) method, you can copy the incoming options using the JsonSerializerOptions copy constructor, remove the converter from the copy's Converters list, and pass the modified copy into JsonSerializer.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 the Converters collection, skipping converts of type converterType.

    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.

  3. 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);
}

Call default JsonSerializer in a JsonConverter for certain value type arrays

Firstly, as an aside, I note that Json.NET supports deserializing both an array of integers and a Base64 string to a byte [] array natively. I.e. the following unit test assertions both just work:

Assert.IsTrue(JsonConvert.DeserializeObject<byte []>("[1, 2]")
.SequenceEqual(new [] { (byte)1, (byte)2 }));
Assert.IsTrue(JsonConvert.DeserializeObject<byte []>("\"AQI=\"")
.SequenceEqual(new [] { (byte)1, (byte)2 }));

Demo fiddle #1 here.

That being said, there are a few options given in JSON.Net throws StackOverflowException when using [JsonConvert()] as well as this answer to Newtonsoft Json.NET JsonConverter attribute preserve references issue when deserializing for recursively calling the serializer to get a "default" deserialization:

  1. If you don't need to pre-load the JSON into a JToken hierarchy, you can have the converter disable itself using a thread static member, and then call serializer.Deserialize() recursively.

  2. If you do need to pre-load the JSON into a JToken hierarchy, you can embed the hierarchy inside a parent container and supersede and disable the converter using a dummy converter on the container's member.

An example converter using option #1 might look as follows:

public sealed class ByteConverter : JsonConverter<byte[]>
{
[ThreadStatic]
static bool disabled;

// Disables the converter in a thread-safe manner.
bool Disabled { get { return disabled; } set { disabled = value; } }

public override bool CanRead { get { return !Disabled; } }

public override byte[] ReadJson(JsonReader reader, Type objectType, byte[] existingValue, bool hasExistingValue, JsonSerializer serializer)
{
switch (reader.MoveToContentAndAssert().TokenType) // Skip past comments
{
case JsonToken.Null:
return null;

case JsonToken.StartArray:
// Your custom logic here, e.g.:
return serializer.Deserialize<List<byte>>(reader).ToArray();

default:
using (new PushValue<bool>(true, () => Disabled, val => Disabled = val))
return serializer.Deserialize<byte []>(reader);
}
}

// Remainder omitted
public override bool CanWrite => false;

public override void WriteJson(JsonWriter writer, byte[] value, JsonSerializer serializer) => throw new NotImplementedException();
}

public struct PushValue<T> : IDisposable
{
Action<T> setValue;
T oldValue;

public PushValue(T value, Func<T> getValue, Action<T> setValue)
{
if (getValue == null || setValue == null)
throw new ArgumentNullException();
this.setValue = setValue;
this.oldValue = getValue();
setValue(value);
}

// By using a disposable struct we avoid the overhead of allocating and freeing an instance of a finalizable class.
public void Dispose()
{
if (setValue != null)
setValue(oldValue);
}
}

public static partial class JsonExtensions
{
public static JsonReader MoveToContentAndAssert(this JsonReader reader)
{
if (reader == null)
throw new ArgumentNullException();
if (reader.TokenType == JsonToken.None) // Skip past beginning of stream.
reader.ReadAndAssert();
while (reader.TokenType == JsonToken.Comment) // Skip past comments.
reader.ReadAndAssert();
return reader;
}

public static JsonReader ReadAndAssert(this JsonReader reader)
{
if (reader == null)
throw new ArgumentNullException();
if (!reader.Read())
throw new JsonReaderException("Unexpected end of JSON stream.");
return reader;
}
}

Demo fiddle #2 here.

However, in your case things are simpler. Json.NET considers a byte [] array represented as a Base64 string to be a primitive, so you can simply load it into a JToken and use the JToken Explicit Conversion (JToken toByte[]) operator to cast it to a byte[] array like so:

public class ByteConverter : JsonConverter<byte[]>
{
public override byte[] ReadJson(JsonReader reader, Type objectType, byte[] existingValue, bool hasExistingValue, JsonSerializer serializer)
{
switch (reader.MoveToContentAndAssert().TokenType) // Skip past comments
{
case JsonToken.Null:
return null;

case JsonToken.StartArray:
// Your custom logic here, e.g.:
return serializer.Deserialize<List<byte>>(reader).ToArray();

default:
return (byte[])JToken.Load(reader);
}
}

// Remainder omitted

This entirely avoids use of the serializer. Demo fiddle #3 here.



Related Topics



Leave a reply



Submit