Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Deserialize object based on value type in property

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)

like image 629
David Perfors Avatar asked Nov 16 '16 11:11

David Perfors


1 Answers

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.");
    }
}
like image 194
David Perfors Avatar answered Oct 09 '22 05:10

David Perfors