Click here to Skip to main content
15,881,852 members
Articles / Mobile Apps

A Simple Serializer / Deserializer

Rate me:
Please Sign up or sign in to vote.
4.39/5 (17 votes)
22 Sep 20043 min read 106K   1K   64   14
A simple Serializer / Deserializer.

Introduction

I needed a simple serialization / deserialization mechanism to save and load state information for objects whose type (the class) is constructed and compiled at runtime (see my article on Declaratively Populating A PropertyGrid). Neither XmlSerializer nor BinaryFormatter classes handle this scenario. So, I wrote a lightweight assembly to handle my specific needs.

Serialization

The serializer consists of three steps:

  1. Initialize using the Start() method
  2. Serialize one or more objects to XML
  3. Finish the process with the Finish() method

Initialization

This is very straight forward. A MemoryStream and XmlTextWriter are constructed and the initial XML document is setup:

C#
public void Start()
{
    ms=new MemoryStream();
    xtw = new XmlTextWriter(ms, Encoding.UTF8);
    xtw.Formatting=Formatting.Indented;
    xtw.Namespaces=false;
    xtw.WriteStartDocument();
    xtw.WriteComment("Auto-Serialized");
    xtw.WriteStartElement("Objects");
}

The Serializer

The serializer does a few important things and has a few limitations:

  • It serializes only public properties and enumerations that are writeable.
  • It ignores arrays.
  • It looks for a [DefaultValue] attribute decorating the property, and if it exists and the property value equals the default value, the property is not serialized.
  • It uses a type converter to convert the property value to a string, rather than the ToString() method. This allows deserialization to work on Font, Color, and other classes that know how to serialize to a string.
  • It serializes only properties with the IsSerializable flag set in the property type.
  • It does not walk the object graph to serialize child classes.
C#
// simple property serialization
public void Serialize(object obj)
{
  Trace.Assert(xtw != null, "Must call Serializer.Start() first.");
  Trace.Assert(obj != null, "Cannot serialize a null object.");

  Type t=obj.GetType();
  xtw.WriteStartElement(t.Name);
  foreach(PropertyInfo pi in t.GetProperties())
  {
    Type propertyType=pi.PropertyType;
    // with enum properties, IsPublic==false, even if marked public!
    if ( (propertyType.IsSerializable) && (!propertyType.IsArray) &&
         (pi.CanWrite) && ( (propertyType.IsPublic) || (propertyType.IsEnum) ) )
    {
      object val=pi.GetValue(obj, null);
      if (val != null)
      {
        bool isDefaultValue=false;
        // look for a default value attribute.
        foreach(object attr in pi.GetCustomAttributes(false))
        {
          if (attr is DefaultValueAttribute)
          {
            // it exists--compare current value to default value
            DefaultValueAttribute dva=(DefaultValueAttribute)attr;
            isDefaultValue=val.Equals(dva.Value);
          }
        }

        // only non-default values or properties without a default value are 
        // serialized.
        if (!isDefaultValue)
        {
          // do a type conversion to a string, as this yields a
          // deserializable value, rather than what ToString returns.
          TypeConverter tc=TypeDescriptor.GetConverter(propertyType);
          if (tc.CanConvertTo(typeof(string)))
          {
            val=tc.ConvertTo(val, typeof(string));
            xtw.WriteAttributeString(pi.Name, val.ToString());
          }
          else
          {
            Trace.WriteLine("Cannot convert "+pi.Name+" to a string value.");
          }
        }
      }
      else
      {
        // null values not supported!
      }
    }
  }
  xtw.WriteEndElement();
}

Finisher

The Finish() method cleans up the text writer and returns the XML:

C#
public string Finish()
{
    Trace.Assert(xtw != null, "Must call Serializer.Start() first.");

    xtw.WriteEndElement();
    xtw.Flush();
    xtw.Close();
    Encoding e8=new UTF8Encoding();
    xml=e8.GetString(ms.ToArray(), 1, ms.ToArray().Length-1);
    return xml;
}

Deserialization

The deserializer expects that the instance has already been constructed. This is a very helpful shortcut to take, because constructing an object at runtime often requires a fully qualified assembly name, namespace, and other information. Furthermore, the type information for my runtime constructed classes is actually not available--only the instance is. For my particular requirement, this is not an issue.

Also, the deserializer will inspect each property for a default value and restore that value to the specified object unless it is being overridden in the XML.

Finally, when deserializing multiple objects, you must know the exact sequence that was used to serialize the objects, as an index pointing to the serialized object's element is passed in to the deserializer. Again, for my purposes, this restriction is not an issue.

C#
// simple property deserialization
public void Deserialize(object obj, int idx)
{
  Trace.Assert(doc != null, "Must call Deserializer.Start() first.");
  Trace.Assert(doc.ChildNodes.Count==3, "Incorrect xml format.");
  Trace.Assert(idx < doc.ChildNodes[2].ChildNodes.Count,
                 "No element for the specified index.");
  Trace.Assert(obj != null, "Cannot deserialize to a null object");

  // skip the encoding and comment, and get the indicated
  // child in the Objects tag
  XmlNode node=doc.ChildNodes[2].ChildNodes[idx];
  Type t=obj.GetType();
  Trace.Assert(t.Name==node.Name, "Object name does not match element tag.");

  // set all properties that have a default value and not overridden.
  foreach(PropertyInfo pi in t.GetProperties())
  {
    Type propertyType=pi.PropertyType;

    // look for a default value attribute.
    foreach(object attr in pi.GetCustomAttributes(false))
    {
      if (attr is DefaultValueAttribute)
      {
        // it has a default value
        DefaultValueAttribute dva=(DefaultValueAttribute)attr;
        if (node.Attributes[pi.Name] == null)
        {
          // assign the default value, as it's not being overridden.
          // this reverts the object's property back to the default
          pi.SetValue(obj, dva.Value, null);
        }
      }
    }
  }

  // now parse the xml attributes that are going to change property values
  foreach(XmlAttribute attr in node.Attributes)
  {
    string pname=attr.Name;
    string pvalue=attr.Value;
    PropertyInfo pi=t.GetProperty(pname);
    if (pi != null)
    {
      TypeConverter tc=TypeDescriptor.GetConverter(pi.PropertyType);
      if (tc.CanConvertFrom(typeof(string)))
      {
        try
        {
          object val=tc.ConvertFrom(pvalue);
          pi.SetValue(obj, val, null);
        }
        catch(Exception e)
        {
          Trace.WriteLine("Setting "+pname+" failed:\r\n"+e.Message);
        }
      }
    }
  }
}

Usage

Usage is very simple. Let's say we want to serialize a simple class (in this example, one that is constructed at compile time):

C#
public class TestClass
{
    protected string firstName;
    protected string lastName;

    [DefaultValue("Marc")]
    public string FirstName
    {
        get {return firstName;}
        set {firstName=value;}
    }

    [DefaultValue("Clifton")]
    public string LastName
    {
        get {return lastName;}
        set {lastName=value;}
    }

    public TestClass()
    {
        firstName="Marc";
        lastName="Clifton";
    }
}

Serializing an instance of this class would look like this:

C#
Serializer s=new Serializer();
s.Start();
TestClass tc=new TestClass();
tc.FirstName="Joe";
tc.LastName="Smith";
s.Serialize(tc);
string text=s.Finish();

Resulting in XML that looks like this:

XML
<?xml version="1.0" encoding="utf-8"?> 
<!--Auto-Serialized--> 
<Objects> 
  <TestClass FirstName="Joe" LastName="Smith" /> 
</Objects>

Deserialization of this object is done as:

C#
Deserializer d=new Deserializer();
d.Start(text);
TestClass tc=new TestClass();
d.Deserialize(tc, 0);

After which, the properties of the class are set to "Joe" and "Smith".

Revisions

  • 11/30/2004 - Added support for property types that implement IList

Conclusion

The code presented above meets a very specific requirement that I have. Even if you don't have this requirement, hopefully you'll gain something from the techniques demonstrated, especially the use of the type converter to convert to and from a string. This is an important "trick" to ensure that the serialized string is in a format that the deserializer can handle and avoids writing special case code for Font, Color, and other objects.

License

This article has no explicit license attached to it, but may contain usage terms in the article text or the download files themselves. If in doubt, please contact the author via the discussion board below.

A list of licenses authors might use can be found here.


Written By
Architect Interacx
United States United States
Blog: https://marcclifton.wordpress.com/
Home Page: http://www.marcclifton.com
Research: http://www.higherorderprogramming.com/
GitHub: https://github.com/cliftonm

All my life I have been passionate about architecture / software design, as this is the cornerstone to a maintainable and extensible application. As such, I have enjoyed exploring some crazy ideas and discovering that they are not so crazy after all. I also love writing about my ideas and seeing the community response. As a consultant, I've enjoyed working in a wide range of industries such as aerospace, boatyard management, remote sensing, emergency services / data management, and casino operations. I've done a variety of pro-bono work non-profit organizations related to nature conservancy, drug recovery and women's health.

Comments and Discussions

 
Questionworks great...except Pin
Aaron Sulwer24-Jul-14 11:07
Aaron Sulwer24-Jul-14 11:07 
GeneralMy vote of 4 Pin
Member 1067919018-Mar-14 1:39
Member 1067919018-Mar-14 1:39 
GeneralEror Message [modified] Pin
antoschka18-Jan-09 20:53
antoschka18-Jan-09 20:53 
GeneralBrilliant Article! Pin
Lea Hayes28-Apr-08 13:46
Lea Hayes28-Apr-08 13:46 
GeneralSave to SQL Server 2005 Pin
hugobq7-Jun-06 8:11
hugobq7-Jun-06 8:11 
GeneralRe: Save to SQL Server 2005 Pin
Marc Clifton8-Jun-06 14:31
mvaMarc Clifton8-Jun-06 14:31 
GeneralBugs Pin
janpub4-Sep-05 3:02
janpub4-Sep-05 3:02 
Generallightweight class to serialize/deserialise object and xml string Pin
Member 145201420-Oct-04 9:55
Member 145201420-Oct-04 9:55 
GeneralRe: lightweight class to serialize/deserialise object and xml string Pin
Marc Clifton20-Oct-04 10:06
mvaMarc Clifton20-Oct-04 10:06 
GeneralRe: lightweight class to serialize/deserialise object and xml string Pin
FocusedWolf7-Dec-05 8:28
FocusedWolf7-Dec-05 8:28 
I'm trying to get a modification of your Serialize function working...but for some reason i cant initialize my XPathDocument with the memorystream...get these errors like "Root element is missing", but i get the xml if i do it like you did with StringWriter.

 public static IXPathNavigable Serialize(object obj)<br />
        {<br />
            if (obj == null)<br />
                throw new ArgumentNullException("obj");<br />
<br />
            Type objType = obj.GetType();<br />
<br />
            XmlSerializer xmlSerializer = new XmlSerializer(objType); Serializer.TargetNamespace);<br />
<br />
            MemoryStream memoryStream = new MemoryStream();<br />
            xmlSerializer.Serialize(memoryStream, obj); <br />
<br />
            XPathDocument xml = new XPathDocument(memoryStream); // <- Dies here<br />
<br />
            memoryStream.Close();<br />
<br />
            return xml as IXPathNavigable;

GeneralRe: lightweight class to serialize/deserialise object and xml string Pin
FocusedWolf8-Dec-05 18:15
FocusedWolf8-Dec-05 18:15 
GeneralNice Work Pin
Nick Parker22-Sep-04 9:25
protectorNick Parker22-Sep-04 9:25 
GeneralNice idea with the TypeConverter Pin
Mike Ellison22-Sep-04 7:40
Mike Ellison22-Sep-04 7:40 
GeneralRe: Nice idea with the TypeConverter Pin
Arun Bhalla28-Sep-04 16:25
Arun Bhalla28-Sep-04 16:25 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.