65.9K
CodeProject 正在变化。 阅读更多。
Home

使用 JSON.Net 和泛型的嵌套多态反序列化程序

starIconstarIconstarIconstarIconstarIcon

5.00/5 (3投票s)

2020年10月5日

CPOL

4分钟阅读

viewsIcon

13617

downloadIcon

134

如何反序列化包含嵌套多态对象的JSON字符串

引言

我希望为一个我一直在调查的项目开发一个动态表单构建器,当我偶然发现了 https://jsonforms.io。虽然这个概念按照我的设想工作,但我希望在 C# 中有一些东西,不仅可以用于 Web UI,还可以用于 WPF 表单。我目前的设想是制作 JSONForms 的 C# 端口,这仍然是一个进行中的项目...

我花了很多时间试图让 JSON 正确反序列化。JSONForms 中使用的 UI Schema 是 JSON Schema 的一个变体 (https://json-schema.fullstack.org.cn/),但它不使用 $type 属性来标识类型,并且类型名称与定义的类不一致,这导致了一些挠头。通过 StackOverflow 和 NewtonSoft 网站提供了很多答案,但似乎没有一个适合。

提供的代码是在 LinqPad5 (https://www.linqpad.net/) 上编写的。

源 JSON

来自 JSONForms 的 Person 示例包含在文件 https://github.com/eclipsesource/jsonforms/blob/master/packages/examples/src/person.ts 中,我只从 UISchema 部分开始,为了方便起见,我将其放在一个 static class 中。

public static class Person
{
    public static string UI_Schema => @"{
    type: 'VerticalLayout',
    elements: [
        {
            type: 'HorizontalLayout',
            elements: [
                {
                    type: 'Control',
                    scope: '#/properties/name'
                },
                {
                    type: 'Control',
                    scope: '#/properties/personalData/properties/age'
                },
                {
                    type: 'Control',
                    scope: '#/properties/birthDate'
                }
            ]
        },
        {
            type: 'Label',
            text: 'Additional Information'
        },
        {
            type: 'HorizontalLayout',
            elements: [
                {
                    type: 'Control',
                    scope: '#/properties/personalData/properties/height'
                },
                {
                    type: 'Control',
                    scope: '#/properties/nationality'
                },
                {
                    type: 'Control',
                    scope: '#/properties/occupation',
                    suggestion: [
                        'Accountant',
                        'Engineer',
                        'Freelancer',
                        'Journalism',
                        'Physician',
                        'Student',
                        'Teacher',
                        'Other'
                    ]
                }
            ]
        },
        {
            type: 'Group',
            elements: [
                {
                    type: 'Control',
                    label: 'Eats vegetables?',
                    scope: '#/properties/vegetables'
                },
                {
                    type: 'Control',
                    label: 'Kind of vegetables',
                    scope: '#/properties/kindOfVegetables',
                    rule: {
                        effect: 'HIDE',
                        condition: {
                            type: 'SCHEMA',
                            scope: '#/properties/vegetables',
                            schema: {
                                const: false
                            }
                        }
                    }
                }
            ]
        }
    ]
}";
}

需要注意的关键点是大多数对象上都存在 type 属性。这是 JSONForms 用于将 JSON 解码到其类中的内容。

类层次结构

为了能够反序列化,我需要从 JSONForms 移植类结构;这导致了两个相关的层次结构:UISchema 和 Conditions。

UISchema

UISchema 类用于概述表单布局,并包含用于 Layouts、Controls、Labels 和 Categories 的类。所有类都通过硬编码到类构造函数中的 type 属性来标识。请注意,分配给类的类型的值与类名不同。

public class UISchemaElement
{
    public string type { get; set; } = "VOID";
    public Dictionary<string, object> options { get; set; } = new Dictionary<string, object>();
    public Rule rule { get; set; }
}

Layouts

public class Layout : UISchemaElement
{
    public List<UISchemaElement> elements { get; set; }

    public Layout()
    {
        elements = new List<UISchemaElement>();
    }
}

public class VerticalLayout : Layout
{
    public VerticalLayout()
    {
        type = "VerticalLayout";
    }
}

public class HorizontalLayout : Layout
{
    public HorizontalLayout()
    {
        type = "HorizontalLayout";
    }
}

public class GroupLayout : Layout
{
    public string label { get; set; }

    public GroupLayout()
    {
        type = "GroupLayout";
    }
}

分类

public class Category : Layout, ICategorize
{
    public string label { get; set; }

    public Category()
    {
        type = "Category";
    }
}

public class Categorization : UISchemaElement, ICategorize
{
    public string label { get; set; }

    public List<ICategorize> elements { get; set; }

    public Categorization()
    {
        {
            type = "Categorization";
        }
    }
}

Category 虽然是一个 Layout 类,但被视为特殊类,它是唯一允许在元素的分类列表中使用的布局;因此,ICategorize 接口被应用于它以及 Categorization 类。 ICategorize 接口是一个空接口。

标签和控件

public class LabelElement : UISchemaElement
{
    public string text { get; set; }

    public LabelElement()
    {
        type = "Label";
    }
}

public class ControlElement : UISchemaElement, IScopable
{
    public string label { get; set; }
    public string scope { get; set; }

    public ControlElement()
    {
        type = "Control";
    }
}

初次尝试

我最初的反序列化尝试完全失败;结果将返回一个带有单个 UISchema 对象,没有元素,只有类型值。

var result = JsonConvert.DeserializeObject<UISchemaElement>(Person.UI_Schema); 

First Attempts result

我的解决方案

在搜索了所有能找到的资源之后,以及一些失败的方向(我可以反序列化顶级对象,但不能反序列化任何子元素),我发现了这个难题的关键部分。

JSON.Net 包含一个 abstractJsonConverter<T>,可用于序列化和反序列化为复杂的类。我一直在使用它来获取顶级对象,但我一直在遵循的示例并没有解决嵌套类问题。直到我发现了 JsonSerializer.Populate 方法。Populate 方法的作用是将 JObject 反序列化为新的 POCO 湾,方法是使用所有相关的转换器调用现有的序列化程序。

我还想避免在我的转换器中使用 switch 语句或嵌套 if 语句;为了克服这个问题,我使用了 TypeMap 来主动定位我的 Converter 可以使用的允许的转换。

public class TypeMapConverter<T> : JsonConverter<T> where T : class
{
    public TypeMapConverter(string Selector)
    {
        selector = Selector;
    }
    protected string selector { get; private set; }
    protected Dictionary<string, Type> TypeMap;

    public new bool CanConvert(Type objectType)
    {
        return typeof(T).IsAssignableFrom(objectType);
    }

    public override bool CanRead => true;

    public override bool CanWrite => false;

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

    public override T ReadJson(JsonReader reader, Type objectType, 
           T existingValue, bool hasExistingValue, JsonSerializer serializer)
    {
        JObject jObject = JObject.Load(reader);
        string key = jObject[selector]?.Value<string>() ?? string.Empty;
        if (string.IsNullOrEmpty(key))
        {
            return (T)System.Activator.CreateInstance(typeof(T));
        }
        T item;
        if (TypeMap.TryGetValue(jObject[selector].Value<string>(), out Type target))
        {
            item = (T)System.Activator.CreateInstance(target);
            serializer.Populate(jObject.CreateReader(), item);
            return item;
        }
        return (T)System.Activator.CreateInstance(typeof(T));
    }
}

public class UISchemaConverter : TypeMapConverter<UISchemaElement>
{
    public UISchemaConverter(string Selector) : base(Selector)
    {
        TypeMap = new Dictionary<string, System.Type>()
        {
            { "Label", typeof(LabelElement) },
            { "Control", typeof(ControlElement) },
            { "Categorization", typeof(Categorization) },
            { "VerticalLayout", typeof(VerticalLayout) },
            { "HorizontalLayout", typeof(HorizontalLayout) },
            { "Group", typeof(GroupLayout) },
            { "Category", typeof(Category) }
        };
    }
}

TypeMapConverterJsonConverter 的一个包装类。它包含主要的转换处理功能和 Type 选择代码。ReadJson 方法提取 JObject 以访问选择器字段,查询 TypeMap 以获取正确的类型,并创建一个该类型的新实例。创建类型后,它被反馈到序列化程序中以填充该项目。这个过程是递归的;它正确地解释了 UISchema JSON 的嵌套多态性质。

通过使用泛型 TypeMapConverter,我现在可以通过在构造函数中创建一个带有 TypeMap 的继承类来轻松地向系统中添加新的转换器;例如,ConditionConverter

public class ConditionConverter : TypeMapConverter<Condition>
{
    public ConditionConverter(string Selector) : base(Selector)
    {
        TypeMap = new Dictionary<string, System.Type>()
        {
            { "LEAF", typeof(LeafCondition)},
            { "OR", typeof(OrCondition)},
            { "AND", typeof(AndCondition)},
            { "SCHEMA", typeof(SchemaCondition)}
        };
    }
}

Using the Code

与任何 JSON.NET 反序列化一样;可以在 SerializeDeserialize 方法调用中添加转换器,例如

result = JsonConvert.DeserializeObject<UISchemaElement>
(Person.UI_Schema, new UISchemaConverter("type"), new ConditionConverter("type"));

重要的是要记住在构造函数调用中设置 Selector 属性;如果将其保留为空字符串(或在 JSON 中未找到选择器),则将返回一个空的根对象。

使用来自 Person.UISchema 的 JSON 的反序列化调用的结果如下所示

Solution Results

历史

  • 2020 年 10 月 5 日:首次发布
© . All rights reserved.