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
1

4 Answers 4

6

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.

Sign up to request clarification or add additional context in comments.

Comments

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.

Comments

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>());

}

Comments

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

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.