Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Parsing JSON string to .NET Object

I´m parsing a JSON string to a corresponding .NET Object with the Newtonsoft library. I have a problem parsing JSON properties that are arrays. Sometimes the JSON property is an array, other times, it is a single element.

Example:

This is the .NET object:

  public class xx
  {
      public string yy { get; set; }       
      public List<string> mm{ get; set; }        
  }

When i receive this JSON:

{ "xx": {"yy":"nn", "mm": [ "zzz", "aaa" ] } }

I perfectly can do:

JsonConvert.DeserializeObject<xx>(json);

But sometimes I receive this JSON:

{ "xx": {"yy":"nn", "mm":"zzz"} }

And the deserialization fails because of the list property on the C# object.

How can I define an object for deserialize the two JSON string in the same object (with List<string>).

-------- UPDATE -----

First of all a WS generate a XML doing some operation.. the XML is like

<xx yy='nn'><mm>zzz</mm></xx>

and if there are more elements is:

<xx yy='nn'><mm>zzz</mm><mm>aaa</mm></xx>

finally the WS convert this XML doing:

XmlDocument doc = new XmlDocument();
doc.LoadXml(xml);           
var json = JsonConvert.SerializeXmlNode(doc); 

and send to me the json.. and here begins my problem..

like image 970
Vic Naranja Avatar asked Oct 09 '22 12:10

Vic Naranja


2 Answers

Updated Answer:

Looking at how JSON.Net maps XML, it takes the approach that what it sees is what it serializes, except that if it sees multiples, it will make an array. This is great for many XML DOM trees with consistent layout, but unfortunately cannot work for your purposes.

You can verify this by looking at the body for the functions SerializeGroupedNodes() and SerializeNode() in the following file source.

XmlNodeConverter.cs source code @ CodePlex, ChangeSet #63616

There's another option that I'd previously thought might be overkill but would be helpful now that we know what to expect from the default behavior on the serializing end.

Json.Net supports using custom converters derived from JsonConverter to map particular cases of values on a case-by-case basis.

We can handle this either at the serializing side or the deserializing side. I've chosen to write a solution on the deserializing side, as you probably have other existing reasons to map XML to JSON.

One great benefit is that your class can stay intact except for the override, which requires that you apply an attribute. Here's a code sample demonstrating how to use JsonAttribute and a custom converter class (MMArrayConverter) to fix your problem. Note that you will probably want to test this more thoroughly and maybe update the converter to handle other cases, say if you eventually migrate to IList<string> or some other funky case like Lazy<List<string>>, or even make it work with generics.

using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using Newtonsoft.Json.Converters;

namespace JsonArrayImplictConvertTest
{
    public class MMArrayConverter : JsonConverter
    {
        public override bool CanConvert(Type objectType)
        {
            return objectType.Equals(typeof(List<string>));
        }

        public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
        {
            if (reader.TokenType == JsonToken.StartArray)
            {
                List<string> parseList = new List<string>();
                do
                {
                    if (reader.Read())
                    {
                        if (reader.TokenType == JsonToken.String)
                        {
                            parseList.Add((string)reader.Value);
                        }
                        else
                        {
                            if (reader.TokenType == JsonToken.Null)
                            {
                                parseList.Add(null);
                            }
                            else
                            {
                                if (reader.TokenType != JsonToken.EndArray)
                                {
                                    throw new ArgumentException(string.Format("Expected String/Null, Found JSON Token Type {0} instead", reader.TokenType.ToString()));
                                }
                            }
                        }
                    }
                    else
                    {
                        throw new InvalidOperationException("Broken JSON Input Detected");
                    }
                }
                while (reader.TokenType != JsonToken.EndArray);

                return parseList;
            }

            if (reader.TokenType == JsonToken.Null)
            {
                // TODO: You need to decide here if we want to return an empty list, or null.
                return null;
            }

            if (reader.TokenType == JsonToken.String)
            {
                List<string> singleList = new List<string>();
                singleList.Add((string)reader.Value);
                return singleList;
            }

            throw new InvalidOperationException("Unhandled case for MMArrayConverter. Check to see if this converter has been applied to the wrong serialization type.");
        }

        public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
        {
            // Not implemented for brevity, but you could add this if needed.
            throw new NotImplementedException();
        }
    }

    public class ModifiedXX
    {
        public string yy { get; set; }

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

        public void Display()
        {
            Console.WriteLine("yy is {0}", this.yy);
            if (null == mm)
            {
                Console.WriteLine("mm is null");
            }
            else
            {
                Console.WriteLine("mm contains these items:");
                mm.ForEach((item) => { Console.WriteLine("  {0}", item); });
            }
        }
    }

    class Program
    {
        static void Main(string[] args)
        {
            string jsonTest1 = "{\"yy\":\"nn\", \"mm\": [ \"zzz\", \"aaa\" ] }";
            ModifiedXX obj1 = JsonConvert.DeserializeObject<ModifiedXX>(jsonTest1);
            obj1.Display();

            string jsonTest2 = "{\"yy\":\"nn\", \"mm\": \"zzz\" }";
            ModifiedXX obj2 = JsonConvert.DeserializeObject<ModifiedXX>(jsonTest2);
            obj2.Display();

            // This test is now required in case we messed up the parser state in our converter.
            string jsonTest3 = "[{\"yy\":\"nn\", \"mm\": [ \"zzz\", \"aaa\" ] },{\"yy\":\"nn\", \"mm\": \"zzz\" }]";
            List<ModifiedXX> obj3 = JsonConvert.DeserializeObject<List<ModifiedXX>>(jsonTest3);
            obj3.ForEach((obj) => { obj.Display(); });

            Console.ReadKey();
        }
    }
}

Original Answer:

It would be best to fix the JSON you're receiving at the source, as many have already pointed out. You may wish to post an update showing how the XML in your updated comment is being mapped to JSON, as that would be the best route overall.

However, if you find that this is not possible and you want some way to serialize and handle the variant value after-the-fact, you can patch things up by declaring mm to be type object, and then handling the possible cases yourself using JSON.Net's Linq support. In the two scenarios you described, you'll find that declaring mm to be type object will result in either a null, a string, or a JArray being assigned to mm by the call to DeserializeObject<>.

Here's a code sample that shows this in action. There's also a case in other circumstances where you could receive a JObject, which is also covered in this sample. Note that the member function mmAsList() does the work of patching up the difference for you. Also note that I've handled null here by returning a null for List<string>; you will probably want to revise this for your implementation.

using Newtonsoft.Json;
using Newtonsoft.Json.Linq;

namespace JsonArrayUnionTest
{
    public class ModifiedXX
    {
        public string yy { get; set; }
        public object mm { get; set; }

        public List<string> mmAsList()
        {
            if (null == mm) { return null; }
            if (mm is JArray)
            {
                JArray mmArray = (JArray)mm;
                return mmArray.Values<string>().ToList();
            }

            if (mm is JObject)
            {
                JObject mmObj = (JObject)mm;
                if (mmObj.Type == JTokenType.String)
                {
                    return MakeList(mmObj.Value<string>());
                }
            }

            if (mm is string)
            {
                return MakeList((string)mm);
            }

            throw new ArgumentOutOfRangeException("unhandled case for serialized value for mm (cannot be converted to List<string>)");
        }

        protected List<string> MakeList(string src)
        {
            List<string> newList = new List<string>();
            newList.Add(src);
            return newList;
        }

        public void Display()
        {
            Console.WriteLine("yy is {0}", this.yy);
            List<string> mmItems = mmAsList();
            if (null == mmItems)
            {
                Console.WriteLine("mm is null");
            }
            else
            {
                Console.WriteLine("mm contains these items:");
                mmItems.ForEach((item) => { Console.WriteLine("  {0}", item); });
            }
        }
    }

    class Program
    {
        static void Main(string[] args)
        {
            string jsonTest1 = "{\"yy\":\"nn\", \"mm\": [ \"zzz\", \"aaa\" ] }";
            ModifiedXX obj1 = JsonConvert.DeserializeObject<ModifiedXX>(jsonTest1);
            obj1.Display();

            string jsonTest2 = "{\"yy\":\"nn\", \"mm\": \"zzz\" }";
            ModifiedXX obj2 = JsonConvert.DeserializeObject<ModifiedXX>(jsonTest2);
            obj2.Display();

            Console.ReadKey();
        }
    }
}
like image 96
meklarian Avatar answered Oct 13 '22 12:10

meklarian


What the sending service sends is supposed to conform to a contract. If it doesn't, then well, either you beat up the sending developer and make them fix it, or the various things that are sent to you are the contract. A pity you don't have any metadata to know for sure, you'll just have to try a variety of contracts until one works.

object someValue;
try
{
   someValue =JsonConvert.DeserializeObject<TypeWithList>(json);
}
catch
{
    try
    {
      someValue = JsonConvert.DeserializeObject<TypeWithString>(json);
    }
    catch
    {
    //Darn, yet another type
    }
}
like image 41
MatthewMartin Avatar answered Oct 13 '22 10:10

MatthewMartin