如何在 C# 中反序列化包含具有不同形状的对象的 JSON 数组?

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

考虑以下 JSON:

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

注意 Items 数组是一个混合内容数组,它包含具有不同形状的对象。

可以使用以下 class 来描述其中一个形状:

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

另一种形状可以使用以下 C# class 来描述:

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

我想通过使用 newtonsoft.json[= 将此 JSON 反序列化为 C# class 38=]。 在这两种情况下,我都需要一个 class 用于反序列化,但我不知道如何处理 Bar 数组。

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

如何反序列化此 JSON?

对于那些熟悉 typescript 的人,我需要类似联合类型的东西(例如:将 Bar 属性 定义为 List<Person | Item>),但基于我的知识联合C# 不支持类型。

创建一个 class 具有两者的属性,但具有可为 null 的选项。然后,您可以使用各个 classes 的属性创建两个接口。然后,一旦你将它接收到一个列表中,你就可以组织成两个不同的 IYourInterface

类型的列表

这可行,但比我预期的要复杂一些。首先创建一些接口:

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

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

}

我们将使用它们来代表您的个人和物品。

然后创建另一个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; }
}

它与您创建的 class 匹配,但我使用的是 dynamic 数组而不是 List<something>。另请注意,我通过更改引号使您的 JSON 对 C# 更友好 - 它与 JSON.

相同

我们将继续向其中添加成员 class。

首先,我创建了两个实现 IPersonIItem 的私有 classes。这些位于 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;
}

我使用了一些 Newtonsoft 魔法来实现 JsonDynamicList 的下两个成员。他们可以决定一个项目是 IItem 还是 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;
}

如果有人知道更好的方法来判断动态是否有特定成员,我很想知道。

然后我创建了一种方法来 cast (好吧,这不是真正的强制转换,但你可以这样想)数组中的一个项目转换为键入的内容.我用了前两个,我以为我要用后两个。

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

一些辅助测试的worker方法:

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

最后是一些测试代码:

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

这导致:

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

我会为列表项定义一个通用接口 IBar,然后让您的 classes 实现这个接口。 IBar 可以只是一个空接口,或者您可以选择将 Type 属性 放入其中并添加合成 Type 属性 到 Person class 匹配:

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

要从 JSON 填充 classes,您可以像这样使用简单的 JsonConverter

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

拼图的最后一块是用 [JsonConverter] 属性装饰 IBar 接口,以告诉序列化程序在处理 IBar:[=25= 时使用转换器]

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

然后你可以像往常一样反序列化:

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

这是一个工作演示:https://dotnetfiddle.net/ENLgVx