Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / desktop / WinForms

Serialize and Deserialize IEnumerable Objects

4.68/5 (29 votes)
11 Dec 2006CPOL10 min read 3   2.4K  
CustomXmlSerializer is an alternative to XmlSerializer, supporting both shallow and deep serialization of ArrayLists, Collections, and Dictionaries.

Introduction

The process of converting the data in an object to XML is called serialization. The reverse of serialization, loading XML into the object, is called deserialization. Shallow serialization converts the public properties and fields into XML while deep serialization converts both public and private members into XML. System.Xml.Serialization.XmlSerializer is used to perform shallow serialization.

As much as I like using the XmlSerializer, the limitations it has are enough to drive a man crazy. XmlSerializer doesn't support most of the types defined in the System.Collections namespace. I rarely write code that requires serialization without one or more of the collection types! Fed up with these limitations, I finally took the time to create a custom serializer called CustomXmlSerializer. This article demonstrates how to use CustomXmlSerializer to serialize/deserialize various IEnumerable list types.

Background

If all you are interested in doing is serializing CollectionBase abstract classes, then you may want to view the article, "Serialize custom collections of CollectionBase".

Capabilities

CustomXmlSerializer is intended to support serialization/deserialization of any object.

CustomXmlSerializer was specifically designed to support the following IEnumerable array types:

  • System.Array
  • System.Collections.ArrayList
  • System.Collections.BitArray
  • System.Collections.CollectionBase
  • System.Collections.DictionaryBase
  • System.Collections.Hashtable
  • System.Collections.Queue
  • System.Collections.SortedList
  • System.Collections.Stack

There is limited support for the types defined in System.Collections.Generic.

In order to support both serialization and deserialization for the System.Collections namespace, CustomXmlSerializer makes the following assumptions:

  • The Add method and the Item property will be exposed from abstract classes based on an ICollection, IList, ArrayList, BitArray, CollectionBase, DictionaryBase, HashTable, or SortedList object.
  • The Enqueue and Peek methods will be exposed from abstract classes based on the Queue object.
  • The Push and Peek methods will be exposed from abstract classes based on the Stack object.

Limitations

IDictionary

When the IncludeClassNameAttribute is set to False, CustomXmlSerializer uses Reflection in an attempt to detect the variable type that will be added to the collection. A class that implements the IDictionary interface always returns a DictionaryEntry object which always has a value of type Object.

In order for CustomXmlSerializer to correctly deserialize the XML, it must know what data type to instantiate. The class name provides the instructions to instantiate the correct object. When using any collection that implements the IDictionary interface, set the property IncludeClassNameAttribute to True during serializiation.

Queue

Like the IDictionary classes, when serializing a Queue object, you must set the IncludeClassNameAttribute to True.

Structures

CustomXmlSerializer is unable to deserialize structures. Structures are boxed, and this makes them very difficult to deserialize. If deserialization is required, use a class instead of a structure.

Generics

CustomXmlSerializer provides limited support for generics. CustomXmlSerializer can serialize abstract generic classes, but not the base generic class. For example, System.Collections.Generic.Dictionary can't be serialized as a variable. But, if an abstract class is created that inherits from System.Collections.Generic.Dictionary, then serialization and deserialization work just fine.

From the perspective of serialization, it is best to use standard objects for data classes. The classes that need to be serialized should be well defined classes. Although generics give us the ability to create type-safe reusable classes, I wouldn't consider them well defined. The type of data a generic stores isn't official until run-time, and from the generic's perspective, each instantiation can be defined as a different type.

The use of generics makes more sense for helper classes where you need to support different types with the same code. I have come to this conclusion after having seen that there aren't any good ways to serialize generics referencing other generics. The use of generic data objects forces the developer to support serialization with the IXmlSerializable interface. The best articles I have found so far discussing serializing generics is DevX's "Serializing, Consuming, and Binding Generics in .NET 2.0", and Microsoft's "An Introduction to C# Generics".

Before I get any hate mail on this topic, I am not saying that you shouldn't use generics. The concept of generics is a powerful new addition to the VS.NET developer's arsenal. I'm just saying don't go generic crazy just because you can. If you need to support serialization, you are better off avoiding generic classes.

CustomXmlSerializer Members

CustomXmlSerializer serializes and deserializes objects into and from XML. Write operations serialize the object into various target mediums. Read operations deserialize the object from various source mediums.

Figure 1: CustomXmlSerializer Class Diagram

Figure 1: CustomXMLSerializer Class Diagram

Table 1: CustomXmlSerializer Property Members
PropertyDescription
CDataStorageSerialize all string values into XML CData tags.
IncludeClassNameAttributeRecord the name of the class when serializing to ensure that the class can be deserialized.
IgnoreWarningsIgnore warnings, allowing deserialization to continue when an unsupported type is encountered.
MethodDefines how to serialize/deserialize the class. Supports the following values: SerializationMethod.Shallow (default) or SerializationMethod.Deep.
Table 2: CustomXmlSerializer Method Members
MethodParametersDescription
ReadXML Overloaded routine.Deserializes the XML into the target object.
ByVal reader As System.Xml.XmlReader, ByVal target As ObjectLoads the XML from the specified reader into the specified target.
ByVal node As System.Xml.XmlNode, ByVal target As ObjectLoads the XML from the specified XmlNode into the specified target.
ByVal document As System.Xml.XmlDocument, ByVal target As ObjectLoads the XML from the specified document into the specified target.
ByVal path As String, ByVal target As ObjectLoads the XML from the specified file into the specified target.
ByVal text As System.Text.StringBuilder, ByVal target As ObjectLoads the XML from the specified StringBuilder into the specified target.
WriteDocumentByVal source As ObjectSerializes the source object into an XmlDocument following "Shallow Copy" business logic.
WriteFileByVal source As Object, ByVal path As String, Optional ByVal replaceFile As Boolean = FalseSerializes the source object into a file following "Shallow Copy" business logic.
WriteStringByVal source As ObjectSerializes the source object into a string following "Shallow Copy" business logic.
WriteTextByVal source As ObjectSerializes the source object into a StringBuilder following "Shallow Copy" business logic.
WriteXMLByVal source As Object, ByVal writer As System.Xml.XmlWriter, Optional ByVal propertyName As String = NothingSerializes the source object into an XmlWriter following "Shallow Copy" business logic.

Using CustomXmlSerializer

The following example demonstrates how to make serializable data objects for use with a Web Service using CustomXmlSerializer.

Example Data Object Model

Several data objects were created to demonstrate how CustomXmlSerializer works (see Figure 2). The example has been modeled to show how the serializer can handle several layers of object relationships.

Figure 2: Example Data Objects Class Diagram

Figure 2: Example Data Objects Class Diagram

The Buyer object has to implement the System.XML.Serialization.IXmlSerializable interface because some of the class types in the diagram can't be serialized by System.XML.Serialization.XmlSerializer. Normally, a great deal of work would need to be performed to support the interface. But, using CustomXmlSerializer, we can reduce hundreds of lines of complex code down to just four lines of code.

Listing 1: Supporting IXmlSerializable
VB
<Serializable()> Public Class Buyer_
      Implements System.Xml.Serialization.IXmlSerializable

'Download Demo to View Class Properties

#Region "Implements IXmlSerializable"
    Public Function GetSchema() As System.Xml.Schema.XmlSchema _
    Implements System.Xml.Serialization.IXmlSerializable.GetSchema
        Return Nothing
    End Function

    Public Sub ReadXml(ByVal reader As System.Xml.XmlReader) _
    Implements System.Xml.Serialization.IXmlSerializable.ReadXml
        Dim _XMLReader As New CustomXmlSerializer
        _XMLReader.ReadXML(reader, Me)
    End Sub

    Public Sub WriteXml(ByVal writer As System.Xml.XmlWriter) _
    Implements System.Xml.Serialization.IXmlSerializable.WriteXml
        Dim _XMLWriter As New CustomXmlSerializer
        _XMLWriter.WriteXML(Me, writer)
    End Sub
#End Region

Loading the Object Model

Before the serialization capabilities can be demonstrated, a Buyer class has to be created and loaded with default values.

Listing 2: Loading the Example Data Objects
VB
Private Function CreateBuyer() As Buyer
    Dim _Person As New Buyer

    _Person.Identifier = 24
    _Person.Name = "Joe Schmoe"
    _Person.EMailAddress = "JoeSchmoe@FutureHomeOwners.com"
    _Person.PhoneNumber = "666-777-8888"

    Dim _Question As Profile.Question

    _Question = New Profile.Question
    _Question.Identifier = 1
    _Question.Text = "Target Price Range"
    _Question.Tip = "Select the price range that you" & _ 
                    " are able/willing to pay for a new home"
    _Question.Type = QuestionTypes.SelectOne
    _Question.Options = New System.Collections.ArrayList
    _Question.Options.Add("$200,000 - $250,000")
    _Question.Options.Add("$250,000 - $300,000")
    _Question.Options.Add("$300,000 - $500,000")
    _Question.Options.Add("$500,000 or More")
    _Question.OtherOptions = New System.Collections.ArrayList
    _Question.OtherOptions.Add("Test")
    _Question.OtherOptions.Add(CType(1, Integer))
    _Question.OtherOptions.Add(CType(1.1, Double))
    ReDim _Question.TestArray(2)
    _Question.TestArray(0) = "Test Array 1"
    _Question.TestArray(1) = "Test Array 2"

    _Person.Questionnaire.Questions.Add(_Question)

    _Question = New Profile.Question
    _Question.Identifier = 2
    _Question.Text = "Building Style"
    _Question.Tip = "Identify the styles that you like"
    _Question.Type = QuestionTypes.SelectMany
    _Question.Options = New System.Collections.ArrayList
    _Question.Options.Add("Modern")
    _Question.Options.Add("Gothic")
    _Question.Options.Add("European")
    _Question.Options.Add("Spanish")
    _Question.Options.Add("Colonial")
    
    _Person.Questionnaire.Questions.Add(_Question)

    _Question = New Profile.Question
    _Question.Identifier = 3
    _Question.Text = "Special Requests"
    _Question.Tip = "Document any additional needs/wants that we should consider"
    _Question.Type = QuestionTypes.Text
    
    _Person.Questionnaire.Questions.Add(_Question)

    Return _Person
End Function

Serializing the Object Model into XML

The loaded Buyer object is then serialized into a StringBuilder and the results are displayed. You have the option of manually serializing the class as shown in Listing 3, or you can use the helper method, WriteText, as shown in Listing 4.

Listing 3: Serializing the Data Objects Manually
VB
'Instantiate and Load the Buyer Object
Dim _Person As Buyer = CreateBuyer()

'Serialize the Buyer Object into XML
Dim _xmlText As New System.Text.StringBuilder
Dim _TextStreamWriter As New System.IO.StringWriter(_xmlText)
Dim _xmlWriter As New System.Xml.XmlTextWriter(_TextStreamWriter)
_Person.WriteXml(_xmlWriter)

'Display the XML Results
MsgBox(_xmlText.ToString, MsgBoxStyle.OKOnly, "Serialization Completed")
Listing 4: Serializing the Data Objects Using the Helper Routine
VB
'Instantiate and Load the Buyer Object
Dim _Person As Buyer = CreateBuyer()

'Serialize the Buyer Object into XML

Dim _XMLWriter As New CustomXmlSerializer
Dim _xmlText As New System.Text.StringBuilder = _XmlWriter.WriteText(_Person)

'Display the XML Results
MsgBox(_xmlText.ToString, MsgBoxStyle.OKOnly, "Serialization Completed")

When you run the example, you will notice that the carriage returns and spaces are missing. CustomXmlSerializer excludes these characters to conserve space. The carriage returns and spaces were manually added to the example output below to make it easier to read.

In the generated output, you can see that all classes and properties become elements, with their values inside the element tags. Attributes are used by CustomXmlSerializer to identify how to deserialize the data back into the target objects. One of the attributes used by the deserialization logic is the className attribute.

The className is used to help the deserializer understand what object to instantiate. The className attribute is only required when you have different value types stored in the same list. The OtherOptions tag shows an example requiring the className because of the various child class types.

If the className attribute is excluded, the deserializer attempts to instantiate the child classes based on the element tag name of the first child element. Using this approach, the deserializer expects all children to be of the same type. The Options tag represents an example that would work without class names in the child elements.

The TestArray element shows how a string variable array (System.Array type) was serialized. This demonstrates that CustomXmlSerializer can serialize and deserialize array values.

Listing 5: Example Output from Serialized Data Objects
XML
<Buyer className="Example.Buyer">
  <Identifier>24</Identifier>
  <Name>Joe Schmoe</Name>
  <EMailAddress>JoeSchmoe@FutureHomeOwners.com</EMailAddress>
  <PhoneNumber>666-777-8888</PhoneNumber>
  <Questionnaire className="Example.Profile.Questionnaire">
    <Questions className="Example.Profile.Questions">
      <Question className="Example.Profile.Question">
        <OtherOptions className="System.Collections.ArrayList">
          <String className="System.String">Test</String>
          <Int32 className="System.Int32">1</Int32>
          <Double className="System.Double">1.1</Double>
        </OtherOptions>
        <TestArray size="3" className="System.Array" type="System.String">
          <System.Array.Item point="0">
            <String className="System.String">Test Array 1</String>
          </System.Array.Item>
          <System.Array.Item point="1">
            <String className="System.String">Test Array 2</String>
          </System.Array.Item>
        </TestArray>
        <Identifier>1</Identifier>
        <Text>Target Price Range</Text>
        <Tip>Select the price range that you are able/willing to pay for a new home</Tip>
        <Type>SelectOne</Type>
        <Options className="System.Collections.ArrayList">
          <String className="System.String">$200,000 - $250,000</String>
          <String className="System.String">$250,000 - $300,000</String>
          <String className="System.String">$300,000 - $500,000</String>
          <String className="System.String">$500,000 or More</String>
        </Options>
        <TestArray2 className="System.Collections.ArrayList">
          <String className="System.String">Test</String>
          <Int32 className="System.Int32">1</Int32>
          <Double className="System.Double">1.1</Double>
        </TestArray2>
    </Question>
      <Question className="Example.Profile.Question">
        <OtherOptions />
        <TestArray size="1" className="System.Array" type="System.String">
          <System.Array.Item point="0">
            <String className="System.String" />
          </System.Array.Item>
        </TestArray>
        <Identifier>2</Identifier>
        <Text>Building Style</Text>
        <Tip>Identify the styles that you like</Tip>
        <Type>SelectMany</Type>
        <Options className="System.Collections.ArrayList">
          <String className="System.String">Modern</String>
          <String className="System.String">Gothic</String>
          <String className="System.String">European</String>
          <String className="System.String">Spanish</String>
          <String className="System.String">Colonial</String>
        </Options>
        <TestArray2 />
      </Question>
      <Question className="Example.Profile.Question">
        <OtherOptions />
        <TestArray size="1" className="System.Array" type="System.String">
          <System.Array.Item point="0">
            <String className="System.String" />
          </System.Array.Item>
        </TestArray>
        <Identifier>3</Identifier>
        <Text>Special Requests</Text>
        <Tip>Document any additional needs/wants that we should consider</Tip>
        <Type>Text</Type>
        <Options />
        <TestArray2 />
      </Question>
    </Questions>
    <Responses className="Example.Profile.Responses" />
    <Identifier>0</Identifier>
    <DateCreated>1/1/0001 12:00:00 AM</DateCreated>
    <DateModified>1/1/0001 12:00:00 AM</DateModified>
    <State>Created</State>
    <ChangeCount>0</ChangeCount>
  </Questionnaire>
  <PreferredPlans />
  <SelectedPlan />
</Buyer>

Deserializing the XML into the Object Model

To deserialize the XML back into the Buyer object, we create a new Buyer. Next, the XML string value is read back into the buyer. You can manually deserialize the object as shown in Listing 6, or use the helper routine, ReadXml, as shown in Listing 7.

Listing 6: Deserializing the XML back into the Example Data Objects Manually
VB
_Person = New Buyer

Dim _textStreamReader As New System.IO.StringReader(_xmlText.ToString)
Dim _xmlReader As New System.Xml.XmlTextReader(_textStreamReader)
_Person.ReadXml(_xmlReader)
Listing 7: Deserializing the XML back into the Example Data Objects Using the Helper Routine
VB
_Person = New Buyer
Dim _XMLReader As New CustomXmlSerializer
_Person = _XMLReader.ReadXml(_xmlText, _Person)

Points of Interest

Using Reflection to serialize classes was fairly straightforward until generics entered the picture. From the very beginning, deserialization was very difficult to implement.

I ran into problems with identifying what assembly contained the classes needing to be instantiated. To get around the problem, I used the assembly of the parent class. Therefore, all classes to be deserialized will need to be in the same assembly.

Additionally, dictionaries support key/value pairs. To associate the key with the value, the serializer records the key as an attribute of the value's element tag.

History

DateVersion #Change History
06/17/20061.0

Initial release of the VB version.

  • Supports serialization and deserialization of all System.Collection list types.
  • Supports serialization of System.Array.
06/30/20061.1

Bug Fixes (for details, refer to inline "Fix" comments)

  • Values of Nothing/null are no longer assigned. Assigning a value of Nothing/null was unreliable for some of the data types.
  • Added code to verify if an IEnumerable was deserialized.

New Features

  • Initial release of C# version.
  • Supports both custom VB.NET and C# data types.
  • If class definitions match, supports [serializing/deserializing] C# [to/from] VB.NET.
  • Code changes to the VB.NET version to simplify conversion to C#, and continued maintenance in both languages.
  • In addition to shallow serialization, now supports deep serialization through the Method member.
  • Supports 2003 and 2005 without any code changes.
  • Deserializing VS 2003 [to/from] VS 2005 supported.
07/06/20061.2
  • Added ability to deserialize System.Array types.
07/13/20061.3
  • Fixed a few bugs associated with System.Array serialization/deserialization.
  • Added support for serializing/deserializing IEnumerable child classes.
08/01/20061.4
  • Fixed a problem that occurred when converting the VB code into C#.
  • The routine SaveValue has been modified in the C# version. Special thanks to a friend who pointed out the issue!
12/09/20061.5
  • Fixed problem with deserializing collections that implement the IDictionary interface.
  • Added support for XmlIgnoreAttribute attribute.
12/12/20061.6
  • Fixed problem with deserializing Queue collections.

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)