如何让 Json.NET 为具有复杂值的属性设置 IsSpecified 属性?

Posted

技术标签:

【中文标题】如何让 Json.NET 为具有复杂值的属性设置 IsSpecified 属性?【英文标题】:How to make Json.NET set IsSpecified properties for properties with complex values? 【发布时间】:2021-05-14 13:27:39 【问题描述】:

我有一个使用 ASP.Net 构建的 Web 服务,到目前为止,它只使用 XML 进行输入和输出。现在它还需要能够使用 JSON。

我们使用 xsd2code++ 从 XSD 生成模型,并启用创建 "IsSpecified" properties 的选项(即,如果在 XML 中指定属性,则其各自的“指定”属性将为 true)。

来自这样的 XSD...

<?xml version="1.0" encoding="UTF-8" ?>
<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema">

  <xs:element name="Person">
    <xs:complexType>
      <xs:sequence>
        <xs:element name="ID" type="xs:string"/>
        <xs:element name="Details" type="PersonalDetails"/>
      </xs:sequence>
    </xs:complexType>
  </xs:element>
  
  
  <xs:complexType name="PersonalDetails">
    <xs:sequence>
      <xs:element name="FirstName" type="xs:string"/>
      <xs:element name="LastName" type="xs:string"/>
    </xs:sequence>
  </xs:complexType>
</xs:schema>

... xsd2code++ 创建一个类,其属性如下:

public partial class Person

    #region Private fields
    private string _id;
    private PersonalDetails _details;
    private Address _address;
    private bool _iDSpecified;
    private bool _detailsSpecified;
    private bool _addressSpecified;
    #endregion

    public Person()
    
        this._address = new Address();
        this._details = new PersonalDetails();
    

    [System.Xml.Serialization.XmlElementAttribute(Form = System.Xml.Schema.XmlSchemaForm.Unqualified)]
    public string ID
    
        get
        
            return this._id;
        
        set
        
            this._id = value;
        
    

    [System.Xml.Serialization.XmlElementAttribute(Form = System.Xml.Schema.XmlSchemaForm.Unqualified)]
    public PersonalDetails Details
    
        get
        
            return this._details;
        
        set
        
            this._details = value;
        
    

    [System.Xml.Serialization.XmlElementAttribute(Form = System.Xml.Schema.XmlSchemaForm.Unqualified)]
    public Address Address
    
        get
        
            return this._address;
        
        set
        
            this._address = value;
        
    

    [XmlIgnore()]
    public bool IDSpecified
    
        get
        
            return this._iDSpecified;
        
        set
        
            this._iDSpecified = value;
        
    

    [XmlIgnore()]
    public bool DetailsSpecified
    
        get
        
            return this._detailsSpecified;
        
        set
        
            this._detailsSpecified = value;
        
    

    [XmlIgnore()]
    public bool AddressSpecified
    
        get
        
            return this._addressSpecified;
        
        set
        
            this._addressSpecified = value;
        
    

这对 XML 非常有效。 例如,如果输入 XML 中未指定 ID,则属性 IDSpecified 将为 false。我们可以在业务逻辑层使用这些“指定”属性,这样我们就知道哪些数据必须插入/更新,哪些数据可以忽略/跳过。

然后,我们尝试添加 JSON 序列化。 我们在 WebApiConfig 类中添加了一个 Json 格式化程序:

config.Formatters.Add(new JsonMediaTypeFormatter());
config.Formatters.JsonFormatter.SerializerSettings.ContractResolver = new Newtonsoft.Json.Serialization.DefaultContractResolver();

API 现在可以识别 JSON 输入,但“指定”属性不像对 XML 那样适用于复杂对象,并且总是说它们是 false


    "ID": "abc123", // IDSpecified comes through as "true"
    "Details":  // DetailsSpecified always comes through as "false"
        "FirstName": "John", // FirstNameSpecified = true
        "LastName": "Doe", // LastNameSpecified = true
        "BirthDate": "1990-06-20" // BirthDateSpecified = true
    

Newtonsoft 的 DefaultContractResolver 是否像 XML 那样与这些“指定”字段不完全兼容?如果每个属性的“指定”值为真,我是否应该明确说明每个属性? 还是我错过了什么?

编辑: 我已经上传了一些示例代码到 GitHub:https://github.com/AndreNobrega/XML-JSON-Serialization-POC

我尝试发送的请求正文可以在项目的示例文件夹中找到。 POST 请求可以发送到 .../api/Person。 在发送 XML 示例时,我将 Content-Type 标头设置为 application/xml。发送 JSON 示例时,我将其设置为 application/json

如果您在 PersonController 类的 Post() 方法中设置断点,您将看到 XML 请求的 xxxSpecified 成员设置正确,但 JSON 不正确。

也许它与由 xsd2code++ 自动生成的 Person.Designer 类有关? [System.Xml.Serialization.XmlElementAttribute(Form = System.Xml.Schema.XmlSchemaForm.Unqualified)] 属性是否有 JSON 等效项?

【问题讨论】:

Json.Net 支持开箱即用的Specified 属性约定,如here 所示。您要反序列化的实际 JSON 是什么?它与你的类的形状相匹配吗? @BrianRogers 我要反序列化的 JSON 是 OP 中的最后一段代码,它确实与我的类匹配。我的代码和你的代码之间的区别在于,在到达控制器之前,反序列化是由 JsonMediaTypeFormatter(倒数第二个代码块中的那个)完成的,而不是调用 JsonConvert.DeserializeObject()。 Json.NET 绝对支持xxxIsSpecified 模式,参见例如How to force Newtonsoft Json to serialize all properties? (Strange behavior with “Specified” property) 和 XSD.EXE + JSON.NET - How to deal with xxxSpecified generated members? 例如问题。 你可以检查一下你是否在某个地方设置了DefaultContractResolver.IgnoreIsSpecifiedMembers 有没有机会在Person 构造函数中预先分配Details?如果这样做,Json.NET 将填充预先存在的实例而不设置新实例,因此似乎 DetailsSpecified 永远不会被设置。见dotnetfiddle.net/0taaIn。为了比较,XmlSerializer 从不填充现有的非集合类型实例。 【参考方案1】:

您似乎在 Json.NET 对 propertyNameSpecified members 的支持中遇到了限制:在填充 预分配 引用类型属性的实例时未设置 propertyNameSpecified 属性。 作为一种解决方法,您可以使用设置JsonSerializerSettings.ObjectCreationHandling = ObjectCreationHandling.Replace 进行反序列化。如果这样做,序列化程序将创建引用类型属性的新实例并在创建后设置回,从而切换相应的propertyNameSpecified 属性。

下面有详细的解释。在您的 Person 类型中,您会在默认构造函数中自动分配子属性 AddressDetails 的实例:

public Person()

    this._address = new Address();
    this._details = new PersonalDetails();

现在,由于 Json.NET 支持 populating an existing object,在反序列化期间,在调用默认的 Person() 构造函数后,它将填充您构造的 AddressDetails 的值,而不是创建新的值。正因为如此,它显然从不调用 AddressDetails 的设置器,也许是因为 Newtonsoft 认为没有必要这样做。但是,反过来,这似乎阻止了设置相应的Specified 属性,因为 Json.NET 似乎仅在调用 setter 时才切换它们。

(作为比较,XmlSerializer 从不填充集合值属性以外的预分配引用类型属性,因此XmlSerializer 不会出现这种情况。)

这可能是 Json.NET 的 propertyNameSpecified 模式实现中的一个错误。您可能想通过 Newtonsoft 联系 open an issue。

演示小提琴 #1 here.

作为一种解决方法,您可以:

使用设置JsonSerializerSettings.ObjectCreationHandling = ObjectCreationHandling.Replace 反序列化,如下所示:

config.Formatters.JsonFormatter.SerializerSettings.ObjectCreationHandling = ObjectCreationHandling.Replace;

此选项将始终创建新对象,从而触发Specified 属性的设置。

演示小提琴#2 here.

Person 的默认构造函数中删除AddressDetails 的分配。不太推荐,但确实可以解决问题。

演示小提琴#3 here.

【讨论】:

config.Formatters.JsonFormatter.SerializerSettings.ObjectCreationHandling = ObjectCreationHandling.Replace; 添加到格式化程序设置似乎使它的行为就像 XML 格式化程序一样,而不需要对模型进行任何更改。这应该可以了,谢谢!

以上是关于如何让 Json.NET 为具有复杂值的属性设置 IsSpecified 属性?的主要内容,如果未能解决你的问题,请参考以下文章

如何在 json 序列化时使用 DefaultContractResolver 覆盖具有字符串值的复杂类型属性

如何让 emmet 将具有值的属性添加到 div 标签中?

为具有闭包默认值的属性设置委托

使用带有 TypeNameHandling 标志的 Json.NET 序列化具有 IConvertible 值的字典

如何使用 Json.Net 序列化/反序列化具有附加属性的自定义集合

使用 json.net 中的属性序列化 DataSet/DataTable