简体   繁体   中英

Remove nesting/arrays when deserializing JSON using JSON.Net

I'm running into some issues trying to deserialize some JSON from an API I'm working with. For some reason the API likes to wrap the data in extra layers and arrays when it isn't necessary.

{
    "CustomData": [
        {
            "Wrapper": [
                {
                    "OptionalDataSet1": [
                        {
                            "ItemA": "Basic string"
                        },
                        {
                            "ItemB": "Another basic string"
                        }
                    ]
                }
            ]
        }
    ]
}

Using json2csharp I've gotten classes that work for the above example.

public class OptionalDataSet1
{
    public string ItemA { get; set; }
    public string ItemB { get; set; }
}

public class Wrapper
{
    public List<OptionalDataSet1> OptionalDataSet1 { get; set; }
}

public class CustomData
{
    public List<Wrapper> Wrapper { get; set; }
}

public class RootObject
{
    public List<CustomData> CustomData { get; set; }
}

The issue I'm having is the "Wrapper" class is unneeded according to the API. It's always there but will be the only item in Custom Data. Furthermore, there will only ever be a single instance of "OptionalDataSet1" inside of the Wrapper. There are other "OptionalDataSets", but again, they will be unique per request.

Finally, the Wrapper deserializes TWO objects for "OptionalDataSet1", the first with ItemA's value, the second with ItemBs. There are other data sets that can have upwards of forty items available to them, I don't want to scan through forty instances of an object to find which one has the single data attribute I'm trying to find.

Should I massage the JSON string I'm receiving from the API before sending it off to be deserialized by removing the "Wrapper" and converting the List<> properties to singular instances, or is there another method I'm missing using JSON.Net to produce something like

RootObject.CustomData.OptionalDataSet1.ItemB

Instead of

RootObject.CustomData[0].Wrapper[0].OptionalDataSet[1].ItemB

It sounds like you're really only interested in the key-value pairs inside the innermost array ( OptionalDataSet1 ) in the JSON, and those keys can vary depending on the data set you're requesting from the API. In that case, I would make a helper method using the LINQ-to-JSON API to parse the JSON and return the desired data in a Dictionary<string, string> . Then you do not need to worry about defining different classes for all the different data sets.

public static Dictionary<string, string> Deserialize(string json)
{
    return JObject.Parse(json)
        .SelectToken("CustomData[0].Wrapper[0].OptionalDataSet1")
        .Children<JObject>()
        .SelectMany(jo => jo.Properties())
        .ToDictionary(jp => jp.Name, jp => (string)jp.Value);
}

Then you can deserialize like this:

Dictionary<string, string> optionalDataSet1 = Deserialize(json);

From there you can easily access any item you're interested in directly:

string itemA = optionalDataSet1["ItemA"];

Or you can dump out all the key-value pairs like this:

foreach (var kvp in optionalDataSet1)
{
    Console.WriteLine(kvp.Key + ": " + kvp.Value);
}

Fiddle: https://dotnetfiddle.net/6ekOFp

Well, though Im very distracted by the fact that you so sure that API will always return 1 instance in arrays, I will answer like it should be done instead of hacking over json.

The main reason is to abstract your implementation from actual data objects (DTO). So for your generated classes - just leave them like this. It is transfer protocol for your API and NOT your business logic scope, so you better not touch it in any way. Just trust me, every change in protocol will touch your business part - it is not good, it will lead to unneccesary developer time consumption every time someone wants to change API on front-end.

It is just generated proxy classes, it can be easily regenerated, you must not change them. API can be changed and your code should be intact.

Instead of this hacks just map them in whatever your heart desire structures:

public class MyApiResponse
{
     public string ItemA {get;set;}
     public string ItemB {get;set;}
}

var n = new MyApiResponse
{
    ItemA = ...,
    ItemB = ...
}

Well, the solution ended up being to write my own JsonConverter as mentioned by dbc

public override object ReadJson(JsonReader reader, Type objectType, 
                                object existingValue, JsonSerializer serializer)
{
    JObject jo = JObject.Load(reader);
    object targetObj = Activator.CreateInstance(objectType);

    foreach (PropertyInfo prop in objectType.GetProperties()) {
        string jsonPath = prop.Name;
        JToken token = jo.SelectToken(jsonPath);

        if (jsonPath.Equals("OptionalDataSet1")) {
            var innerItems = jo.SelectToken("Wrapper[0]").SelectToken(jsonPath).Values();
            var finalItem = new JObject();
            foreach (var item in innerItems) {
                var tempItem = new JObject(item);
                finalItem.Merge(tempItem);
            }
        } else {
            token = jo.SelectToken(jsonPath).First();
        }

        if (token != null && token.Type != JTokenType.Null) {
            object value = token.ToObject(prop.PropertyType, serializer);
            prop.SetValue(targetObj, value, null);
        }
    }

    return targetObj;
}

With this custom converter, I was able to remove the need for the Wrapper class, and have single instances of OptionalDataSet1 and CustomData, and the single instance of OptionalDataSet1 has all of it's info correctly populated.

(code might be off, converted from VB.Net on the fly for this answer)

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM