将属性进行序列化和反序列化,属性名根据特性中给定的类型动态分配
我在使用Newtonsoft.Json。
我已经有一些从XSD转换成的C#类。
在这些类中,有一些属性的名称在XML序列化时应根据属性中指示的类型动态改变。
// inside a broader class
[System.Xml.Serialization.XmlElementAttribute("ReportRequest", typeof(ReportRequestType), Form = System.Xml.Schema.XmlSchemaForm.Unqualified, ElementName = "ReportRequest")] [System.Xml.Serialization.XmlElementAttribute("StatusRequest", typeof(StatusRequestType), Form = System.Xml.Schema.XmlSchemaForm.Unqualified, ElementName = "StatusRequest")]
[System.Xml.Serialization.XmlElementAttribute("TransmitRequest", typeof(TransmitRequestType), Form = System.Xml.Schema.XmlSchemaForm.Unqualified, ElementName = "TransmitRequest")]
public object Item
{
get
{
return this.itemField;
}
set
{
this.itemField = value;
}
}
使用Newtonsoft.Json进行序列化时,属性名称是Item,而在XML中,例如如果生成的类型是TransmitRequestType,则对应的名称为TransmitRequest。
我尝试对Item应用一个自定义的JsonConverter,但在这种情况下似乎无法访问属性来根据Item的类型确定要使用的名称。
我尝试了ContractResolver,但那样我可以访问属性,但似乎无法知道将实际使用的类型。
我可能漏掉了什么,但我不知道是什么,也不知道问题出在哪里。
我期望得到的是类似于
来自:
class A { public string Value {get;set;}}
class B { public int B1 {get; set;} public string B2 {get; set;}}
[attribute..., MyType=typeof(A), MyName="A"]
[attribute..., MyType=typeof(B), MyName="B"]
public object Item {get;set;}
如果我创建一个对象,其中Item的类型是A,在序列化时我得到的名称来自属性
{
...
"A": {... content of class A},
...
}
如果我创建一个对象,其中Item的类型是B,在序列化时我得到的名称来自属性
{
...
"B": {... content of class B},
...
}
注:
- 不要相信这些类、类型、属性本身……它们只是用来描述问题的示例。
- 我的实际类型还包含许多其他属性,它们应用了Json.NET的 [serialization attributes] 来控制它们的名称、是否忽略,等等。Json.NET的默认contract resolver会考虑到这些,我希望避免从头重新做所有这些工作。
解决方案
你希望有一种使用Json.NET序列化模型的方法,能够像 XmlSerializer 那样处理多态属性:
- 对于每个属性,可能的多态子类型集合在编译时已知,可以通过一系列属性来指定,类似于 [
XmlElementAttribute.Type](https://learn.microsoft.com/en-us/dotnet/api/system.xml.serialization.xmlelementattribute.type) 的工作方式:
public partial class Model
{
[XmlElement("A", typeof(A)), XmlElement("B", typeof(B))]
public object Item { get; set; }
}
2.当将多态属性序列化为JSON时,使用的名称应等于当前正在序列化的实际、具体类型在属性列表中指定的名称。
3.在反序列化时,当遇到多态属性名称时,JSON中的值应被反序列化为由属性指定的类型。
鉴于这些要求,使用诸如Brian Rogers的 JsonConverter 的方案(如 [How to change property names depending on the type when serializing with Json.net?] 的链接)将不太方便。相反,我建议创建一个 自定义契约解析器,继承自 [DefaultContractResolver],为每个多态属性对应的可能值类型添加适当重命名的综合属性,然后使用 条件序列化 仅序列化当前值类型对应的单个属性。然后在反序列化时,序列化器将能够从综合属性名称自动推断类型。
首先引入以下属性:
[AttributeUsage(AttributeTargets.Field | AttributeTargets.Property, AllowMultiple = true)]
public sealed class JsonPolymorphicNameAttribute : System.Attribute
{
public JsonPolymorphicNameAttribute(string polymorphicName, Type polymorphicType)
{
this.PolymorphicName = polymorphicName;
this.PolymorphicType = polymorphicType;
}
public string PolymorphicName { get; private set; }
public Type PolymorphicType { get; private set; }
}
接着创建以下契约解析器:
public class JsonPolymorphicNameAttributeResolver : DefaultContractResolver
{
protected override JsonObjectContract CreateObjectContract(Type objectType)
{
var contract = base.CreateObjectContract(objectType);
for (int i = 0; i < contract.Properties.Count; i++)
{
var property = contract.Properties[i];
var attributeProvider = property.AttributeProvider;
var valueProvider = property.ValueProvider;
if (attributeProvider == null || valueProvider == null || property.Ignored)
continue;
var attrs = attributeProvider.GetAttributes(typeof(JsonPolymorphicNameAttribute), true);
if (attrs.Count == 0)
continue;
if (property.PropertyName != null && contract.CreatorParameters.GetClosestMatchProperty(property.PropertyName) != null)
{
// TODO: decide how to handle polymorphic property names in parameterized objects where the polymorphically named property needs to be passed to the constructor.
// Unfortunately Json.NET matches constructor parameters to JSON properties by name which is inconsistent with polymorphic property naming.
throw new ArgumentException(string.Format("Polymorphically named properties are not supported for parameterized constructors: property \"{0}\", type \"{1}\"", property.PropertyName, contract.UnderlyingType));
}
var polymorphicTypes = new HashSet<Type>(attrs.Count);
foreach (var attr in attrs.Cast<JsonPolymorphicNameAttribute>())
{
if (property.PropertyType != null && !property.PropertyType.IsAssignableFrom(attr.PolymorphicType))
{
throw new ArgumentException(string.Format("JsonProperty.PropertyType {0} is not assignable from JsonPolymorphicNameAttribute.PolymorphicType {1}", property.PropertyType, attr.PolymorphicType));
}
var newProperty = property.ShallowClone();
newProperty.PropertyName = attr.PolymorphicName;
newProperty.PropertyType = attr.PolymorphicType;
if (property.Readable)
{
newProperty.ShouldSerialize = newProperty.ShouldSerialize.And(
o =>
{
var value = valueProvider.GetValue(o);
return value != null && value.GetType() == attr.PolymorphicType;
});
}
else
{
newProperty.ShouldSerialize = o => false;
}
contract.Properties.Insert(++i, newProperty);
polymorphicTypes.Add(attr.PolymorphicType);
}
property.ShouldSerialize = property.ShouldSerialize.And(
o =>
{
var value = valueProvider.GetValue(o);
// TODO: decide what to do if the value is null, since we can't get the concretetype.
return value != null && !polymorphicTypes.Contains(value.GetType());
});
}
return contract;
}
}
public static partial class JsonExtensions
{
static readonly Func<JsonProperty, JsonProperty> ShallowClonePropertyFunc = CreateShallowCloneMethod<JsonProperty>();
public static JsonProperty ShallowClone(this JsonProperty property)
{
if (property == null)
throw new ArgumentNullException("property");
return ShallowClonePropertyFunc(property);
}
internal static Predicate<T> And<T>(this Predicate<T> first, Predicate<T> second)
{
if (second == null)
return first;
else if (first == null)
return second;
else return v => first(v) && second(v);
}
internal static Func<T, T> CreateShallowCloneMethod<T>()
{
var method = typeof(T).GetMethod("MemberwiseClone", BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public);
if (method == null)
throw new ArgumentException(string.Format("No MemberwiseClone() method was found for type {0}", typeof(T)));
var cloneUntyped = (Func<T, object>)Delegate.CreateDelegate(typeof(Func<T, object>), method);
return delegate(T obj) { return (T)cloneUntyped(obj); };
}
}
然后,要使用它,修改你的模型并按需应用 [JsonPolymorphicName]:
public class Model
{
// Other properties not shown in the question
[JsonPolymorphicName("A", typeof(A))]
[JsonPolymorphicName("B", typeof(B))]
[JsonProperty(NullValueHandling = NullValueHandling.Ignore)] // Other JsonPropertyAttribute settings which must needs be retained.
public object Item { get; set; }
}
接着用你需要的命名策略构造一个 JsonPolymorphicNameAttributeResolver 的实例:
DefaultContractResolver resolver = new JsonPolymorphicNameAttributeResolver()
{
NamingStrategy = new CamelCaseNamingStrategy(),
};
然后在序列化和反序列化一个 Model 的实例时,在 [JsonSerializerSettings.ContractResolver] 中使用它,如下所示:
var model = new Model
{
Item = new B { B1 = 13, B2 = "value of B2" },
};
var settings = new JsonSerializerSettings
{
ContractResolver = resolver,
// Add any other settings you require
};
var json = JsonConvert.SerializeObject(model, settings);
var model2 = JsonConvert.DeserializeObject<Model>(json, settings);
现在模型将来回传输为 {"B":{"b1":13,"b2":"value of B2"}},并且在反序列化时类型将被正确推断。
注:
- [
JsonProperty.ValueProvider] 有一个方法 [IValueProvider.GetValue(object target)],用于返回特定实例的属性值。 - 因此,在 [
Predicate<object>] 指定的上下文中,通过 [JsonProperty.ShouldSerialize] 可以使用值提供者来获取当前属性值,以确定其类型,从而判断该属性是否应由当前的多态属性进行序列化。 - 多态的 构造函数参数 不被支持。如果你的某个多态属性对应构造函数参数,则会抛出一个
ArgumentException。 - 我不确定当多态属性的值为null时你希望如何处理。在上面的代码中我禁用了序列化。
- 为获得最佳性能,Newtonsoft建议缓存并重复使用你的契约解析器实例,详见 Performance。
- 我在上面的实现中使用了较旧的C#语法,以便你在.NET Framework上编码时不会有问题。
- 我使用了自定义属性来指定多态名称,但如果你愿意,也可以改用 [
XmlElementAttribute]。如要获取给定属性的特定类型的属性集合,你可以使用 [JsonProperty.AttributeProvider],也就是:
var attrs = property.AttributeProvider?.GetAttributes(typeof(System.Xml.Serialization.XmlElementAttribute), true);
演示.NET Framework fiddle在这里。