1

I have a hierarchy of polymorphic immutable classes that I would like to serialize to and deserialize from JSON using Newtonsoft JSON.

Here's a simplified set of example classes:

[JsonConverter(typeof(ItemJsonConveter))]
public abstract class Item
{
    [JsonConstructor]
    public Item(string itemType, IList<Item> subItems)
    {
        ItemType = itemType;
        SubItems = subItems ?? new List<Item>();
    }

    public string ItemType { get; }

    public IList<Item> SubItems { get; }
}


public class Foo : Item
{
    public const string TypeName = "foo";

    [JsonConstructor]
    public Foo(string fooProperty, IList<Item> subItems = null) : base(TypeName, subItems)
    {
        FooProperty = fooProperty;
    }

    public string FooProperty { get; }
}

public class Bar : Item
{
    public const string TypeName = "bar";

    public Bar(string barProperty, IList<Item> subItems = null) : base(TypeName, subItems)
    {
        BarProperty = barProperty;
    }

    public string BarProperty { get; }
}

I need the resulting JSON follow a specific format which does not identify types by full class names or using a $type parameter as would happen with other values of TypeNameHandling

So I created the following ItemJsonConverter class to read my custom "ItemType" property from the JSON to determine the correct type to deserialize to.

internal class ItemJsonConveter : JsonConverter
{
    public override bool CanWrite => false;

    public override bool CanConvert(Type objectType)
    {
        if (typeof(Item).IsAssignableFrom(objectType)) return true;
        else return false;
    }

    public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
    {
        JObject jo = JObject.Load(reader);
        string typeValue = jo[nameof(Item.ItemType)].Value<string>();

        switch (typeValue)
        {
            case Foo.TypeName:
                return jo.ToObject<Foo>();

            case Bar.TypeName:
                return jo.ToObject<Bar>();

            default:
                throw new InvalidOperationException("Unxpected item type: " + typeValue);
        }
    }

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

Here's the JSON that is created

{
  "FooProperty": "Foo 1",
  "ItemType": "foo",
  "SubItems": [
    {
      "BarProperty": "Bar 2",
      "ItemType": "bar",
      "SubItems": [
        {
          "FooProperty": "Foo 3",
          "ItemType": "foo",
          "SubItems": []
        },
        {
          "BarProperty": "Bar 3",
          "ItemType": "bar",
          "SubItems": []
        }
      ]
    },
    {
      "FooProperty": "Foo 2",
      "ItemType": "foo",
      "SubItems": []
    }
  ]
}

Yet when I run the following code to test this a StackOverFlowException is thrown in ItemJsonConverter.ReadJson()

//Create Model
Foo foo3 = new Foo("Foo 3");
Bar bar3 = new Bar("Bar 3");
Foo foo2 = new Foo("Foo 2");
Bar bar2 = new Bar("Bar 2", new List<Item>() { foo3, bar3 });
Foo foo1 = new Foo("Foo 1", new List<Item>() { bar2, foo2 });

//Create serializer
var ser = new Newtonsoft.Json.JsonSerializer
{
    Formatting = Newtonsoft.Json.Formatting.Indented,
    TypeNameHandling = Newtonsoft.Json.TypeNameHandling.None,
};

//Serialize modelt to string
var tw = new StringWriter();
ser.Serialize(tw, foo1);
var stringvalue = tw.ToString();

//Deserialize
try
{
    using (var sr = new StringReader(stringvalue))
    {
        using (var jr = new JsonTextReader(sr))
        {
            //This throws StackOverflowException ItemJsonConveter.ReadJson()
            var deserialziedFoo = ser.Deserialize<Foo>(jr);
        }
    }
}
catch (System.Exception exp)
{
    throw;
}

I've read other similar questions about the same issue and the solution has been to replace the calls to JObject.ToObject<T>() with explicit construction of new objects and then use JsonConvert.PopulateObject<t>() to update the objects with the values from the JSON.

However, I would prefer these objects to be completely immutable and for there to be no way for them to be changed after being constructed. (Unless this is the only way to get this to work.)

I was able to get this to work by manually reading values from the JObject in ReadJson() and calling Foo and Bar constructors directly. However, for more complex objects I'd really like to take advantage of Newtonsoft's ability to automagically serialize/derserialize through attributes and reflection and not have to update my serialization code every time a class changes.

Here's the complete sample code as a Gist: https://gist.github.com/aireq/51e4527886ddb076ee8c981264b439a7

Vadim Kotov
  • 8,084
  • 8
  • 48
  • 62
Eric Anastas
  • 21,675
  • 38
  • 142
  • 236
  • 1
    Could you print serialized version of ser.Serialize(tw, foo1) to console and see if the JSON is correct? – jnr Feb 05 '19 at 23:09
  • I added the JSON that is generated to the question, which looks correct to me. – Eric Anastas Feb 06 '19 at 00:21
  • The first part of [this answer](https://stackoverflow.com/a/45554062/3744182) to [How to call JsonConvert.DeserializeObject and disable a JsonConverter applied to a base type via `[JsonConverter]`?](https://stackoverflow.com/q/45547123/3744182) should work for you also, which is to apply `[JsonConverter(typeof(NoConverter))]` to the derived type(s). In fact this might be a duplicate; agree? – dbc Feb 06 '19 at 10:02
  • Demo of the solution from [How to call JsonConvert.DeserializeObject and disable a JsonConverter applied to a base type via `[JsonConverter]`?](https://stackoverflow.com/q/45547123/3744182) here: https://dotnetfiddle.net/eKbTyX – dbc Feb 06 '19 at 20:48

0 Answers0