.NET Core 序列化继承的类属性——在保留引用时不仅仅是基础属性
Posted
技术标签:
【中文标题】.NET Core 序列化继承的类属性——在保留引用时不仅仅是基础属性【英文标题】:.NET Core Serialize Inherited Class Properties -- Not Just Base Properties When Preserving References 【发布时间】:2021-12-09 12:59:52 【问题描述】:我们在使用 System.Text.Json.JsonSerializer 进行序列化时遇到问题。
在此示例中,我们有三个类:Store
、Employee
和 Manager
。请注意,Manager 继承自 Employee。
public class Employee
public string Name get; set;
public int Age get; set;
public class Manager : Employee
public int AllowedPersonalDays get; set;
public class Store
public Employee EmployeeOfTheMonth get; set;
public Manager Manager get; set;
public string Name get; set;
在Store
类中,我们有一个名为EmployeeOfTheMonth
的属性。好吧,举个例子,假设这个属性引用了与Manager
属性相同的对象。因为EmployeeOfTheMonth
首先被序列化,所以它只会序列化Employee
属性。在序列化Manager
属性时——因为它是第二个并且是同一个对象——它将添加对EmployeeOfTheMonth
的引用。当我们这样做时,我们将丢失附加到Manager
的附加属性,即AllowedPersonalDays
。此外,如您所见,它不会反序列化,因为——虽然经理是员工——但员工不是经理。
这是我们的简短示例:
Manager mgr = new Manager()
Age = 42,
AllowedPersonalDays = 14,
Name = "Jane Doe",
;
Store store = new Store()
EmployeeOfTheMonth = mgr,
Manager = mgr,
Name = "ValuMart"
;
System.Text.Json.JsonSerializerOptions options = new System.Text.Json.JsonSerializerOptions();
options.ReferenceHandler = System.Text.Json.Serialization.ReferenceHandler.Preserve;
string serialized = System.Text.Json.JsonSerializer.Serialize<Store>(store, options);
var deserialized = System.Text.Json.JsonSerializer.Deserialize<Store>(serialized, options); // <-- Will through an exception per reasons stated above
如果我们查看变量serialized
,这是内容:
"$id":"1",
"EmployeeOfTheMonth":
"$id":"2",
"Name":"Jane Doe",
"Age":42
,
"Manager":
"$ref":"2"
,
"Name":"ValuMart"
使用 System.Text.Json.JsonSerializer,我们如何才能让 EmployeeOfTheMonth
正确序列化为 Manager
?也就是说,我们需要序列化如下所示:
"$id":"1",
"EmployeeOfTheMonth":
"$id":"2",
"Name":"Jane Doe",
"Age":42,
"AllowedPersonalDays":14 <-- We need to retain this property even if the EmployeeOfTheMonth is a Manager
,
"Manager":
"$ref":"2"
,
"Name":"ValuMart"
我知道我可以调整Store
类中属性的顺序,但这不是一个选项,也是一个非常糟糕的选择。谢谢大家。
【问题讨论】:
我认为您可以使用支持类似类型处理的序列化/反序列化的自定义序列化程序来解决这个问题(使用一些discriminator,而不是实际类型)。 你需要反序列化,还是仅仅序列化? 如果您需要反序列化和序列化,将引用跟踪与自定义转换器相结合的要求是棘手的 - 并且 MSFT 不直接支持,请参阅 [ReferenceHandler.IgnoreCycles 不适用于自定义转换器 #51715 ](github.com/dotnet/runtime/issues/51715)。但是如果你只需要序列化,有一些相当简单的方法可以得到你想要的。 是的,我们需要序列化和反序列化。我想我需要创建一个自定义转换器。谢谢大家的帮助。 This answer by Alexander Sheremetyev to Resolve cycle references of complex type during JSON serialization using System.Text.Json.Serialization.JsonConverter 展示了如何编写一个自定义转换器,该转换器也发出参考信息。这样做需要实现自定义ReferenceHandler
,因为自 .NET 5 起,MSFT 不会将其内部引用处理程序提供给转换器。
【参考方案1】:
documentation on writing custom converters 有一个非常相似的示例(区分属性声明类型的两个子类),可以进行如下调整:
public class EmployeeConverter : JsonConverter<Employee>
enum TypeDiscriminator
Employee = 1,
Manager = 2
private static string s_typeDiscriminatorLabel = "$TypeDiscriminator";
public override bool CanConvert(Type typeToConvert) =>
typeof(Employee).IsAssignableFrom(typeToConvert);
public override Employee Read(
ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
if (reader.TokenType != JsonTokenType.StartObject)
throw new JsonException();
reader.Read();
if (reader.TokenType != JsonTokenType.PropertyName)
throw new JsonException();
string propertyName = reader.GetString();
if (propertyName != s_typeDiscriminatorLabel)
throw new JsonException();
reader.Read();
if (reader.TokenType != JsonTokenType.Number)
throw new JsonException();
// Instantiate type based on type discriminator value
TypeDiscriminator typeDiscriminator = (TypeDiscriminator)reader.GetInt32();
Employee employee = typeDiscriminator switch
TypeDiscriminator.Employee => new Employee(),
TypeDiscriminator.Manager => new Manager(),
_ => throw new JsonException()
;
while (reader.Read())
if (reader.TokenType == JsonTokenType.EndObject)
return employee;
if (reader.TokenType == JsonTokenType.PropertyName)
propertyName = reader.GetString();
reader.Read();
switch (propertyName)
case "Name":
string name = reader.GetString();
employee.Name = name;
break;
case "Age":
int age = reader.GetInt32();
employee.Age = age;
break;
case "AllowedPersonalDays":
int allowedPersonalDays = reader.GetInt32();
if(employee is Manager manager)
manager.AllowedPersonalDays = allowedPersonalDays;
else
throw new JsonException();
break;
throw new JsonException();
public override void Write(
Utf8JsonWriter writer, Employee person, JsonSerializerOptions options)
writer.WriteStartObject();
// Write type indicator based on whether the runtime type is Manager
writer.WriteNumber(s_typeDiscriminatorLabel, (int)(person is Manager ? TypeDiscriminator.Manager : TypeDiscriminator.Employee));
writer.WriteString("Name", person.Name);
writer.WriteNumber("Age", person.Age);
// Write Manager-ony property only if runtime type is Manager
if(person is Manager manager)
writer.WriteNumber("AllowedPersonalDays", manager.AllowedPersonalDays);
writer.WriteEndObject();
添加一个自定义转换器的实例,它应该可以正确反序列化:
options.Converters.Add(new EmployeeConverter());
string serialized = JsonSerializer.Serialize<Store>(store, options);
var deserialized = JsonSerializer.Deserialize<Store>(serialized, options);
string reserialized = JsonSerializer.Serialize<Store>(deserialized, options);
System.Diagnostics.Debug.Assert(serialized == reserialized, "Manager property should be retained");
【讨论】:
这实际上并没有生成所需的"$id"
和"$ref"
属性,不是吗?以上是关于.NET Core 序列化继承的类属性——在保留引用时不仅仅是基础属性的主要内容,如果未能解决你的问题,请参考以下文章
在.net core 的webapi项目中将对象序列化成json
获取 .NET Core JsonSerializer 以序列化私有成员