Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to conditionally deserialize JSON object based on another JSON property?

Tags:

json

c#

json.net

Suppose I have following model class:

public class Action
{
    public enum Type
    {
        Open,
        Close,
        Remove,
        Delete,
        Reverse,
        Alert,
        ScaleInOut,
        Nothing
    }

    [JsonProperty("id")]
    public string Id { get; set; }

    [JsonProperty("name")]
    public string Name { get; set; }

    [JsonProperty("active")]
    [JsonConverter(typeof(IntToBoolConverter))]
    public bool Active { get; set; }

    [JsonProperty("type")]
    [JsonConverter(typeof(ActionTypeConverter))]
    public Type ActionType { get; set; }

    [JsonProperty("result")]
    [JsonConverter(typeof(ActionResultConverter))]
    public ActionResult Result { get; set; }
}

and I want to deserialize following JSON into that class:

{
    "name":"test1",
    "id":"aa0832f0508bb580ce7f0506132c1c13",
    "active":"1",
    "type":"open",
    "result":{
        "property1":"buy",
        "property2":"123.123",
        "property3":"2016-07-16T23:00:00",
        "property4":"768",
        "property5":true
     }
}

Result object can be different each time (one of 6 models) and its type depends on JSON property type.

I have created custom ActionResultConverter (JsonConverter annotation above Result property of Action class) that should be able to create specific result object based on string in type property of JSON.

My problem is that I don't know how to access that property from converter because only the result part of whole JSON is passed to JsonReader.

Any ideas or help will be appreciated.

Thanks!

like image 963
Martin Vrábel Avatar asked Jul 13 '16 17:07

Martin Vrábel


2 Answers

Json.NET does not provide a method to access the value of a property of a parent object in the JSON hierarchy while deserializing a child object. Likely this is because a JSON object is defined to be an unordered set of name/value pairs, according to the standard, so there can be no guarantee the desired parent property occurs before the child in the JSON stream.

Thus, rather than handling the Type property in a converter for ActionResult, you'll need to do it in a converter for Action itself:

[JsonConverter(typeof(ActionConverter))]
public class Action
{
    readonly static Dictionary<Type, System.Type> typeToSystemType;
    readonly static Dictionary<System.Type, Type> systemTypeToType;

    static Action()
    {
        typeToSystemType = new Dictionary<Type, System.Type>
        {
            { Type.Open, typeof(OpenActionResult) },
        };
        systemTypeToType = typeToSystemType.ToDictionary(p => p.Value, p => p.Key);
    }

    public static Type SystemTypeToType(System.Type systemType)
    {
        return systemTypeToType[systemType];
    }

    public static System.Type TypeToSystemType(Type type)
    {
        return typeToSystemType[type];
    }

    public enum Type
    {
        Open,
        Close,
        Remove,
        Delete,
        Reverse,
        Alert,
        ScaleInOut,
        Nothing
    }

    [JsonProperty("id")]
    public string Id { get; set; }

    [JsonProperty("name")]
    public string Name { get; set; }

    [JsonProperty("active")]
    [JsonConverter(typeof(IntToBoolConverter))]
    public bool Active { get; set; }

    [JsonProperty("type")]
    [JsonConverter(typeof(ActionTypeConverter))]
    public Type ActionType { get; set; }

    [JsonProperty("result")]
    public ActionResult Result { get; set; }
}

class ActionConverter : JsonConverter
{
    public override bool CanConvert(Type objectType)
    {
        throw new NotImplementedException();
    }

    public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
    {
        if (reader.TokenType == JsonToken.Null)
            return null;
        var obj = JObject.Load(reader);
        var contract = (JsonObjectContract)serializer.ContractResolver.ResolveContract(objectType);
        var action = existingValue as Action ?? (Action)contract.DefaultCreator();

        // Remove the Result property for manual deserialization
        var result = obj.GetValue("Result", StringComparison.OrdinalIgnoreCase).RemoveFromLowestPossibleParent();

        // Populate the remaining properties.
        using (var subReader = obj.CreateReader())
        {
            serializer.Populate(subReader, action);
        }

        // Process the Result property
        if (result != null)
            action.Result = (ActionResult)result.ToObject(Action.TypeToSystemType(action.ActionType));

        return action;
    }

    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 JToken RemoveFromLowestPossibleParent(this JToken node)
    {
        if (node == null)
            return null;
        var contained = node.AncestorsAndSelf().Where(t => t.Parent is JContainer && t.Parent.Type != JTokenType.Property).FirstOrDefault();
        if (contained != null)
            contained.Remove();
        // Also detach the node from its immediate containing property -- Remove() does not do this even though it seems like it should
        if (node.Parent is JProperty)
            ((JProperty)node.Parent).Value = null;
        return node;
    }
}

Notice the use of JsonSerializer.Populate() inside ReadJson(). This automatically fills in all properties of Action other than Result, avoiding the need for manual deserialization of each.

like image 200
dbc Avatar answered Oct 26 '22 22:10

dbc


Inspired by http://json.codeplex.com/discussions/56031:

public sealed class ActionModelConverter : JsonConverter
{
    public override bool CanConvert(Type objectType)
    {
        return typeof(ActionModel).IsAssignableFrom(objectType);
    }

    public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
    {
        JObject jObject = JObject.Load(reader);
        ActionModel actionModel = new ActionModel();

        // TODO: Manually populate properties
        actionModel.Id = (string)jObject["id"].ToObject<string>();

        var type = (ActionModel.Type)jObject["type"].ToObject<ActionModel.Type>();
        switch (type)
        {
          case ActionModel.Type.Open:
            var actionResult = jObject["result"].ToObject<ActionOpenResult>(jsonSerializer);

          default:
            throw new JsonSerializationException($"Unsupported action type: '{type}'");
        }

        actionModel.Result = actionResult;

        return actionModel;
    }

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

Coded in editor, so sorry for typos :)

like image 38
Tomas Petovsky Avatar answered Oct 27 '22 00:10

Tomas Petovsky