2

I have following JSON and I am using Json.NET (Newtonsoft.Json):

{
  "total_items": "62",
  "page_number": "6",
  "page_size": "10",
  "page_count": "7",
  "cars": {
    "car": [     
      {
        "car_name": "Honda", 
        "engines": {
          "engine": [  <-- HONDA has multiple engines, so this is an array
            {
              "name": "1.2L"
            },
            {
              "name": "1.8L"
            }
          ]
        },
        "country": "Japan"
        "image": {
            "thumb": {
                "url": "http://image_path/Honda.jpg" <-- Image provided
            }
        }
      },
      {
        "car_name": "Ford",
        "engines": {
          "engine": {   <-- FORD has single engine, so this is an object
              "name": "2.2L"
          }
        },
        "country": "Japan"
        "image": null  <-- image is null
      },
      { 
        "car_name": "VW",
        "engines": null,  <-- VW has no engines, so this is null
        "country": "Germany"  
        "image": null    <-- image is null
      }
    ]
  }
}

And I have following Car object:

class Car
{
    public Car() { }

    public string Name { get; set; }
    public string Country { get; set; }
    public List<String> EngineNames { get; set; }
}

I need to handle all 3 cases above (array for HONDA, object for FORD, null for VW). If it is not null, then get all engine names. So, for example above, my EngineNames list for the 3 cars would be:

Honda.EngineNames = {"1.2L", "1.8L"} // array in JSON
Ford.EngineNames = {"2.2L"} //object in JSON
VW.EngineNames = null //null in JSON

I need to parse the JSON above to get car data. I am parsing car_name and country but I don't know how to parse all engine names by handling the 3 situations above.

private Cars GetCars(string json)
{
    dynamic data = (JObject)JsonConvert.DeserializeObject(json);

    foreach (dynamic d in data.cars.car)
    {
        Car c = new Car(); 

        c.Name = (string)d.SelectToken("car_name");
        c.Country = (string)d.SelectToken("country");

        // PROBLEM: This works fine for array or null in JSON above (HONDA and VW), but it errors on JSON object (in case of FORD)
        // When handling FORD, I get error "'Newtonsoft.Json.Linq.JProperty' does not contain a definition for 'name'"
        c.EngineNames = (d.engines != null ? ((IEnumerable)d.engines.engine).Cast<dynamic>().Select(e => (string)e.name) : null);

        CarList.Add(c);
    }
    return CarList;
}
pixel
  • 9,653
  • 16
  • 82
  • 149
  • Why don't you make single item engines into an array of one item? Like: `"engine": [{ "name": "2.2L" }]` – Steve Dec 21 '15 at 23:18
  • @Steve JSON is provided to me, I did not make it. It is how it is, I need to parse it. Thanks – pixel Dec 21 '15 at 23:19
  • 4
    Possible duplicate of [How to handle both a single item and an array for the same property using JSON.net](http://stackoverflow.com/questions/18994685/how-to-handle-both-a-single-item-and-an-array-for-the-same-property-using-json-n) – Rob Dec 21 '15 at 23:20
  • 1
    Linked question provides a nice solution to this – Rob Dec 21 '15 at 23:20
  • @Rob Thanks Rob but it is not solving my issue. I believe that example is specifically written for cases where you have either an array (with either single element or multiple elements). I tried using that example and plugging in my JSON above and I get error like below (could not post here since it is to long) – pixel Dec 21 '15 at 23:38
  • @Rob Error: "Cannot deserialize the current JSON object (e.g. {"name":"value"}) into type 'System.Collections.Generic.List`1[JSONParseTest.Item]' 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." – pixel Dec 21 '15 at 23:38
  • 1
    It appears the issue is that the classes don't exactly match the JSON - see answer – Rob Dec 21 '15 at 23:51

2 Answers2

2

Using the converter from here (originally a proposed duplicate, but this question had some other issues with the JSON)

Your class structure needs to be modified a bit.

Looking at this JSON:

"cars": { <-- cars is an object, not an array
    "car": [ <-- the cars object actually contains the array
      {
        "car_name": "Honda", 
        "engines": { <-- same goes for this
          "engine": [
            {

Therefore, you'll need to write wrapper classes to properly reflect the JSON. Here's what I've come up with:

public class Root 
{
    public CarHolder Cars {get;set;}
}
public class CarHolder
{
    public IList<Car> Car { get; set; }
}
public class Car
{
    public Car() { }

    public string car_name { get; set; }
    public string Country { get; set; }

    public EngineHolder Engines { get; set; }
}
public class EngineHolder 
{
    [JsonConverter(typeof(SingleOrArrayConverter<Engine>))]
    public List<Engine> Engine { get; set; }
}
public class Engine
{
    public string Name { get; set; }
}

And using the convert from the above question:

public class SingleOrArrayConverter<T> : JsonConverter
{
    public override bool CanConvert(Type objectType)
    {
        return (objectType == typeof(List<T>));
    }

    public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
    {
        JToken token = JToken.Load(reader);
        if (token.Type == JTokenType.Array)
        {
            return token.ToObject<List<T>>();
        }
        return new List<T> { token.ToObject<T>() };
    }

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

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

Usage:

var result = JsonConvert.DeserializeObject<Root>(jsonStr);
Console.WriteLine(result.Cars.Car[0].Engines.Engine[0].Name == "1.2L");
Console.WriteLine(result.Cars.Car[0].Engines.Engine[1].Name == "1.8L");
Console.WriteLine(result.Cars.Car[1].Engines.Engine[0].Name == "2.2L");
Console.WriteLine(result.Cars.Car[2].Engines == null);

All print true

Looping through the cars & engines

foreach(var car in result.Cars.Car)
{
    if (car.Engines != null)
    {
        foreach(var engine in car.Engines.Engine)
        {
            var engineName = engine.Name;
        }
    }
}
Community
  • 1
  • 1
Rob
  • 26,989
  • 16
  • 82
  • 98
  • Yes, I am getting same but I have problem figuring how to loop through your "result" var and print car_name, country, and all the engines for each car? Thank you so much – pixel Dec 22 '15 at 00:34
  • 1
    @dbnex14 I've added an example to loop through the results – Rob Dec 22 '15 at 01:51
  • Thanks Rob. I have one more question and I updated my original post where I added "image" to JSON. I need to get the image url but handle case where image might be null. I can post new question and mark this one as answered if you want me to. Much much appreciated – pixel Dec 22 '15 at 02:55
  • 1
    @dbnex14 You'd just just need to create an `image` and a `thumb` class. `image` would have a property `thumb` of type `thumb`, and `thumb` would have a `url` property of type `string`. Then you'd just need the null check on `car.image != null`. – Rob Dec 22 '15 at 03:02
  • 1
    @dbnex14 No worries mate :) – Rob Dec 22 '15 at 05:17
  • Rob, how would I bind the classes you suggested above to WPF countrols? I think binding the car_name and Country would be simply (I guess, it would be like: but how about binding the Image and the Engine.Names list? Thanks, – pixel Dec 22 '15 at 23:57
  • 1
    Not sure mate, I haven't really worked with `WPF`, sorry – Rob Dec 22 '15 at 23:58
  • Thank you Rob, I'll try to figure out – pixel Dec 22 '15 at 23:59
1

You should be able to use this as your class structure;

public class Rootobject
{
    public string total_items { get; set; }
    public string page_number { get; set; }
    public string page_size { get; set; }
    public string page_count { get; set; }
    public Cars cars { get; set; }
}

public class Cars
{
    public Car[] car { get; set; }
}

public class Car
{
    public string car_name { get; set; }
    public Engines engines { get; set; }
    public string country { get; set; }
}

public class Engines
{
    public object engine { get; set; }
}

//I created below class manually
public class Engine
{
    public string name { get; set; }
}

I used inbuilt functionality of VS to generate this. Steps;

  1. Open a new cs file.
  2. Copy your json
  3. Go to Edit menu> Paste special
  4. Select Paste JSON as classes

Once this is done, it should be just a matter of creating two methods to serialize and deserialize.

Updated with serialise/deserialise methods

        private static T Deserialise<T>(string json)
        {
            var myopject = JsonConvert.DeserializeObject<T>(json);
            return myopject;
        }

        private static string Serialise<T>(T value)
        {
            var mycontent =  JsonConvert.SerializeObject(value);
            return mycontent;
        }

Now to test above methods, you can do this.

            var jsonstring = @"{
              ""total_items"": ""62"",
              ""page_number"": ""6"",
              ""page_size"": ""10"",
              ""page_count"": ""7"",
              ""cars"": {
                ""car"": [
                  {
                    ""car_name"": ""Honda"",
                    ""engines"": {
                      ""engine"": [
                        {
                          ""name"": ""1.2L""
                        },
                        {
                          ""name"": ""1.8L""
                        }
                      ]
                    },
                    ""country"": ""Japan""
                  },
                  {
                    ""car_name"": ""Ford"",
                    ""engines"": {
                      ""engine"": {
                        ""name"": ""2.2L""
                      }
                    },
                    ""country"": ""Japan""
                  },
                  {
                    ""car_name"": ""VW"",
                    ""engines"": null,
                    ""country"": ""Germany""
                  }
                ]
              }
            }";
            var myobject = Deserialise<Rootobject>(jsonstring);

            //if you want to parse engines you can do something like    this.

          if (myobject.cars != null && myobject.cars.car != null && myobject.cars.car.Any())
        {
            foreach (Car car in myobject.cars.car)
            {
                if (car.engines != null && car.engines.engine != null)
                {
                    bool isList = false;
                    try
                    {
                        var eng = Deserialise<Engine>(car.engines.engine.ToString());
                    }
                    catch
                    {
                        isList = true;
                    }
                    if (isList)
                    {
                        try
                        {
                            var eng = Deserialise<List<Engine>>(car.engines.engine.ToString());
                        }
                        catch
                        {
                            Debug.WriteLine("Not a list");
                        }
                    }
                }
            }
        }

            var myjson = Serialise(myobject);
Kosala W
  • 2,133
  • 1
  • 15
  • 20
  • Thanks. How would I deserialize this created class. Would you mind providing example as I am new to this. Thanks, – pixel Dec 22 '15 at 01:05
  • I get error "Expected class, delegate, enum, interface, or struct" and T in private static T Deserialise(string json) is red-underlined – pixel Dec 22 '15 at 01:44
  • First, before trying to implement this change in to your project, create a console app. Just copy paste my code and see if it works. Once you understand the technique fully, you can implement this in your real project. (For the time being ignore engine parsing code that i added just now.) – Kosala W Dec 22 '15 at 02:17