Sunday, 17 October 2010

Default Values Don't Get Serialised

The Problem

I came across some unusual behaviour during XML Serialisation recently and thought I'd share both the problem and my solution to it.

To demonstrate the behaviour I've created a small console application which serialises two Book objects to the Console. I've marked the default value of the Author property as "Anonymous", which seems reasonable enough. The second of the two Book objects specifically has its Author property set to "Anonymous", which it doesn't really need to have - as that's the default value.

Here's the code:

using System;
using System.ComponentModel;
using System.IO;
using System.Xml.Serialization;

namespace ConsoleApplication
{
  public class Program
  {
    static void Main(string[] args)
    {
      // create some Books
      Book[] books = new Book[]
      {
        new Book { Title = "The Road Ahead", Author = "Bill Gates", Isbn13 = "978-0670859139" },
        new Book { Title = "Beowulf", Author = "Anonymous", Isbn13 = "978-1588278296" },
      };

      // serialise it into a MemoryStream
      XmlSerializer xmlSerializer = new XmlSerializer(typeof(Book[]));
      MemoryStream memoryStream = new MemoryStream();
      xmlSerializer.Serialize(memoryStream, books);
    
      // write the contents of the MemoryStream to the Console
      memoryStream.Position = 0L;
      StreamReader streamReader = new StreamReader(memoryStream);
      Console.WriteLine(streamReader.ReadToEnd());

      // wait for user to hit ENTER
      Console.ReadLine();
    }
  }

  public class Book
  {
    [XmlElement("title")]
    public string Title { get; set; }

    [XmlElement("author")]
    [DefaultValue("Anonymous")]
    public string Author { get; set; }
    
    [XmlElement("isbn13")]
    public string Isbn13 { get; set; }
  }
}

When I run the above, something odd happens. The Author property is not serialised for the second book, as you'll see below. It turns out that this is by design. The theory is that if the default value has been used, then there's no need to explicitly output it - a consumer of the data will simply see that the element is missing and infer the default value.

<?xml version="1.0"?>
<ArrayOfBook xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema">
  <Book>
    <title>The Road Ahead</title>
    <author>Bill Gates</author>
    <isbn13>978-0670859139</isbn13>
  </Book>
  <Book>
    <title>Beowulf</title>
    <isbn13>978-1588278296</isbn13>
  </Book>
</ArrayOfBook>

The problem, obviously, is that this approach assumes that consumer is aware that a default value exists. Can you see any indication from the serialised XML that a default value exists, or that an element has been suppressed because it's value was equal to the default value? No - that information is hidden from the consumer. We're simply left to hope that the consumer is aware of this default value.

The Solution

Well, the obvious solution is to simply remove the Default attribute from the Author property. But what if you can't? It's perfectly possible that you're serialising an object to which you do not have the source code. In my case I was serialising an object where the source code had been auto-generated from an series of XSDs - and was 43,600 lines long, and contained 72 default constraints. If I can avoid editing auto-generated code I always will. So how?

Well, although I'd never used it before, there's an overload to the XmlSerializer which allows you to provide a series of overrides for the serialisation attributes. Unfortunately, it overrides all of the serialisation attributes for a given type and member - you can't just say "use this Default attribute, but leave all other attributes alone". This means we need to re-state the XmlElement attribute to specify a name of "author" (otherwise you'll get the default of "Author"). The call to the XmlSerializer's constructor thus becomes:

      XmlAttributes bookAuthorXmlAttributes = new XmlAttributes();
      bookAuthorXmlAttributes.XmlElements.Add(new XmlElementAttribute("author"));
      XmlAttributeOverrides xmlAttributeOverrides = new XmlAttributeOverrides();
      xmlAttributeOverrides.Add(typeof(Book), "Author",  bookAuthorXmlAttributes);
      XmlSerializer xmlSerializer = new XmlSerializer(typeof(Book[]), xmlAttributeOverrides);

The output this time is:

<?xml version="1.0"?>
<ArrayOfBook xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema">
  <Book>
    <title>The Road Ahead</title>
    <author>Bill Gates</author>
    <isbn13>978-0670859139</isbn13>
  </Book>
  <Book>
    <title>Beowulf</title>
    <author>Anonymous</author>
    <isbn13>978-1588278296</isbn13>
  </Book>
</ArrayOfBook>

Perfect. But it's a shame we've had to re-state the XmlElement attribute isn't it? It would be better if we could inspect the attributes which were already present and add all of these in the XmlAttributeOverrides, with the exception of the Default attribute we're trying to remove. That'll be the subject of my next post.

See Also

No comments:

Post a Comment