5

TL;DR: Is there an easy way in json.net to inspect the type of a property and create an instance based on that?

I have the following two objects in JSON, these are relation objects in the JSON API:

{ "data": { "type": "Test", "id": "1" } }

and

{ "data": [{ "type": "Test", "id": "1" }, { "type": "Test", "id": "2" }]}

Using json.net, I want to serialize these objects to either the class ToOneRelation, or ToManyRelation. The classes look like this:

[JsonConverter(typeof(RelationshipConverter))]
abstract class Relation
{
    [JsonProperty("meta")]
    public Dictionary<string, object> Meta { get; set; }
}
class ToOneRelation : Relation
{
    [JsonProperty("data")]
    public object Data { get; set; }
}
class ToManyRelation : Relation
{
    [JsonProperty("data")]
    public List<object> Data { get; set; }
}

Generally I only know that I want to have a Relation, so I would do the following:

var result = JsonConvert.DeserializeObject<Relation>(json);

In order to instantiate the correct object, I need a custom converter, where I check the type of the "data" property. When the type is an array, I use the ToManyRelation, otherwise I use the ToOneRelation.

Currently I created a custom converter that will walk over all properties and deserialize them.

object toOneData = null;
List<object> toManyRelationData = null;

Dictionary<string, object> meta = null;

reader.Read();
while (reader.TokenType == JsonToken.PropertyName)
{
    string propertyName = reader.Value.ToString();
    reader.Read();
    if (string.Equals(propertyName, "data", StringComparison.OrdinalIgnoreCase))
    {
        if (reader.TokenType == JsonToken.StartObject)
        {
            toOneData = serializer.Deserialize<object>(reader);
        }
        else if (reader.TokenType == JsonToken.StartArray)
        {
            toManyRelationData = serializer.Deserialize<List<object>>(reader);
        }
    }
    else if (string.Equals(propertyName, "meta", StringComparison.OrdinalIgnoreCase))
    {
        meta = serializer.Deserialize<Dictionary<string, object>>(reader);
    }
    else
    {
        reader.Skip();
    }
    reader.Read();
}

Although this works, I have the problem that this code isn't really maintainable. As soon as an extra property is introduced to the Relation object (or the other objects) I need to modify this code to reflect those changes.

So my question is is, is there a better way to inspect a property to identify which type to create and than use serializer.Populate method to do the rest? (The CustomCreationConverter doesn't work, because it can't inspect the properties before creating the object is created...)

(Note, a working example can be found here)

David Perfors
  • 1,273
  • 13
  • 15
  • Make your class `Relation` have a `List Data` in all cases, and use `SingleOrArrayConverter` from [How to handle both a single item and an array for the same property using JSON.net](https://stackoverflow.com/a/18997172/3744182). – dbc Nov 17 '16 at 09:44
  • thanks for the suggestion, when it is just the Data property that is different in both cases, it would be a good solution, but what if either the ToOneRelation or the ToManyRelation gets more properties that the other doesn't have? – David Perfors Nov 17 '16 at 10:17
  • Then you want either [Deserializing polymorphic json classes without type information using json.net](https://stackoverflow.com/questions/19307752/deserializing-polymorphic-json-classes-without-type-information-using-json-net) or [How to implement custom JsonConverter in JSON.NET to deserialize a List of base class objects?](https://stackoverflow.com/questions/8030538). – dbc Nov 17 '16 at 10:27
  • Actually this solution doesn't really work. When using a List for both ToOne and ToMany, it is more difficult to serialize an empty relationship correctly. the ToOne should be serialized as `null`, but the ToMany should be serialized as `[]`. This adds complexity to the users of the Data property. – David Perfors Nov 17 '16 at 10:27
  • Possible duplicate of [How to implement custom JsonConverter in JSON.NET to deserialize a List of base class objects?](https://stackoverflow.com/questions/8030538/how-to-implement-custom-jsonconverter-in-json-net-to-deserialize-a-list-of-base) – Heretic Monkey Jul 17 '19 at 14:41

1 Answers1

4

Based on this answer I came up with the following solution:

public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
{
    var json = JToken.Load(reader);
    object relationship = CreateObject(json);
    if (relationship != null)
    {
        serializer.Populate(json.CreateReader(), relationship);
    }
    return relationship;
}
private object CreateObject(JToken token)
{
    if (token.Type == JTokenType.Null)
    {
        return null;
    }
    if (token["data"] == null)
    {
        return new ToOneRelation();
    }

    switch (token["data"].Type)
    {
        case JTokenType.Null:
        case JTokenType.Object:
            return new ToOneRelation();
        case JTokenType.Array:
            return new ToManyRelation();
        default:
            throw new Exception("Incorrect json.");
    }
}
David Perfors
  • 1,273
  • 13
  • 15