7

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!

Erik Philips
  • 53,428
  • 11
  • 128
  • 150
Martin Vrábel
  • 830
  • 1
  • 13
  • 36
  • 1
    you could deserialize into a class that matches the json structure, and use that class to figure out which type of custom class you want the data to end up in. Or use dynamic. Chicken bacon ranch. – Steve's a D Jul 13 '16 at 17:19
  • Also, your `type` property is `open`. Your enum value for `open` is 0. Strings to ints don't convert very well. If you deserialized into a class that matches the json structure, then convert to your end-game class, this conversion could also be where you convert from string to int (Enum). – Steve's a D Jul 13 '16 at 17:23
  • 1
    Also, you named your enum the same as a popular system class: https://msdn.microsoft.com/en-us/library/system.type(v=vs.110).aspx Will probably be confusing to some who maintain the system. – Steve's a D Jul 13 '16 at 17:26
  • @SteveG There is no problem with deserializing the `type` property as I have written custom converter for that too and it works well. What do you mean by using `dynamic`? That I should serialize JSON into `dynamic` and manually parse JSON into my `Action` class? – Martin Vrábel Jul 13 '16 at 17:46
  • And the naming is on purpose. Only from inside that class it can be confusing as there is used `Type` name itself. But outside it has to be used as `Action.Type` and `Action.ActionType` seems a little bit weird to me :) – Martin Vrábel Jul 13 '16 at 17:49
  • http://www.newtonsoft.com/json/help/html/QueryJsonDynamic.htm , http://stackoverflow.com/questions/3142495/deserialize-json-into-c-sharp-dynamic-object – Steve's a D Jul 13 '16 at 18:01
  • basically, deserialize it twice, once dynamically to get the type, and another into your specific class. Could also use some string parsing so you don't have to deserialize twice, but in all honesty, in my opinion, it would be more readable if you mimicked the json structure when you deserialize, then convert that to your specific class – Steve's a D Jul 13 '16 at 18:03
  • When you do var type = Action.Type, then Type escapes outside of this class. Also, when you add a using for the enum....`if(Action.Type == Type.Open)`, your `using`s could get weird looking if somebody using this class also wants to use the System.Type class – Steve's a D Jul 13 '16 at 18:05

2 Answers2

9

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) },
            // Add additional dictionary entries corresponding to each different subtype of ActionResult
        };
        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];
    }

    // Add enum values for Type corresponding to each different subtype of ActionResult
    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.

Demo fiddle here: https://dotnetfiddle.net/2I2oVP

dbc
  • 104,963
  • 20
  • 228
  • 340
  • Do you propose generating different types for each enum as Open => OpenActionResult and place them in the typeToSystemType dictionary? Also, where/How do you use OpenActionResult in this solution? Is OpenActionResult going to be a type in the Action class as well as the other XXXActionResults? – yyardim Apr 20 '23 at 23:11
  • 1
    @yyardim - I answered this almost 7 years ago so I don't exactly recall. I found my old code and made a fiddle here: https://dotnetfiddle.net/2I2oVP. `OpenActionResult` was a subtype of `ActionResult`, presumably there were more corresponding to result values not shown in the question. For each subtype of `ActionResult` there would need to be an enum value of `Type`, and an entry in the dictionary. – dbc Apr 24 '23 at 15:10
3

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 :)

Tomas Petovsky
  • 285
  • 4
  • 14