3

I find myself doing this a lot. I have a class that looks something like this:

public class Foo
{
    public SomeEnum SomeValue { get; set; }
    public SomeAbstractBaseClass SomeObject { get; set; }
}

And what I need to do is deserialize a specfic class derived from SomeAbstractBaseClass based on the value in SomeValue. So what I do is put a JsonConverterAttribute on the whole class and then write a custom converter derived from JsonConverter that will in its ReadJson, first examine SomeValue and then have some logic to deserialize SomeObject to a specific class. This works, but it's kind of annoying. The only part that really needs special handling is the SomeObject property, but I have to put the converter at the higher level of the class and have my converter be responsible for populating all the other members of Foo (i.e. SomeValue, but you could imagine if you had lots of other properties that were fine with the default deserialization behavior). This could be avoided if there was only some way to get access to the parent object (or at least some property or properties from it) in the ReadJson method of JsonConverter. But there doesn't seem to be any way to do that. So if I could do something like:

public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
{
    var parent = //...somehow access the parent or at least SomeValue
    switch (parent.SomeValue)
    {
        case Value1:
        serialized.Deserialize<SpecificType1>(reader);   
        break;
        //... other cases
    }
}

There is the very suggestively named existingValue parameter, but it always seems to be null? Is there a better way to do this?

Matt Burland
  • 44,552
  • 18
  • 99
  • 171
  • 1
    I feel as though `SomeEnum SomeValue { get; set; }` ought to be a property of `SomeAbstractBaseClass` not the parent class. If it were, you could follow the pattern in [Deserializing polymorphic json classes without type information using json.net](http://stackoverflow.com/questions/19307752/deserializing-polymorphic-json-classes-without-type-information-using-json-net). – dbc Aug 10 '15 at 20:08
  • 2
    I also note that, according to the [JSON specification](http://json.org/), that a JSON object is "an unordered set of name/value pairs", so you can't necessarily assume `SomeValue` will be encountered before `SomeObject`. So there's no 100% reliable way to use `existingValue` even if you allocate a default `SomeObject` when `SomeValue` is read. – dbc Aug 10 '15 at 20:14
  • 1
    @dbc: That's actually a very good point. What I usually end up doing (at the higher level of `Foo`) is something like `var obj = JObject.Load(reader)` to read the whole object in and then I can access it in whatever order I want. But without that I can't assume that `SomeValue` would have been loaded yet. That's probably means it's impossible for a good reason. – Matt Burland Aug 10 '15 at 20:36

1 Answers1

1

According to the JSON specification, a JSON object is "an unordered set of name/value pairs", so trying to access the parent's SomeValue enum while reading an instance of SomeAbstractBaseClass isn't guaranteed to work -- as it might not have been read yet.

So, I'd first like to suggest a couple of alternative designs. Since Json.NET is basically a contract serializer, it will be easier to use if the polymorphic object itself conveys its type information, rather than parent container objects. Thus you could either:

  1. Move the polymorphic type enum into SomeAbstractBaseClass along the lines of Json.Net Serialization of Type with Polymorphic Child Object.

  2. Use Json.NET's built-in support for polymorphic types by setting JsonSerializerSettings.TypeNameHandling to TypeNameHandling.Auto.

That being said, you can reduce your pain somewhat by, inside a JsonConverter, reading the JSON for your container class Foo into a JObject, splitting out the polymorphic properties for custom handling, and using JsonSerializer.Populate to fill in the remaining properties. You can even standardize this pattern by creating an abstract converter that does this for you, using a custom attribute to determine which properties to split out:

[System.AttributeUsage(System.AttributeTargets.Property | System.AttributeTargets.Field, AllowMultiple = false)]
public sealed class JsonCustomReadAttribute : Attribute
{
}

public abstract class JsonCustomReadConverter : JsonConverter
{
    public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
    {
        if (reader.TokenType == JsonToken.Null)
            return null;
        var contract = serializer.ContractResolver.ResolveContract(objectType) as JsonObjectContract;
        if (contract == null)
            throw new JsonSerializationException("invalid type " + objectType.FullName);
        var value = existingValue ?? contract.DefaultCreator();
        var jObj = JObject.Load(reader);

        // Split out the properties requiring custom handling
        var extracted = contract.Properties
            .Where(p => p.AttributeProvider.GetAttributes(typeof(JsonCustomReadAttribute), true).Count > 0)
            .Select(p => jObj.ExtractProperty(p.PropertyName))
            .Where(t => t != null)
            .ToList();

        // Populare the properties not requiring custom handling.
        using (var subReader = jObj.CreateReader())
            serializer.Populate(subReader, value);

        ReadCustom(value, new JObject(extracted), serializer);

        return value;
    }

    protected abstract void ReadCustom(object value, JObject jObject, JsonSerializer serializer);

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

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

public static class JsonExtensions
{
    public static JProperty ExtractProperty(this JObject obj, string name)
    {
        if (obj == null)
            throw new ArgumentNullException();
        var property = obj.Property(name);
        if (property == null)
            return null;
        property.Remove();
        return property;
    }
}

And then use it like:

public abstract class SomeAbstractBaseClass
{
}

public class Class1 : SomeAbstractBaseClass
{
    public string Value1 { get; set; }
}

public class Class2 : SomeAbstractBaseClass
{
    public string Value2 { get; set; }
}

public static class SomeAbstractBaseClassSerializationHelper
{
    public static SomeEnum SerializedType(this SomeAbstractBaseClass baseObject)
    {
        if (baseObject == null)
            return SomeEnum.None;
        if (baseObject.GetType() == typeof(Class1))
            return SomeEnum.Class1;
        if (baseObject.GetType() == typeof(Class2))
            return SomeEnum.Class2;
        throw new InvalidDataException();
    }

    public static SomeAbstractBaseClass DeserializeMember(JObject jObject, string objectName, string enumName, JsonSerializer serializer)
    {
        var someObject = jObject[objectName];
        if (someObject == null || someObject.Type == JTokenType.Null)
            return null;
        var someValue = jObject[enumName];
        if (someValue == null || someValue.Type == JTokenType.Null)
            throw new JsonSerializationException("no type information");
        switch (someValue.ToObject<SomeEnum>(serializer))
        {
            case SomeEnum.Class1:
                return someObject.ToObject<Class1>(serializer);
            case SomeEnum.Class2:
                return someObject.ToObject<Class2>(serializer);
            default:
                throw new JsonSerializationException("unexpected type information");
        }
    }
}

public enum SomeEnum
{
    None,
    Class1,
    Class2,
}

[JsonConverter(typeof(FooConverter))]
public class Foo
{
    [JsonCustomRead]
    public SomeEnum SomeValue { get { return SomeObject.SerializedType(); } }

    [JsonCustomRead]
    public SomeAbstractBaseClass SomeObject { get; set; }

    public string SomethingElse { get; set; }
}

public class FooConverter : JsonCustomReadConverter
{
    protected override void ReadCustom(object value, JObject jObject, JsonSerializer serializer)
    {
        var foo = (Foo)value;
        foo.SomeObject = SomeAbstractBaseClassSerializationHelper.DeserializeMember(jObject, "SomeObject", "SomeValue", serializer);
    }

    public override bool CanConvert(Type objectType)
    {
        return typeof(Foo).IsAssignableFrom(objectType);
    }
}
Community
  • 1
  • 1
dbc
  • 104,963
  • 20
  • 228
  • 340
  • Wondering if anything has changed over the last 8 years since your answer that could simplify this further? – alhazen Aug 07 '23 at 17:35
  • @alhazen - Not really no. In fact the alternative [`TypeNameHandling` setting](https://www.newtonsoft.com/json/help/html/serializetypenamehandling.htm) has been found to be insecure in certain cases, see [TypeNameHandling caution in Newtonsoft Json](https://stackoverflow.com/q/39565954/3744182). – dbc Aug 07 '23 at 18:03