Enrico Massone
Enrico Massone

Reputation: 7348

How to deserialize a JSON array containing objects having different shape in C#?

Consider the following JSON:

{
  "Foo": "Whatever",
  "Bar": [
   { "Name": "Enrico", "Age": 33, "Country": "Italy" }, { "Type": "Video", "Year": 2004 },
   { "Name": "Sam", "Age": 18, "Country": "USA" }, { "Type": "Book", "Year": 1980 }
  ]
}

Notice that the Items array is a mixed content array, it contains objects having different shapes.

One of these shapes can be described by using the following class:

class Person 
{
    public string Name { get; set; }
    public int Age { get; set; }
    public string Country { get; set; }
}

The other shape, instead, can be described by using the following C# class:

class Item 
{
    public string Type { get; set; }
    public int Year { get; set; }
}

I would like to deserialize this JSON to a C# class by using either newtonsoft.json or System.Text.Json. In both cases I need a class to be used for the deserialization, but I don't know how to treat the Bar array.

class ClassToDeserialize
{
    public string Foo { get; set; }
    public List<what should I put here ???> Bar { get; set; }
}

How can I deserialize this JSON ?

For the ones familiar with typescript, I would need something like an union type (e.g.: defining the Bar property as a List<Person | Item>), but based on my knowledge union types are not supported in C#.

Upvotes: 1

Views: 1564

Answers (3)

Brian Rogers
Brian Rogers

Reputation: 129777

I would define a common interface IBar for the list items, then make your classes implement this interface. IBar could just be an empty interface, or you can optionally put the Type property into it and add a synthetic Type property to the Person class to match:

interface IBar
{
    string Type { get; }
}

class Person : IBar
{
    public string Type => "Person";
    public string Name { get; set; }
    public int Age { get; set; }
    public string Country { get; set; }
}

class Item : IBar
{
    public string Type { get; set; }
    public int Year { get; set; }
}

class ClassToDeserialize
{
    public string Foo { get; set; }
    public List<IBar> Bar { get; set; }
}

To populate the classes from the JSON, you can use a simple JsonConverter like this:

public class BarConverter : JsonConverter
{
    public override bool CanConvert(Type objectType)
    {
        return typeof(IBar).IsAssignableFrom(objectType);
    }

    public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
    {
        JObject jo = JObject.Load(reader);

        // If the "age" property is present in the JSON, it's a person, otherwise it's an item
        IBar bar;
        if (jo["age"] != null)
        {
            bar = new Person();
        }
        else
        {
            bar = new Item();
        }

        serializer.Populate(jo.CreateReader(), bar);

        return bar;
    }

    public override bool CanWrite => false;

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

The final piece of the puzzle is to decorate the IBar interface with a [JsonConverter] attribute to tell the serializer to use the converter when dealing with IBar:

[JsonConverter(typeof(BarConverter))]
interface IBar
{
    string Type { get; }
}

Then you can just deserialize as you normally would:

var root = JsonConvert.DeserializeObject<ClassToDeserialize>(json);

Here is a working demo: https://dotnetfiddle.net/ENLgVx

Upvotes: 1

Flydog57
Flydog57

Reputation: 7111

This works, but it's somewhat more complicated than I expected. First create some interfaces:

public interface IPerson
{
    string Name { get; }
    int Age { get; }
    string Country { get; }
}

public interface IItem
{
    string Type { get; }
    int Year { get; }
}

}

We'll use those to represent your persons and items.

Then create the other class:

public class JsonDynamicList
{
    private const string JsonData =
        @"{
            'Foo': 'Whatever',
            'Bar': [
                { 'Name': 'Enrico', 'Age': 33, 'Country': 'Italy' }, { 'Type': 'Video', 'Year': 2004 },
                { 'Name': 'Sam', 'Age': 18, 'Country': 'USA' }, { 'Type': 'Book', 'Year': 1980 }
                ]
        }";

    public string Foo { get; set; }
    public dynamic[] Bar { get; set; }
}

It matches the class you created, but I used an array of dynamic instead of a List<something>. Also note that I made your JSON more C# friendly by changing the quotation marks - it's the same JSON.

We'll keep adding members to that class.

First, I created two private classes that implement IPerson and IItem. These sit inside the JsonDynamicList class:

private class PersonImpl :IPerson
{
    private readonly dynamic _person;
    public PersonImpl(dynamic person)
    {
        _person = person;
    }

    public IPerson AsPerson()
    {
        if (!IsPerson(_person))
        {
            return null;
        }
        //otherwise
        Name = _person.Name;
        Age = _person.Age;
        Country = _person.Country;
        return this;
    }

    public string Name { get; private set; } = default;
    public int Age { get; private set; } = default;
    public string Country { get; private set; } = default;
}

private class ItemImpl : IItem
{
    private readonly dynamic _item;
    public ItemImpl(dynamic item)
    {
        _item = item;
    }

    public IItem AsItem()
    {
        if (!IsItem(_item))
        {
            return null;
        }
        //otherwise
        Type = _item.Type;
        Year = _item.Year;
        return this;
    }

    public string Type { get; private set; } = default;
    public int Year { get; private set; } = default;
}

I used some Newtonsoft magic to implement the next two members of JsonDynamicList. They get to decide if an item is an IItem or an IPerson:

public static bool IsPerson(dynamic person)
{
    var jObjectPerson = ((JObject) person).ToObject<Dictionary<string, object>>();
    return jObjectPerson?.ContainsKey("Age") ?? false;
}

public static bool IsItem(dynamic item)
{
    var jObjectItem = ((JObject)item).ToObject<Dictionary<string, object>>();
    return jObjectItem?.ContainsKey("Year") ?? false;
}

If someone knows a better way to tell if a dynamic has a specific member, I'd love to know.

Then I created a way to cast (well, it's not really a cast, but you can think of it like that) one of the items in the array into something typed. I used the first two, I thought I was going to use the second two.

public static IPerson AsPerson(dynamic person)
{
    var personImpl = new PersonImpl(person);
    return personImpl.AsPerson();
}

public static IItem AsItem(dynamic item)
{
    var itemImpl = new ItemImpl(item);
    return itemImpl.AsItem();
}

public IItem AsItem(int index)
{
    if (index < 0 || index >= Bar.Length)
    {
        throw new IndexOutOfRangeException();
    }
    return AsItem(Bar[index]);
}

public IPerson AsPerson(int index)
{
    if (index < 0 || index >= Bar.Length)
    {
        throw new IndexOutOfRangeException();
    }
    return AsPerson(Bar[index]);
}

Some worker methods to facilitate testing:

public static string ItemToString(IItem item)
{
    return $"Type: {item.Type} - Year: {item.Year}";
}

public static string PersonToString(IPerson person)
{
    return $"Name: {person.Name} - Age: {person.Age} - Country: {person.Country}";
}

And finally some test code:

var data = JsonConvert.DeserializeObject<JsonDynamicList>(JsonData);
Debug.WriteLine($"Foo: {data.Foo}");
foreach (dynamic obj in data.Bar)
{
    if (IsItem(obj))
    {
        string itemString = ItemToString(AsItem(obj));
        Debug.WriteLine(itemString);
    }
    else
    {
        string personString = PersonToString(AsPerson(obj));
        Debug.WriteLine(personString);
    }
}

This results in:

Foo: Whatever
Name: Enrico - Age: 33 - Country: Italy
Type: Video - Year: 2004
Name: Sam - Age: 18 - Country: USA
Type: Book - Year: 1980

Upvotes: 1

CorrieJanse
CorrieJanse

Reputation: 2598

Create a class that has the properties of both, but with nullable options. You can then create two interfaces with the properties of the individual classes. Then once you received it into a list you can organise into two different list of type IYourInterface

Upvotes: 3

Related Questions