Json.Net Serialization of Type with Polymorphic Child Object

14,675

Solution 1

Having the subtype information in the container class is problematic for two reasons:

  1. The container class instance is not accessible when Json.NET is reading the contained class.
  2. If you later need to convert the SubTypeClassBase property into, say, a list, there will be nowhere to put the subtype information.

Instead, I would recommend adding the subtype information as a property in the base class:

[JsonConverter(typeof(SubTypeClassConverter))]
public class SubTypeClassBase
{
    [JsonConverter(typeof(StringEnumConverter))] // Serialize enums by name rather than numerical value
    public SubType Type { get { return typeToSubType[GetType()]; } }
}

Now the custom subtype enum will be serialized whenever an object assignable to SubTypeClassBase is serialized. Having done that, for deserialization you can create a JsonConverter that loads the json for a given SubTypeClassBase into a temporary JObject, checks the value of the "Type" property, and deserializes the JSON object as the appropriate class.

Prototype implementation below:

public enum SubType
{
    BaseType,
    Type1,
    Type2,
}

[JsonConverter(typeof(SubTypeClassConverter))]
public class SubTypeClassBase
{
    static readonly Dictionary<Type, SubType> typeToSubType;
    static readonly Dictionary<SubType, Type> subTypeToType;

    static SubTypeClassBase()
    {
        typeToSubType = new Dictionary<Type,SubType>()
        {
            { typeof(SubTypeClassBase), SubType.BaseType },
            { typeof(SubTypeClass1), SubType.Type1 },
            { typeof(SubTypeClass2), SubType.Type2 },
        };
        subTypeToType = typeToSubType.ToDictionary(pair => pair.Value, pair => pair.Key);
    }

    public static Type GetType(SubType subType)
    {
        return subTypeToType[subType];
    }

    [JsonConverter(typeof(StringEnumConverter))] // Serialize enums by name rather than numerical value
    public SubType Type { get { return typeToSubType[GetType()]; } }
}

public class SubTypeClass1 : SubTypeClassBase
{
    public string AaaField { get; set; }
}

public class SubTypeClass2 : SubTypeClassBase
{
    public string ZzzField { get; set; }
}

public class SubTypeClassConverter : JsonConverter
{
    public override bool CanConvert(Type objectType)
    {
        return objectType == typeof(SubTypeClassBase);
    }

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

    public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
    {
        var token = JToken.Load(reader);
        var typeToken = token["Type"];
        if (typeToken == null)
            throw new InvalidOperationException("invalid object");
        var actualType = SubTypeClassBase.GetType(typeToken.ToObject<SubType>(serializer));
        if (existingValue == null || existingValue.GetType() != actualType)
        {
            var contract = serializer.ContractResolver.ResolveContract(actualType);
            existingValue = contract.DefaultCreator();
        }
        using (var subReader = token.CreateReader())
        {
            // Using "populate" avoids infinite recursion.
            serializer.Populate(subReader, existingValue);
        }
        return existingValue;
    }

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

Solution 2

You can try with JsonSubtypes converter implementation that support registering type mapping with enum values.

In your case it looks like this:

        public class MainClass
        {
            public SubTypeClassBase SubTypeData { get; set; }
        }

        [JsonConverter(typeof(JsonSubtypes), "SubTypeType")]
        [JsonSubtypes.KnownSubType(typeof(SubTypeClass1), SubType.WithAaaField)]
        [JsonSubtypes.KnownSubType(typeof(SubTypeClass2), SubType.WithZzzField)]
        public class SubTypeClassBase
        {
            public SubType SubTypeType { get; set; }
        }

        public class SubTypeClass1 : SubTypeClassBase
        {
            public string AaaField { get; set; }
        }

        public class SubTypeClass2 : SubTypeClassBase
        {
            public string ZzzField { get; set; }
        }

        public enum SubType
        {
            WithAaaField,
            WithZzzField
        }

        [TestMethod]
        public void Deserialize()
        {
            var obj = JsonConvert.DeserializeObject<MainClass>("{\"SubTypeData\":{\"ZzzField\":\"zzz\",\"SubTypeType\":1}}");
            Assert.AreEqual("zzz", (obj.SubTypeData as SubTypeClass2)?.ZzzField);
        }

Solution 3

Here's a full example that can both read and write JSON with polymorphic objects.

Assuming we have the following class structure:

public class Base {}
public class SubClass1 : Base {
    public int field1;
}
public class SubClass2 : Base {
    public int field2;
}

We can use a custom converter that creates an extra field in JSON named type when serializing and reads it when deserializing.

public class PolymorphicJsonConverter : JsonConverter
{
    public override object ReadJson (JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer) {
        JObject item = JObject.Load(reader);
        var type = item["type"].Value<string>();

        if (type == "SubClass1") {
            return item.ToObject<SubClass1>();
        } else if (type == "SubClass2") {
            return item.ToObject<SubClass2>();
        } else {
            return null;
        }
    }

    public override void WriteJson (JsonWriter writer, object value, JsonSerializer serializer) {
        JObject o = JObject.FromObject(value);
        if (value is SubClass1) {
            o.AddFirst(new JProperty("type", new JValue("SubClass1")));
        } else if (value is SubClass1) {
            o.AddFirst(new JProperty("type", new JValue("SubClass2")));
        }

        o.WriteTo(writer);
    }

    public override bool CanConvert (Type objectType) {
        return typeof(Base).IsAssignableFrom(objectType);
    }
}

You could use this converter in a container class like so:

public class Container {
    public List<Base> items;

    public string Save() {
        return JsonConvert.SerializeObject(items, new PolymorphicJsonConverter())
    }

    public void Load(string jsonText) {
        items = JsonConvert.DeserializeObject<List<Base>>(jsonText, new PolymorphicJsonConverter());
    }
}

Alternatively you could use JSON.net's built in type hinting, instead of rolling your own JsonConverter, but it's not so flexible and creates very un-portable JSON.

JsonConvert.SerializeObject(items, new JsonSerializerSettings {
    TypeNameHandling = TypeNameHandling.Auto
});
Share:
14,675

Related videos on Youtube

GaTechThomas
Author by

GaTechThomas

Updated on September 15, 2022

Comments

  • GaTechThomas
    GaTechThomas over 1 year

    We would like to be able to serialize/deserialize json from/to C# classes, with the main class having an instance of a polymorphic child object. Doing so is easy using Json.Net's TypeNameHandling.Auto setting. However, we would like to do so without the "$type" field.

    The first thought is to be able to rename "$type" to a value of our choosing and to have the value for the type be an enum that would map the sub-types properly. I have not seen that as being an option but would be glad to hear if it is possible.

    The second thought was along the following lines... Below is a first pass at classes, with the top level class having an indicator (SubTypeType) as to what type of data is contained in the child object (SubTypeData). I've dug around a bit into the Json.Net documentation and have tried a few things but have had no luck.

    We currently have full control over the data definition, but once it is deployed, then things are locked.

    public class MainClass
    {
      public SubType          SubTypeType { get; set; }
      public SubTypeClassBase SubTypeData { get; set; }
    }
    
    public class SubTypeClassBase
    {
    }
    
    public class SubTypeClass1 : SubTypeClassBase
    {
      public string AaaField { get; set; }
    }
    
    public class SubTypeClass2 : SubTypeClassBase
    {
      public string ZzzField { get; set; }
    }
    
  • GaTechThomas
    GaTechThomas about 9 years
    That is exactly what I was trying to do!
  • Joseph
    Joseph over 6 years
    Thank you! That infinite recursion part was killing me!
  • Raz Friman
    Raz Friman about 5 years
    I adapted my solution from this and it worked. Nice write up!
  • r2d2
    r2d2 about 4 years
    There exist a NuGet nuget.org/packages/JsonSubTypes of it. It is possible to make the configuration only in the JsonSerializerSettings without touching the model classes. Great!