Search code examples
c#xmlserializationxmlserializer

XmlSerializer sets null to property after self-closed tag defined as XmlElement


Imagine having XML structure like the following:

[XmlRoot("Foo")]
public class Foo
{
    [XmlElement("Bar")]
    public Bar Bar { get; set; }
    [XmlElement("SuperImportant")]
    public SuperImportant SuperImportant { get; set; }
}

[XmlRoot("Bar")]
public class Bar
{
    [XmlElement("Baz")]
    public XmlElement Baz { get; set; }
}

[XmlRoot("SuperImportant")]
public class SuperImportant
{
    [XmlElement("MegaImportant")]
    public string MegaImportant { get; set; }
}

Baz defined as XmlElement for some reason.

Now check this code:

var template = @"
<Foo>
  <Bar>
    {0}
  </Bar>
  <SuperImportant>
    <MegaImportant>42</MegaImportant>
  </SuperImportant>
</Foo>";

var selfClosed = new StringReader(String.Format(template, "<Baz/>"));    

var openClosePair = new StringReader(String.Format(template, "<Baz></Baz>"));

XmlSerializer xmlSerializer = new XmlSerializer(typeof(Foo));

var o1 = (Foo)xmlSerializer.Deserialize(selfClosed);
Console.WriteLine(o1.SuperImportant == null); // True, it's not there

var o2 = (Foo)xmlSerializer.Deserialize(openClosePair);
Console.WriteLine(o2.SuperImportant == null); // False, it's there

As you can see, if some tag, defined as XmlElement in class definition appears to be self closed, the element right after its parent tag is nulled. How can I configure XmlSerializer to treat self-closed tags as open-close pairs? SuperImportant should be deserialized in both cases, but it's not in the former, which is wrong.


Solution

  • You should mark the property Baz with [XmlAnyElement("Baz")] like so:

    [XmlRoot("Bar")]
    public class Bar
    {
        [XmlAnyElement("Baz")]
        public XmlElement Baz { get; set; }
    }
    

    As explained in the docs, this attribute

    Specifies that the member (a field that returns an array of XmlElement or XmlNode objects) contains objects that represent any XML element that has no corresponding member in the object being serialized or deserialized.
    ...
    You can also apply the XmlAnyElementAttribute to a field that returns a single XmlElement object. If you do so, you must use the properties and methods of the XmlElement class to recursively iterate through the unknown elements.

    Once the attribute is applied, the Baz property will correctly capture both the <Baz/> and <Baz></Baz> elements without interfering with deserialization of subsequent node(s). It does seem odd that [XmlElement("Baz")] causes the inconsistency you are seeing, but since [XmlAnyElement("Baz")] is designed to handle this situation it should be used instead.

    Sample working .Net fiddle here.