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)]
?
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.