Using custom JsonConverter and TypeNameHandling in Json.net

I have a class with a typed interface:

public class Foo { public IBar Bar { get; set; } } 

I also have some specific IBar interface implementations that can be set at runtime. Some of these specific classes require a custom JsonConverter for serialization and deserialization.

Using the TypeNameHandling.Auto option, a non-converter requiring IBar classes can be serialized and deserialized perfectly. Classes with serialized serialization, on the other hand, have no $type output, and if they are serialized as expected, they cannot be deserialized to their specific type.

I tried writing the metadata of the name $type myself in a custom JsonConverter ; however, upon deserialization, the converter then completely bypasses.

Is there a workaround or the right way to handle this situation?

+7
source share
2 answers

I solved a similar problem and I found a solution. This is not very elegant, and I think there should be a better way, but at least it works. So my idea was to have a JsonConverter for each type that implements IBar and one converter for IBar .

So, let's start with the models:

 public interface IBar { } public class BarA : IBar { } public class Foo { public IBar Bar { get; set; } } 

Now create a converter for IBar . It will only be used when deserializing JSON. It will try to read the $type variable and the call converter to implement the type:

 public class BarConverter : JsonConverter { public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer) { throw new NotSupportedException(); } public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer) { var jObj = JObject.Load(reader); var type = jObj.Value<string>("$type"); if (type == GetTypeString<BarA>()) { return new BarAJsonConverter().ReadJson(reader, objectType, jObj, serializer); } // Other implementations if IBar throw new NotSupportedException(); } public override bool CanConvert(Type objectType) { return objectType == typeof (IBar); } public override bool CanWrite { get { return false; } } private string GetTypeString<T>() { var typeOfT = typeof (T); return string.Format("{0}, {1}", typeOfT.FullName, typeOfT.Assembly.GetName().Name); } } 

And this is the converter for the BarA class:

 public class BarAJsonConverter : BarBaseJsonConverter { public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer) { // '$type' property will be added because used serializer has TypeNameHandling = TypeNameHandling.Objects GetSerializer().Serialize(writer, value); } public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer) { var existingJObj = existingValue as JObject; if (existingJObj != null) { return existingJObj.ToObject<BarA>(GetSerializer()); } throw new NotImplementedException(); } public override bool CanConvert(Type objectType) { return objectType == typeof(BarA); } } 

You may notice that it is inherited from the BarBaseJsonConverter class, not JsonConverter . Also, we do not use the serializer parameter in the WriteJson and ReadJson . There is a problem using the serializer parameter inside custom converters. You can read it here. We need to create a new instance of JsonSerializer , and the base class is a good candidate for this:

 public abstract class BarBaseJsonConverter : JsonConverter { public JsonSerializer GetSerializer() { var serializerSettings = JsonHelper.DefaultSerializerSettings; serializerSettings.TypeNameHandling = TypeNameHandling.Objects; var converters = serializerSettings.Converters != null ? serializerSettings.Converters.ToList() : new List<JsonConverter>(); var thisConverter = converters.FirstOrDefault(x => x.GetType() == GetType()); if (thisConverter != null) { converters.Remove(thisConverter); } serializerSettings.Converters = converters; return JsonSerializer.Create(serializerSettings); } } 

JsonHelper is just a class for creating JsonSerializerSettings :

 public static class JsonHelper { public static JsonSerializerSettings DefaultSerializerSettings { get { return new JsonSerializerSettings { Converters = new JsonConverter[] { new BarConverter(), new BarAJsonConverter() } }; } } } 

Now it will work, and you can still use your custom converters for serialization and deserialization:

 var obj = new Foo { Bar = new BarA() }; var json = JsonConvert.SerializeObject(obj, JsonHelper.DefaultSerializerSettings); var dObj = JsonConvert.DeserializeObject<Foo>(json, JsonHelper.DefaultSerializerSettings); 
+4
source share

Using information from Alexard Ivanov, answer above, I created a generic WrappedJsonConverter<T> class that wraps (and expands) specific classes that require a converter using the $wrappedType metadata property that follows the same type serialization type as the standard $type .

WrappedJsonConverter<T> added as a converter to the interface (i.e., IBar ), but otherwise this shell is completely transparent for classes that do not require a converter, and also do not require changes in convertible converters.

I used a slightly different hack to get around the converter / serializer cycle (static fields), but it does not require any knowledge about serializer parameters, and allows the graph of IBar objects to have a child IBar property.

For wrapped objects, Json looks like this:

 "IBarProperty" : { "$wrappedType" : "Namespace.ConcreteBar, Namespace", "$wrappedValue" : { "ConvertedID" : 90, "ConvertedPropID" : 70 ... } } 

Full text can be found here.

 public class WrappedJsonConverter<T> : JsonConverter<T> where T : class { [ThreadStatic] private static bool _canWrite = true; [ThreadStatic] private static bool _canRead = true; public override bool CanWrite { get { if (_canWrite) return true; _canWrite = true; return false; } } public override bool CanRead { get { if (_canRead) return true; _canRead = true; return false; } } public override T ReadJson(JsonReader reader, T existingValue, JsonSerializer serializer) { var jsonObject = JObject.Load(reader); JToken token; T value; if (!jsonObject.TryGetValue("$wrappedType", out token)) { //The static _canRead is a terrible hack to get around the serialization loop... _canRead = false; value = jsonObject.ToObject<T>(serializer); _canRead = true; return value; } var typeName = jsonObject.GetValue("$wrappedType").Value<string>(); var type = JsonExtensions.GetTypeFromJsonTypeName(typeName, serializer.Binder); var converter = serializer.Converters.FirstOrDefault(c => c.CanConvert(type) && c.CanRead); var wrappedObjectReader = jsonObject.GetValue("$wrappedValue").CreateReader(); wrappedObjectReader.Read(); if (converter == null) { _canRead = false; value = (T)serializer.Deserialize(wrappedObjectReader, type); _canRead = true; } else { value = (T)converter.ReadJson(wrappedObjectReader, type, existingValue, serializer); } return value; } public override void WriteJson(JsonWriter writer, T value, JsonSerializer serializer) { var type = value.GetType(); var converter = serializer.Converters.FirstOrDefault(c => c.CanConvert(type) && c.CanWrite); if (converter == null) { //This is a terrible hack to get around the serialization loop... _canWrite = false; serializer.Serialize(writer, value, type); _canWrite = true; return; } writer.WriteStartObject(); { writer.WritePropertyName("$wrappedType"); writer.WriteValue(type.GetJsonSimpleTypeName()); writer.WritePropertyName("$wrappedValue"); converter.WriteJson(writer, value, serializer); } writer.WriteEndObject(); } } 
0
source share

All Articles