4

I am having trouble trying to determine how to make my Serialization Properly be able to access a single result, as well as an array.

When I make a REST call looking for something on a server, sometimes it will return an Array of models, but if the search results only have a single model, it will not be returned as an error. This is when I get an exception that I cannot deserialize because the Object Property is expecting an array, but is instead receiving a single object.

Is there a way to define my class so that it can handle a single object of type ns1.models when that is returned instead of an array of objects?

[JsonObject]
public class Responses
{
    [JsonProperty(PropertyName = "ns1.model")]
    public List<Model> Model { get; set; }
}

Response that can be deserialized:

{"ns1.model":[
  {"@mh":"0x20e800","ns1.attribute":{"@id":"0x1006e","$":"servername"}},
  {"@mh":"0x21a400","ns1.attribute":{"@id":"0x1006e","$":"servername2"}}
]}

Response that cannot be serialized (because JSON includes only a singe "ns1.model"):

{"ns1.model":
   {"@mh":"0x20e800","ns1.attribute":{"@id":"0x1006e","$":"servername"}}
}

Exception:

Newtonsoft.Json.JsonSerializationException was unhandled   HResult=-2146233088   Message=Cannot deserialize the current JSON object (e.g. {"name":"value"}) into type 'System.Collections.Generic.List`1[ConsoleApplication1.Model]' 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<T>) 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 '['ns1.model-response-list'].['ns1.model-responses'].['ns1.model'].@mh', line 1, position 130
blgrnboy
  • 4,877
  • 10
  • 43
  • 94
  • 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) – Andrew Whitaker Nov 20 '14 at 20:10

4 Answers4

5

To handle this you have to use a custom JsonConverter. But you probably already had that in mind. You are just looking for a converter that you can use immediately. And this offers more than just a solution for the situation described. I give an example with the question asked.

How to use my converter:

Place a JsonConverter Attribute above the property. JsonConverter(typeof(SafeCollectionConverter))

public class Response
{
    [JsonProperty("ns1.model")]
    [JsonConverter(typeof(SafeCollectionConverter))]
    public List<Model> Model { get; set; }
}

public class Model
{
    [JsonProperty("@mh")]
    public string Mh { get; set; }

    [JsonProperty("ns1.attribute")]
    public ModelAttribute Attribute { get; set; }
}

public class ModelAttribute
{
    [JsonProperty("@id")]
    public string Id { get; set; }

    [JsonProperty("$")]
    public string Value { get; set; }
}

And this is my converter:

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

namespace stackoverflow.question18994685
{
    public class SafeCollectionConverter : JsonConverter
    {
        public override bool CanConvert(Type objectType)
        {
            return true;
        }

        public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
        {
            //This not works for Populate (on existingValue)
            return serializer.Deserialize<JToken>(reader).ToObjectCollectionSafe(objectType, serializer);
        }     

        public override bool CanWrite => false;

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

And this converter uses the following class:

using System;

namespace Newtonsoft.Json.Linq
{
    public static class SafeJsonConvertExtensions
    {
        public static object ToObjectCollectionSafe(this JToken jToken, Type objectType)
        {
            return ToObjectCollectionSafe(jToken, objectType, JsonSerializer.CreateDefault());
        }

        public static object ToObjectCollectionSafe(this JToken jToken, Type objectType, JsonSerializer jsonSerializer)
        {
            var expectArray = typeof(System.Collections.IEnumerable).IsAssignableFrom(objectType);

            if (jToken is JArray jArray)
            {
                if (!expectArray)
                {
                    //to object via singel
                    if (jArray.Count == 0)
                        return JValue.CreateNull().ToObject(objectType, jsonSerializer);

                    if (jArray.Count == 1)
                        return jArray.First.ToObject(objectType, jsonSerializer);
                }
            }
            else if (expectArray)
            {
                //to object via JArray
                return new JArray(jToken).ToObject(objectType, jsonSerializer);
            }

            return jToken.ToObject(objectType, jsonSerializer);
        }

        public static T ToObjectCollectionSafe<T>(this JToken jToken)
        {
            return (T)ToObjectCollectionSafe(jToken, typeof(T));
        }

        public static T ToObjectCollectionSafe<T>(this JToken jToken, JsonSerializer jsonSerializer)
        {
            return (T)ToObjectCollectionSafe(jToken, typeof(T), jsonSerializer);
        }
    }
}

What does it do exactly? If you place the converter attribute the converter will be used for this property. You can use it on a normal object if you expect a json array with 1 or no result. Or you use it on an IEnumerable where you expect a json object or json array. (Know that an array -object[]- is an IEnumerable) A disadvantage is that this converter can only be placed above a property because he thinks he can convert everything. And be warned. A string is also an IEnumerable.

And it offers more than an answer to the question: If you search for something by id you know that you will get an array back with one or no result. The ToObjectCollectionSafe<TResult>() method can handle that for you.

This is usable for Single Result vs Array using JSON.net and handle both a single item and an array for the same property and can convert an array to a single object.

I made this for REST requests on a server with a filter that returned one result in an array but wanted to get the result back as a single object in my code. And also for a OData result response with expanded result with one item in an array.

Have fun with it.

Roberto B
  • 542
  • 5
  • 13
1

I think your question has been answered already. Please have a look at this thread: How to handle both a single item and an array for the same property using JSON.net .

Basically the way to do it is to define a custom JsonConvertor for your property.

Community
  • 1
  • 1
boyomarinov
  • 615
  • 4
  • 12
1

There is not an elegant solution to your problem in the current version of JSON.NET. You will have to write custom parsing code to handle that.

As @boyomarinov said you can develop a custom converter, but since your JSON is pretty simple you can just parse your JSON into an object and then handle the two cases like this:

var obj = JObject.Parse(json);

var responses = new Responses { Model = new List<Model>() };

foreach (var child in obj.Values())
{
    if (child is JArray)
    {
        responses.Model = child.ToObject<List<Model>>();
        break;
    }
    else
        responses.Model.Add(child.ToObject<Model>());

}
Faris Zacina
  • 14,056
  • 7
  • 62
  • 75
1

Use JRaw type proxy property ModelRaw:

public class Responses
{
    [JsonIgnore]
    public List<Model> Model { get; set; }

    [JsonProperty(PropertyName = "ns1.model")]
    public JRaw ModelRaw
    {
        get { return new JRaw(JsonConvert.SerializeObject(Model)); }
        set
        {
            var raw = value.ToString(Formatting.None);
            Model = raw.StartsWith("[")
                ? JsonConvert.DeserializeObject<List<Model>>(raw)
                : new List<Model> { JsonConvert.DeserializeObject<Model>(raw) };
        }
    }
}
dizel3d
  • 3,619
  • 1
  • 22
  • 35