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

1

2

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

Enswathe answered 10/2, 2021 at 19:50 Comment(8)
Json.Net supports the Specified property convention out of the box, as shown here. What is the actual JSON you are deserializing? Does it match the shape of your classes?Dia
@BrianRogers The JSON I'm deserializing is the last block of code in the OP, and it does match my classes. The difference between my code and yours, is that instead of calling JsonConvert.DeserializeObject(), the deserialization is done by a JsonMediaTypeFormatter (the one in the penultimate code block), before arriving at the controller.Enswathe
Still can't reproduce if I use JsonMediaTypeFormatter.ReadFromStream() to deserialize, see dotnetfiddle.net/zWBnxW. Might you please edit your question to share a minimal reproducible example?Thermodynamics
The xxxIsSpecified pattern is definitely supported by Json.NET, see e.g. How to force Newtonsoft Json to serialize all properties? (Strange behavior with “Specified” property) and XSD.EXE + JSON.NET - How to deal with xxxSpecified generated members? for example questions.Thermodynamics
You might check to see whether you are setting DefaultContractResolver.IgnoreIsSpecifiedMembers somewhere.Thermodynamics
Is there any chance you preallocate Details in the Person constructor? If you do, Json.NET will populate the pre-existing instance and not set back a new instance, and so it seems DetailsSpecified never gets set. See dotnetfiddle.net/0taaIn. For comparison XmlSerializer never populates existing instances of non-collection types.Thermodynamics
@Thermodynamics Thanks for the replies. I've added an example to the OP, to better illustrate what I'm trying to do.Enswathe
@Thermodynamics I have tried setting DefaultContractResolver.IgnoreIsSpecifiedMembers, but as it's to be expected, the formatter then returns all xxxSpecified as false. When xsd2code++ generates the classes, it does initialize Address and PersonalDetails (see /Models/Person.Designer.cs).Enswathe
T
1

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.

Thermodynamics answered 12/2, 2021 at 19:43 Comment(1)
Adding config.Formatters.JsonFormatter.SerializerSettings.ObjectCreationHandling = ObjectCreationHandling.Replace; to the formatter settings seems to make it behave just like the XML formatter, without needing any changes to the model. This should work, thank you!Enswathe

© 2022 - 2024 — McMap. All rights reserved.