5

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..

6
  • 2
    Can't you change the way the JSON is produced? Because this way is quite inconsistent and always producing an array makes more sense. Commented Nov 10, 2011 at 18:27
  • 1
    Why is the format of the incoming JSON object insconsistent? If mm may contain more than one element it should always be conveyed as an array ([]) and never as a simple name: value pair. Commented Nov 10, 2011 at 18:27
  • You should be able to massage the JSON before you send it to the server so it is always in the format that works. Show the JS that you use to construct the JSON object. Commented Nov 10, 2011 at 18:28
  • Well, I might change the JSON... I receive this JSON from a WS. In this WS there are some operations and finally a XML is generated. This XML sometimes is <xx yy='nn'><mm>zzz</mm></xx> and sometimes is <xx yy='nn'><mm>zzz</mm><mm>aaa</mm></xx>, depends of the number of elmement... finally that WS convert this XML to JSON (doing JsonConvert.SerializeXmlNode(doc) ) and send to me, and here start my problem... Commented Nov 10, 2011 at 18:52
  • @VicNaranja, and why do you convert XML to JSON? Why don't you use the XML directly? Commented Nov 12, 2011 at 11:38

4 Answers 4

5

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();
        }
    }
}
Sign up to request clarification or add additional context in comments.

Comments

1

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
    }
}

2 Comments

Ouch. This would be a nightmare if this turned out to be the case. I hadn't thought that he may not have control over what he is receiving.
I thought in this solutions.. but I dont like at all. I thought maybe with some kind of properties could I solve..
0

In your case you can directly use the static method from JsonConvert class

PopulateObject(string value, object target, JsonSerializerSettings settings);

pass the JsonSerializerSettings object as

new JsonSerializerSettings(){TypeNameHandling = TypeNameHandling.All})

Comments

-2

I think you need to look at your Javascript object. If you explicitly declare the type of the properties of the object that you are going to serialize into JSON you shouldn't run into any inconsistencies.

var stringProperty = new String();
var arrayProperty = new Array();

// Assign value to stringProperty and push elements into arrayProperty

var object = {
    stringProperty: stringProperty,
    arrayProperty: arrayProperty
};

var jsonObject = JSON.stringify(object);

document.write(jsonObject);

If you look at the output you will see that arrayProperty is always serialized into an array whether there are zero, one or many elements.

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.