Reputation: 7348
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
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
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
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