How to handle both a single item and an array for the same property using JSON.net

Solution 1:

The best way to handle this situation is to use a custom JsonConverter.

Before we get to the converter, we'll need to define a class to deserialize the data into. For the Categories property that can vary between a single item and an array, define it as a List<string> and mark it with a [JsonConverter] attribute so that JSON.Net will know to use the custom converter for that property. I would also recommend using [JsonProperty] attributes so that the member properties can be given meaningful names independent of what is defined in the JSON.

class Item
{
    [JsonProperty("email")]
    public string Email { get; set; }

    [JsonProperty("timestamp")]
    public int Timestamp { get; set; }

    [JsonProperty("event")]
    public string Event { get; set; }

    [JsonProperty("category")]
    [JsonConverter(typeof(SingleOrArrayConverter<string>))]
    public List<string> Categories { get; set; }
}

Here is how I would implement the converter. Notice I've made the converter generic so that it can be used with strings or other types of objects as needed.

class SingleOrArrayConverter<T> : JsonConverter
{
    public override bool CanConvert(Type objectType)
    {
        return (objectType == typeof(List<T>));
    }

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

    public override bool CanWrite
    {
        get { return false; }
    }

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

Here is an short program demonstrating the converter in action with your sample data:

class Program
{
    static void Main(string[] args)
    {
        string json = @"
        [
          {
            ""email"": ""[email protected]"",
            ""timestamp"": 1337966815,
            ""category"": [
              ""newuser"",
              ""transactional""
            ],
            ""event"": ""open""
          },
          {
            ""email"": ""[email protected]"",
            ""timestamp"": 1337966815,
            ""category"": ""olduser"",
            ""event"": ""open""
          }
        ]";

        List<Item> list = JsonConvert.DeserializeObject<List<Item>>(json);

        foreach (Item obj in list)
        {
            Console.WriteLine("email: " + obj.Email);
            Console.WriteLine("timestamp: " + obj.Timestamp);
            Console.WriteLine("event: " + obj.Event);
            Console.WriteLine("categories: " + string.Join(", ", obj.Categories));
            Console.WriteLine();
        }
    }
}

And finally, here is the output of the above:

email: [email protected]
timestamp: 1337966815
event: open
categories: newuser, transactional

email: [email protected]
timestamp: 1337966815
event: open
categories: olduser

Fiddle: https://dotnetfiddle.net/lERrmu

EDIT

If you need to go the other way, i.e. serialize, while keeping the same format, you can implement the WriteJson() method of the converter as shown below. (Be sure to remove the CanWrite override or change it to return true, or else WriteJson() will never be called.)

    public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
    {
        List<T> list = (List<T>)value;
        if (list.Count == 1)
        {
            value = list[0];
        }
        serializer.Serialize(writer, value);
    }

Fiddle: https://dotnetfiddle.net/XG3eRy

Solution 2:

I was working on this for ages, and thanks to Brian for his answer. All I am adding is the vb.net answer!:

Public Class SingleValueArrayConverter(Of T)
sometimes-array-and-sometimes-object
    Inherits JsonConverter
    Public Overrides Sub WriteJson(writer As JsonWriter, value As Object, serializer As JsonSerializer)
        Throw New NotImplementedException()
    End Sub

    Public Overrides Function ReadJson(reader As JsonReader, objectType As Type, existingValue As Object, serializer As JsonSerializer) As Object
        Dim retVal As Object = New [Object]()
        If reader.TokenType = JsonToken.StartObject Then
            Dim instance As T = DirectCast(serializer.Deserialize(reader, GetType(T)), T)
            retVal = New List(Of T)() From { _
                instance _
            }
        ElseIf reader.TokenType = JsonToken.StartArray Then
            retVal = serializer.Deserialize(reader, objectType)
        End If
        Return retVal
    End Function
    Public Overrides Function CanConvert(objectType As Type) As Boolean
        Return False
    End Function
End Class

then in your class:

 <JsonProperty(PropertyName:="JsonName)> _
 <JsonConverter(GetType(SingleValueArrayConverter(Of YourObject)))> _
    Public Property YourLocalName As List(Of YourObject)

Hope this saves you some time