使用 JSON.Net 和泛型的嵌套多态反序列化程序
如何反序列化包含嵌套多态对象的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);
我的解决方案
在搜索了所有能找到的资源之后,以及一些失败的方向(我可以反序列化顶级对象,但不能反序列化任何子元素),我发现了这个难题的关键部分。
JSON.Net 包含一个 abstract
类 JsonConverter<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) }
};
}
}
TypeMapConverter
是 JsonConverter
的一个包装类。它包含主要的转换处理功能和 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 反序列化一样;可以在 Serialize
或 Deserialize
方法调用中添加转换器,例如
result = JsonConvert.DeserializeObject<UISchemaElement>
(Person.UI_Schema, new UISchemaConverter("type"), new ConditionConverter("type"));
重要的是要记住在构造函数调用中设置 Selector
属性;如果将其保留为空字符串(或在 JSON 中未找到选择器),则将返回一个空的根对象。
使用来自 Person.UISchema
的 JSON 的反序列化调用的结果如下所示
历史
- 2020 年 10 月 5 日:首次发布