Search code examples
asp.netjsonxmljson.netxsd2code

How to make Json.NET set IsSpecified properties for properties with complex values?


I have a web service built with ASP.Net, which until now only used XML for its input and output. Now it needs to also be able to work with JSON.

We use xsd2code++ to generate the model from a XSD, with the option to create "IsSpecified" properties enabled (i.e. if a property is specified in a XML, its respective "Specified" property will be true).

From a XSD like this...

<?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++ creates a class, with properties like this:

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;
        }
    }
}

This works great for XML. For example, if ID isn't specified in the input XML, the property IDSpecified will be false. We can use these "Specified" properties in the business logic layer, so we know what data has to be inserted/updated, and what can be ignored/skipped.

Then, we tried to add JSON serialization. We added a Json formatter to the WebApiConfig class:

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

The API will now recognize JSON inputs, but the "Specified" properties don't work for complex objects as they do for XML, and will always say they're 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
    }
}

Is Newtonsoft's DefaultContractResolver not fully compatible with these "Specified" fields, like XML is? Am I expected to explicitly state for each property if its "Specified" value is true? Or am I missing something?

EDIT: I've uploaded some sample code to GitHub: https://github.com/AndreNobrega/XML-JSON-Serialization-POC

The request bodies I've tried sending can be found in the Examples folder of the project. POST requests can be sent to .../api/Person. When sending the XML example, I set the Content-Type header to application/xml. When sending the JSON example, I set it to application/json.

If you set a breakpoint in the Post() method of the PersonController class, you will see that xxxSpecified members for XML requests are set correctly, but not for JSON.

Maybe it's got something to do with the Person.Designer class, that is auto-generated by xsd2code++? Is there a JSON equivalent for the attribute [System.Xml.Serialization.XmlElementAttribute(Form = System.Xml.Schema.XmlSchemaForm.Unqualified)]?


Solution

  • You appear to have encountered a limitation in Json.NET's support for {propertyName}Specified members: the {propertyName}Specified property is not set when populating an instance of a preallocated reference type property. As a workaround, you can deserialize with the setting JsonSerializerSettings.ObjectCreationHandling = ObjectCreationHandling.Replace. If you do, new instances of reference type properties will be created by the serializer and set back after creation, thereby toggling the corresponding {propertyName}Specified property.

    A detailed explanation follows. In your Person type, you automatically allocate instances of the child properties Address and Details in the default constructor:

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

    Now, because Json.NET supports populating an existing object, during deserialization, after calling your default Person() constructor, it will populate the values of Address and Details that you constructed, rather than creating new ones. And because of that, it apparently never calls the setters for Address and Details, perhaps because Newtonsoft assumed there was no need to do so. But that, in turn, seems to prevent the corresponding Specified properties from being set, as it appears Json.NET toggles them only when the setter is called.

    (For comparison, XmlSerializer never populates preallocated reference type properties other than collection-valued properties, so this situation situation should not arise with XmlSerializer.)

    This might be a bug in Json.NET's implementation of the {propertyName}Specified pattern. You might want to open an issue about it with Newtonsoft.

    Demo fiddle #1 here.

    As a workaround, you could:

    • Deserialize with the setting JsonSerializerSettings.ObjectCreationHandling = ObjectCreationHandling.Replace like so:

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

      This option will Always create new objects and thereby triggers the setting of Specified properties.

      Demo fiddle #2 here.

    • Remove allocation of Address and Details from the default constructor for Person. Not really recommended, but it does solve the problem.

      Demo fiddle #3 here.