How to convert JSON to list string, when some of the JSON objects are string and some are array?

15,060

Solution 1

You have a lot of problems going on here. Let me start with the reason you are getting this error: your JSON contains a single outer object, but you are trying to deserialize it into a list. This won't work. If there's just a single object in the JSON, you need to deserialize into a single object.

Second issue, the data for your StaffDetail class is not at the top level of your JSON; it is one level down-- in the value of the d property of the outer JSON object. To fix this, you need to introduce a wrapper class and deserialize into that. Then you can retrieve the StaffDetail from the wrapper.

Third issue, it looks like you are trying to flatten the Columns and Set data from the JSON into List<string> properties in your class. It looks like you have correctly realized that you need a converter to do this; however, your converter doesn't handle the JSON data correctly-- it is assuming it is going to get just a simple array of strings or a simple string. Neither Columns nor Set is structured in this way in the JSON, and furthermore, the data is structured differently for both of them. The former is an object containing properties whose values are arrays of integers. The latter is an array of arrays of strings. Since they are different structures, they need to be handled differently. I would suggest using two different converters in this case.

Fourthly, while you correctly decorate your StaffDetail class with [JsonConverter] attributes to indicate which properties should use your converter, you incorrectly also add the converter to the serializer settings. The problem with this is that your converter says in CanConvert that it can handle any string or any list of strings. If you apply the converter in the settings that means that Json.Net will try to use your converter for any property anywhere that is either a string or a list of strings. Clearly, you do not want this-- your converter is really intended just to handle one specific case.

Fifth, it looks like you have also decorated your Rows property with a [JsonConverter] attribute, but the JSON shows an empty object. Will this field have any data that you care about? If not, just declare it as object; if you do care, please show an example of what might be in there. Or if you know that it will be structured the same as either Columns or Set, then you can keep it as List<string> and reuse one of those converters.

There are also some other minor issues in your question such as your JSON being invalid due to an extra comma (already pointed out by @frno), and the fact that your call to JsonConvert.DeserializeObject() mentions a class called AlertDetails but the class you show is actually named StaffDetail. But we'll chalk those up to simple copy-paste mistakes.

Whew!

OK, so how do we fix all this?

Let start with your classes. I mentioned that you need a wrapper class since your data is actually one level down in the JSON; here's what that would look like:

class Wrapper
{
    public StaffDetail d { get; set; }
}

For your StaffDetail class, I've changed the Columns and Set properties to use different converters, since the JSON is different for each. I'll define those converters next. I also changed the type of Rows to object and removed the [JsonConverter] attribute for now, since it's not clear from the question how that field should be handled. If the data will be structured like Columns or Set then you can change it back and use the appropriate converter, as I mentioned.

class StaffDetail
{
    public string __type { get; set; }
    public string SessionID { get; set; }
    public string Code { get; set; }
    public string Message { get; set; }
    public object Rows { get; set; }

    [JsonConverter(typeof(ColumnsConverter))]
    public List<string> Columns { get; set; }

    [JsonConverter(typeof(SetConverter))]
    public List<string> Set { get; set; }
}

Here is the converter which will handle the Columns data. This converter will take a JSON object and extract the property names into a list of strings.

class ColumnsConverter : JsonConverter
{
    public override bool CanConvert(Type objectType)
    {
        // this converter can handle converting some JSON to a List<string>
        return objectType == typeof(List<string>);
    }

    public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
    {
        // Convert an object to a flat list of strings containing
        // just the property names from the object.
        JObject obj = JObject.Load(reader);
        return obj.Properties().Select(p => p.Name).ToList();
    }

    public override bool CanWrite 
    { 
        get { return false; }
    }

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

Here is the converter which will handle the Set data. This converter will take an array of arrays of strings and convert it into a flat list of strings.

class SetConverter : JsonConverter
{
    public override bool CanConvert(Type objectType)
    {
        // this converter can handle converting some JSON to a List<string>
        return objectType == typeof(List<string>);
    }

    public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
    {
        // Convert an array of arrays of strings to a flat list of strings
        JArray array = JArray.Load(reader);
        return array.Children<JArray>()
            .SelectMany(ja => ja.Children(), (ja, jt) => jt.Value<string>()).ToList();
    }

    public override bool CanWrite
    {
        get { return false; }
    }

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

To deserialize, you can call JsonConvert.DeserializeObject() like this. Notice how I deserialize into the Wrapper class, then retrieve the StaffDetail from it. Also notice that I don't need to (and shouldn't in this case) pass the converters to the deserializer. They will get picked up automatically and at the appropriate times by virtue of the [JsonConverter] attributes on the StaffDetail class properties.

StaffDetail detail = JsonConvert.DeserializeObject<Wrapper>(json).d;

Here is simple demo program to show how it all works:

class Program
{
    static void Main(string[] args)
    {
        string json = @"
        {
            ""d"": {
                ""__type"": ""CubeJsonData"",
                ""SessionID"": null,
                ""Code"": 0,
                ""Message"": """",
                ""Rows"": {},
                ""Columns"": {
                    ""Place 1"": [
                        0,
                        1
                    ],
                    ""Place 2"": [
                        0,
                        2,
                        4,
                        6
                    ]
                },
                ""Set"": [
                    [
                        ""Number 1""
                    ],
                    [
                        ""Number 2""
                    ],
                    [
                        ""Number 3""
                    ]
                ]
            }
        }";

        StaffDetail detail = JsonConvert.DeserializeObject<Wrapper>(json).d;

        Console.WriteLine("Columns: " + string.Join(", ", detail.Columns));
        Console.WriteLine("Set: " + string.Join(", ", detail.Set));
    }
}

Output:

Columns: Place 1, Place 2
Set: Number 1, Number 2, Number 3

Solution 2

your Json is a bit weird, if you can change it. Nevertheless, correct classes : (+ DONT FORGET TO REMOVE A COMMA I WROTE ABOUT )

public class Columns
{
    [JsonProperty(PropertyName="Place 1")]
    public List<int> Place1;
    [JsonProperty(PropertyName="Place 2")]
    public List<int> Place2;
}

public class Rows { }

public class D
{
    public string __type;
    public object SessionID;
    public int Code;
    public string Message;
    public Rows Rows;
    public Columns Columns;
    public List<List<string>> Set;
}
public class StaffDetail
{
    public D d { get; set; }
}

and a single simple way to get it all

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

then just get the properties all you want, like

result.d.Columns.Place1[0] // for example
Share:
15,060
Raii
Author by

Raii

Updated on June 05, 2022

Comments

  • Raii
    Raii almost 2 years

    My JSON looks like

    {
    "d": {
    "__type": "CubeJsonData",
    "SessionID": null,
    "Code": 0,
    "Message": "",
    "Rows": {},
    "Columns": {
      "Place 1": [
        0,
        1
      ],
      "Place 2": [
        0,
        2,
        4,
        6
      ],
    },
    "Set": [
      [
        "Number 1"
      ],
      [
        "Number 2"
      ],
      [
        "Number 3"
      ]
    ]
    }
    }
    

    I need to get the following values

    List<string> Columns must contain: "Place 1", "Place 2"
    List<string> Set must contain: "Number 1", "Number 2", "Number 3"
    

    My caller is

    var settings = new JsonSerializerSettings();
    settings.Converters.Add(new AssosiativeArrayConverter());
    var staffAlerts = JsonConvert.DeserializeObject<List<AlertDetails>>(jo.ToString(), settings);
    

    My JsonConverter is

    class AssosiativeArrayConverter : JsonConverter
    {
        public override bool CanConvert(Type objectType)
        {
            return (objectType == typeof(string)) || (objectType == typeof(List<string>));
        }
    
        public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
        {
            if (reader.TokenType == JsonToken.StartArray)
            {
                var l = new List<string>();
                reader.Read();
                while (reader.TokenType != JsonToken.EndArray)
                {
                    l.Add(reader.Value as string);
    
                    reader.Read();
                }
                return l;
            }
            else
            {
                return new List<string> { reader.Value as string };
            }
        }
    
        public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
        {//to do
        }
    }
    

    My class is

    class StaffDetail
    {
        public string __type { get; set; }
        public string SessionID { get; set; }
        public string Code { get; set; }
        public string Message { get; set; }
    
        [JsonConverter(typeof(AssosiativeArrayConverter))]
        public List<string> Rows { get; set; }
        [JsonConverter(typeof(AssosiativeArrayConverter))]
        public List<string> Columns { get; set; }
        [JsonConverter(typeof(AssosiativeArrayConverter))]
        public List<string> Set { get; set; }
    }
    

    I am getting an error

    Cannot deserialize the current JSON object (e.g. {"name":"value"}) into type 'System.Collections.Generic.List`1[ReportingServicesAlerts.AlertDetails]' because the type requires a JSON array (e.g. [1,2,3]) to deserialize correctly. To fix this error either change the JSON to a JSON array (e.g. [1,2,3]) or change the deserialized type so that it is a normal .NET type (e.g. not a primitive type like integer, not a collection type like an array or List) that can be deserialized from a JSON object. JsonObjectAttribute can also be added to the type to force it to deserialize from a JSON object.

    Can you help me figure out what I'm doing wrong?