11

I have a class with an interface-typed property like:

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

I also have multiple concrete implementations of the IBar interface that can be set at runtime. Some of these concrete classes require a custom JsonConverter for serialization & deserialization.

Utilizing the TypeNameHandling.Auto option the non-convertor requiring IBar classes can be serialized and deserialized perfectly. The custom-serialized classes on the other hand have no $type name output and while they are serialized as expected, they cannot be deserialized to their concrete type.

I attempted to write-out the $type name metadata myself within the custom JsonConverter; however, on deserialization the converter is then being bypassed entirely.

Is there a workaround or proper way of handling such a situation?

torvin
  • 6,515
  • 1
  • 37
  • 52
Andrew Hanlon
  • 7,271
  • 4
  • 33
  • 53

2 Answers2

5

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

So let's start from models:

public interface IBar { }

public class BarA : IBar  { }

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

Now let's create converter for IBar. It will be used only when deserializing JSON. It will try to read $type variable and call converter for implementing 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 converter for 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's inherited from BarBaseJsonConverter class, not JsonConverter. And also we do not use serializer parameter in WriteJson and ReadJson methods. There is a problem with using serializer parameter inside custom converters. You can read more here. We need to create new instance of JsonSerializer and base class is a good candidate for that:

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 to create 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 still can use your custom converters for both 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);
Community
  • 1
  • 1
Aleksandr Ivanov
  • 2,778
  • 5
  • 27
  • 35
  • Thank you for a great answer! I was working last evening on something similar, but you gave me the extra info I needed to get it working. I took a slightly different approach but one that is completely generic and does not require converters for all inherited types. See my self-answer below. I will mark this as the answer though since it got me on the right track. – Andrew Hanlon Apr 23 '15 at 14:30
  • This approach won't work if the types that are being converted also have references to other types that needs converters. You are essentially deserializing the whole sub-tree with converters disabled. – torvin Nov 22 '17 at 06:34
0

Using information from Alesandr Ivanov's answer above, I created a generic WrappedJsonConverter<T> class that wraps (and unwraps) concrete classes requiring a converter using a $wrappedType metadata property that follows the same type name serialization as the standard $type.

The WrappedJsonConverter<T> is added as a converter to the Interface (ie. IBar), but otherwise this wrapper is completely transparent to classes that do not require a converter and also requires no changes to the wrapped converters.

I used a slightly different hack to get around the converter/serializer looping (static fields), but it does not require any knowledge of the serializer settings being used, and allows for the IBar object graph to have child IBar properties.

For wrapped objects the Json looks like:

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

The full gist 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();
    }
}
Andrew Hanlon
  • 7,271
  • 4
  • 33
  • 53
  • 1
    Use of `static bool _canWrite` and `static bool _canRead` is not thread-safe. Since several frameworks including [tag:asp.net-web-api] share converters across threads, this can cause problems. Instead, use a `[ThreadStatic]` or `ThreadLocal` as suggested in [this answer](https://stackoverflow.com/a/30179162/3744182). – dbc Aug 11 '17 at 18:56
  • @dbc Good call, it wasn't an issue in my usage scenario (desktop), but a valuable default! Thanks. – Andrew Hanlon Aug 11 '17 at 19:35
  • Same problem as with Alesandr Ivanov's answer: it won't work if the types that are being converted also have references to other types that needs converters. You are essentially deserializing the whole sub-tree with converters disabled. – torvin Nov 22 '17 at 06:35
  • @torvin You should test this answer yourself, it does not have that problem. Since it is generic, the static members are not shared with other types. Also I used the `ToObject` method that accepts the serializer. In essence it is transparent to other types, whether they have a converter or not. – Andrew Hanlon Nov 22 '17 at 12:16
  • @AndrewHanlon Sorry, I guess I wasn't clear enough. Here's the [test case](https://gist.github.com/anonymous/208a4ec2727c8033e51660ab86f6998f) where your converter fails. – torvin Nov 23 '17 at 00:40
  • @torvin The piece you are missing is that you still need to have TypeNameHandling turned on within your serialization settings. When you turn it on for both serialization and deserialization (i.e. the json text needs to change) your above test case works perfectly. Cheers! – Andrew Hanlon Nov 23 '17 at 18:16
  • @AndrewHanlon this is really weird. Why do you need `$wrappedType` if you enable `TypeNameHandling`? Just use `$type` in your `WrappedJsonConverter` instead. This will also work only if you have properties declared as interfaces. If you use `object` for example - it breaks again... – torvin Nov 27 '17 at 01:36
  • @torvin At the time of asking, there was a limitation that Json.Net did not place type information on concrete types with custom converters when `TypeNameHandling.Auto` was used on an interface member. I don't know if this is still the case, as I have moved away from using Json.Net. It may very well be fixed in newer versions, and I'll try to run a test and mark this QA as out of date if no longer required. Cheers. – Andrew Hanlon Nov 27 '17 at 17:29