简体   繁体   中英

Parsing both Array and single object

API retuned json object like below 2 forms.

Form 1

{
"Pricing": [
    {
        "total": 27,
        "currency": "USD",
        "charges": [ //Chargers Array
            {
                "code": "C1",
                "currency": "USD",
                "rate": 15
            },
            {
                "code": "C45",
                "currency": "USD",
                "rate": 12
            }
        ]
    }
  ]
}

Form 2

{
"Pricing": [
    {
        "total": 12,
        "currency": "USD",
        "charges": { //Chargers single object
            "code": "C1",
            "currency": "USD",
            "rate": 12
        }
    }
  ]
}

As you can see sometime chargers object return with array and some times not. My question is how to parse this to C# class object? If I added the C# class like below it cannot be parse properly for Form 2. (Form 1 parsing properly)

public class Charge
{
    public string code { get; set; }
    public string currency { get; set; }
    public decimal rate { get; set; }
}

public class Pricing
{
    public decimal total { get; set; }
    public string currency { get; set; }
    public List<Charge> charges { get; set; } //In Form 2, this should be single object
}

public class MainObj
{
    public List<Pricing> Pricing { get; set; }
}

Error occurred when parse with Newtonsoft deserialization.

MainObj obj = JsonConvert.DeserializeObject<MainObj>(json);

Error

Cannot deserialize the current JSON object (eg {"name":"value"}) into type 'System.Collections.Generic.List`1[Charge]' because the type requires a JSON array (eg [1,2,3]) to deserialize correctly. To fix this error either change the JSON to a JSON array (eg [1,2,3]) or change the deserialized type so that it is a normal .NET type (eg 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. Path 'Pricing[0].charges.code', line 1, position 69.

Any common method for parsing, when receiving different type of object types with C#?

(I look into this as well but it's for java. And most of this kind of question raised for java but not C#.)

You could go for an approach to try and parse Form1's object to json, if it fails it will use Form2's object to json.

Example here and below: https://dotnetfiddle.net/F1Yh25

using System;
using System.Collections.Generic;
using Newtonsoft.Json;
public class Program
{
    public static void Main()
    {
        string form1 = "{\"Pricing\":[{\"total\":27,\"currency\":\"USD\",\"charges\":[{\"code\":\"C1\",\"currency\":\"USD\",\"rate\":15},{\"code\":\"C45\",\"currency\":\"USD\",\"rate\":12}]}]}";
        string form2 = "{\"Pricing\":[{\"total\":12,\"currency\":\"USD\",\"charges\":{\"code\":\"C1\",\"currency\":\"USD\",\"rate\":12}}]}";
        
        string json = form1;//Change form1 to form2 and it will also work
        try
        {
            Form1.Root Object = JsonConvert.DeserializeObject<Form1.Root>(json);
            Console.WriteLine("Item 1's Pricing: " + Object.Pricing[0].total);
        }
        catch//If Form1's json is Form2 it will catch here
        {
            Form2.Root Object = JsonConvert.DeserializeObject<Form2.Root>(json);
            Console.WriteLine("Item 1's Pricing: " + Object.Pricing[0].total);
        }
    }
    
    public class Form1
    {
        public class Charge
    {
        public string code { get; set; }
        public string currency { get; set; }
        public int rate { get; set; }
    }

    public class Pricing
    {
        public int total { get; set; }
        public string currency { get; set; }
        public List<Charge> charges { get; set; }
    }

    public class Root
    {
        public List<Pricing> Pricing { get; set; }
    }
        
    }
    
    public class Form2
    {
    public class Charges
    {
        public string code { get; set; }
        public string currency { get; set; }
        public int rate { get; set; }
    }

    public class Pricing
    {
        public int total { get; set; }
        public string currency { get; set; }
        public Charges charges { get; set; }
    }

    public class Root
    {
        public List<Pricing> Pricing { get; set; }
    }
    }
}

Ok so here is another way to do this without having to use two classes and not having a try catch. Basically just update Pricing class to following and it works for both cases. Probably a better way but this is better (at least in my opinion) and having two classes and having try catch do your branching. If you had ten properties with this issue would you then create 10? classes to handle every combo? No way lol!

public class Pricing {
    public int total { get; set; }
    public string currency { get; set; }
    private List<Charge> _charges;
    public object charges {
        get {
            return _charges;
        }
        set {
            if(value.GetType().Name == "JArray") {
                _charges = JsonConvert.DeserializeObject<List<Charge>>(value.ToString());
            }
            else {
                _charges = new List<Charge>();
                var charge = JsonConvert.DeserializeObject<Charge>(value.ToString());
                _charges.Add(charge);
            }
        }
    }
}

Yet another way of dealing with this problem is to define a custom JsonConverter which can handle both cases.

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

    public override bool CanConvert(Type objectType)
        => objectType == typeof(List<T>);

    public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
        =>throw new NotImplementedException(); 
}
  • Inside the ReadJson first we get a JToken to be able to determine the read value's Type (kind)
    • Based on that the we can either call ToObject<List<T>> or ToObject<T>
  • Inside the CanConvert we examine that the to be populated property's type is a List<T>
    • Even though there is a generic JsonConverter<T> where you don't have to define the CanConvert , its ReadJson can be implemented in a bit more complicated way
  • Since the question is all about deserialization I've not implemented the WriteJson method
    • You might also consider to override the CanWrite property of the base class to always return false

With this class in our hand you can decorate your properties with a JsonConverterAttribute to tell to the Json.NET how to deal with those properties

public class Pricing
{
    public decimal total { get; set; }
    public string currency { get; set; }

    [JsonConverter(typeof(ArrayOrObjectConverter<Charge>))]
    public List<Charge> charges { get; set; } 
    
    ...
}

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