Search code examples
c#c#-4.0serializationxml-serializationxmlserializer

When to use ShouldSerializeXXX vs. XmlIgnoreAttribute for XML serialization


I have looked for examples to ignore a property of a class during xml serialization and deserialization. I have found three different methods and can't figure out, when they should be used. My special interest is, which one works with XmlSerializer better.

  1. XmlIgnore attribute

    public class Item
    {
        [XmlIgnore]
        public string Name { get; set; }
    }
    
  2. Method beginning with ShouldSerialize...

    public class Item
    {
        public string Name { get; set; }
    
        public bool ShouldSerializeName()
        {
            return false;
        }
    }
    
  3. NonSerialized attribute

    public class Item
    {
        [NonSerialized]
        public string Name { get; set; }
    }
    

Where there is some explanation about the difference between XmlIgnoreAttribtue and NonSerializedAttribute on stackoverflow and msdn, I was not able to find information about when to use XmlIgnoreAttribtue and when the ShouldSerializeXXX pattern. I tried both of them with the XmlSerializer and both of them see work as expected.


Solution

  • The basic difference between #1 and #2 is that they generate different XML Schemas. If you want a member to be excluded from your type's schema, use [XmlIgnore]. If you want a member to be included conditionally, use ShouldSerializeXXX() or XXXSpecified. (Finally, as stated in this answer, [NonSerialized] in option #3 is ignored by XmlSerializer.)

    To see the difference between #1 and #2, you can use xsd.exe to generate schemas for your types. The following schema is generated for version #1 and completely omits the Name member:

    <xs:complexType name="Item" />
    

    While the following for #2 conditionally includes the Name member:

    <xs:sequence>
      <xs:element minOccurs="0" maxOccurs="1" name="Name" type="xs:string" />
    </xs:sequence>
    

    The difference arises because XmlSerializer and xsd.exe both perform static type analysis rather than dynamic code analysis. Neither tool can determine that the Name property in case #2 will always be skipped, because neither tool attempts to decompile the source code for ShouldSerializeName() to prove it always returns false. Thus Name will appear in the schema for version #2 despite never appearing in practice. If you then create a web service and publish your schema with WSDL (or simply make them available manually), different clients will be generated for these two types -- one without a Name member, and one with.

    An additional complexity can arise when the property in question is of a non-nullable value type. Consider the following three versions of Item. Firstly, a version with an unconditionally included value property:

    public class Item
    {
        public int Id { get; set; }
    }
    

    Generates the following schema with Id always present:

      <xs:complexType name="Item">
        <xs:sequence>
          <xs:element minOccurs="1" maxOccurs="1" name="Id" type="xs:int" />
        </xs:sequence>
      </xs:complexType>
    

    Secondly, a version with an unconditionally excluded value property:

    public class Item
    {
        [XmlIgnore]
        public int Id { get; set; }
    }
    

    Generates the following schema that completely omits the Id property:

      <xs:complexType name="Item" />
    

    And finally a version with a conditionally excluded value property:

    public class Item
    {
        public int Id { get; set; }
    
        public bool ShouldSerializeId()
        {
            return false;
        }
    }
    

    Generates the following schema with Id only conditionally present:

      <xs:complexType name="Item">
        <xs:sequence>
          <xs:element minOccurs="0" maxOccurs="1" name="Id" type="xs:int" />
        </xs:sequence>
      </xs:complexType>
    

    Schema #2 is as expected, but notice there is a difference between #1 and #3: the first has minOccurs="1" while the third has minOccurs="0". The difference arises because XmlSerializer is documented to skip members with null values by default, but has no similar logic for non-nullable value members. Thus the Id property in case #1 will always get serialized, and so minOccurs="1" is indicated in the schema. Only when conditional serialization is enabled will minOccurs="0" be generated. If the third schema is in turn used for client code generation, an IdSpecified property will be added to the auto-generated code to track whether the Id property was actually encountered during deserialization:

    public partial class Item {
    
        private int idField;
    
        private bool idFieldSpecified;
    
        /// <remarks/>
        public int Id {
            get {
                return this.idField;
            }
            set {
                this.idField = value;
            }
        }
    
        /// <remarks/>
        [System.Xml.Serialization.XmlIgnoreAttribute()]
        public bool IdSpecified {
            get {
                return this.idFieldSpecified;
            }
            set {
                this.idFieldSpecified = value;
            }
        }
    }
    

    For more details on binding to conditionally serialized value members, see XML Schema Binding Support: MinOccurs Attribute Binding Support and ShouldSerialize*() vs *Specified Conditional Serialization Pattern.

    So that's the primary difference, but there are secondary differences as well that may influence which you choose:

    • [XmlIgnore] cannot be overridden in a derived class, but ShouldSerializeXXX() can be when marked as virtual; see here for an example.

    • If a member cannot be serialized by XmlSerializer because, for instance, it refers to a type that lacks a parameterless constructor, then marking the member with [XmlIgnore] will allow the containing type to be serialized - while adding a ShouldSerializeXXX() { return false; } will NOT allow the containing type to be serialized, since as stated previously XmlSerializer only performs static type analysis. E.g. the following:

       public class RootObject
       {
           // This member will prevent RootObject from being serialized by XmlSerializer despite the fact that the ShouldSerialize method always returns false.
           // To make RootObject serialize successfully, [XmlIgnore] must be added.
           public NoDefaultConstructor NoDefaultConstructor { get; set; }
      
           public bool ShouldSerializeNoDefaultConstructor() { return false; }
       }
      
       public class NoDefaultConstructor
       {
           public string Name { get; set; }
           public NoDefaultConstructor(string name) { this.Name = name; }
       }
      

      cannot be serialized by XmlSerializer.

    • [XmlIgnore] is specific to XmlSerializer, but ShouldSerializeXXX() is used by other serializers including Json.NET and protobuf-net.

    • As mentioned in comments, renaming a conditionally serialized property in Visual Studio does not automatically rename the corresponding ShouldSerializeXXX() method name, leading to potential maintenance gotchas down the road.