2

I have a Cabin class that contains a list of Row objects. I'd like to serialize the objects like this, but when deserializing I'd like the Row objects to be RowRule objects, which inherit from the Row object. Below is some sample code I've been trying.

class Program
{
    static void Main(string[] args)
    {
        var cabin = new Cabin();
        var row = new Row();
        row.Status = "Success";
        cabin.Rows = new List<Row>()
        {
            row,
            row
        };

        JsonSerializerSettings settings = new JsonSerializerSettings()
        {
            TypeNameHandling = TypeNameHandling.Auto
        };
        string json = JsonConvert.SerializeObject(cabin, Formatting.Indented, settings);
        Console.WriteLine(json);

        Cabin obj = JsonConvert.DeserializeObject<Cabin>(json,
            new JsonSerializerSettings() {TypeNameHandling = TypeNameHandling.Auto});
        Console.WriteLine(obj);
        Debug.Assert(obj.Rows.First().GetType().Name == "RowRule");
    }
}

class Cabin
{
    public IList<Row> Rows { get; set; } 
}

class Row
{
    public string Status { get; set; }
}

class RowRule : Row
{

}
jficker
  • 175
  • 6
  • Why are you using `TypeNameHandling.Auto`? Is there a possibility of other subclasses of `Row` being in the list, and you only want to convert from `Row` to `RowRule` when the type is not otherwise specified? – dbc Apr 12 '16 at 23:34
  • Related: [Using custom JsonConverter and TypeNameHandling in Json.net](https://stackoverflow.com/q/29810004/3744182). – dbc Aug 11 '17 at 19:17

2 Answers2

4

The simple answer is to use a CustomCreationConverter<Row> and return a RowRule from Create():

class RowToRoleRuleConverter : CustomCreationConverter<Row>
{
    public override Row Create(Type objectType)
    {
        if (objectType.IsAssignableFrom(typeof(RowRule)))
            return Activator.CreateInstance<RowRule>();
        return (Row)Activator.CreateInstance(objectType);
    }
}

However, you are using TypeNameHandling.Auto which implies that there may be polymorphic "$type" properties in your JSON. Unfortunately, CustomCreationConverter<T> ignores these properties. Thus it will be necessary to do some additional work and create DowncastingConverter<TBase, TDerived>:

public class DowncastingConverter<TBase, TDerived> : PolymorphicCreationConverter<TBase> where TDerived : TBase
{
    protected override TBase Create(Type objectType, Type polymorphicType, object existingValue, IContractResolver contractResolver, JObject obj)
    {
        Type createType = objectType;
        if (createType.IsAssignableFrom(polymorphicType))
            createType = polymorphicType;
        if (createType.IsAssignableFrom(typeof(TDerived)))
            createType = typeof(TDerived);

        if (existingValue != null && createType.IsAssignableFrom(existingValue.GetType()))
            return (TBase)existingValue;

        var contract = contractResolver.ResolveContract(createType);
        return (TBase)contract.DefaultCreator();
    }
}

public abstract class PolymorphicCreationConverter<T> : JsonConverter
{
    public override bool CanConvert(Type objectType)
    {
        return typeof(T).IsAssignableFrom(objectType);
    }

    public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
    {
        throw new NotSupportedException("CustomCreationConverter should only be used while deserializing.");
    }

    public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
    {
        if (reader.TokenType == JsonToken.Null)
            return null;
        var obj = JObject.Load(reader);
        Type polymorphicType = null;

        var polymorphicTypeString = (string)obj["$type"];
        if (polymorphicTypeString != null)
        {
            if (serializer.TypeNameHandling != TypeNameHandling.None)
            {
                string typeName, assemblyName;
                ReflectionUtils.SplitFullyQualifiedTypeName(polymorphicTypeString, out typeName, out assemblyName);
                polymorphicType = serializer.Binder.BindToType(assemblyName, typeName);
            }
            obj.Remove("$type");
        }

        var value = Create(objectType, polymorphicType, existingValue, serializer.ContractResolver, obj);
        if (value == null)
            throw new JsonSerializationException("No object created.");

        using (var subReader = obj.CreateReader())
            serializer.Populate(subReader, value);
        return value;
    }

    protected abstract T Create(Type objectType, Type polymorphicType, object existingValue, IContractResolver iContractResolver, JObject obj);

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

internal static class ReflectionUtils
{
    // Utilities taken from https://github.com/JamesNK/Newtonsoft.Json/blob/master/Src/Newtonsoft.Json/Utilities/ReflectionUtils.cs
    // I couldn't find a way to access these directly.

    public static void SplitFullyQualifiedTypeName(string fullyQualifiedTypeName, out string typeName, out string assemblyName)
    {
        int? assemblyDelimiterIndex = GetAssemblyDelimiterIndex(fullyQualifiedTypeName);

        if (assemblyDelimiterIndex != null)
        {
            typeName = fullyQualifiedTypeName.Substring(0, assemblyDelimiterIndex.GetValueOrDefault()).Trim();
            assemblyName = fullyQualifiedTypeName.Substring(assemblyDelimiterIndex.GetValueOrDefault() + 1, fullyQualifiedTypeName.Length - assemblyDelimiterIndex.GetValueOrDefault() - 1).Trim();
        }
        else
        {
            typeName = fullyQualifiedTypeName;
            assemblyName = null;
        }
    }

    private static int? GetAssemblyDelimiterIndex(string fullyQualifiedTypeName)
    {
        int scope = 0;
        for (int i = 0; i < fullyQualifiedTypeName.Length; i++)
        {
            char current = fullyQualifiedTypeName[i];
            switch (current)
            {
                case '[':
                    scope++;
                    break;
                case ']':
                    scope--;
                    break;
                case ',':
                    if (scope == 0)
                    {
                        return i;
                    }
                    break;
            }
        }

        return null;
    }
}

Then use it like:

JsonSerializerSettings readSettings = new JsonSerializerSettings()
{
    TypeNameHandling = TypeNameHandling.Auto,
    Converters = new[] { new DowncastingConverter<Row, RowRule>() },
};
Cabin obj = JsonConvert.DeserializeObject<Cabin>(json, readSettings);

Prototype fiddle.

Finally, when using TypeNameHandling, do take note of this caution from the Newtonsoft docs:

TypeNameHandling should be used with caution when your application deserializes JSON from an external source. Incoming types should be validated with a custom SerializationBinder when deserializing with a value other than None.

For a discussion of why this may be necessary, see TypeNameHandling caution in Newtonsoft Json.

dbc
  • 104,963
  • 20
  • 228
  • 340
0

The problem with your sample code is, you're creating object of Row and trying to get RowRule which is not possible.

May be you wanted to do it like this:

        var cabin = new Cabin();
        var row = new RowRule(); // create derived object
        row.Status = "Success";
        cabin.Rows = new List<Row>()
        {
          row,
          row
        };
vendettamit
  • 14,315
  • 2
  • 32
  • 54
  • It is possible, as long as generated Json for both cases is identical. Newtonsoft does not inlcude type information in json by default. – Dan Apr 12 '16 at 21:28
  • With `TypeNameHandling = TypeNameHandling.Auto` it does include the type name. see http://www.newtonsoft.com/json/help/html/T_Newtonsoft_Json_TypeNameHandling.htm – vendettamit Apr 12 '16 at 21:33
  • But you can edit the json string to remove it, can't you? – Dan Apr 21 '16 at 20:00