How do I use an XmlSerializer to deserialize an object that might be of a base or derived class without knowing the type beforehand?
Asked Answered
E

5

22

In C#, how do I use an XmlSerializer to deserialize an object that might be of a base class, or of any of several derived classes without knowing the type beforehand?

All of my derived classes add additional data members. I've made a simple GUI that can serialize and deserialize class objects. It will serialize objects as whatever inherited class (or even just the base class) is appropriate based on which fields the user chooses to populate.

I have no issues with the serialization; the problem is the deserialization. How can I possibly have the XmlSerializer deserialize data to the correct derived class without knowing the class beforehand? I currently create an XmlReader to read the first node of the XML file and determine the class from it, and it seems to work for my purposes, but it seems like an extremely inelegant solution.

I've posted some sample code below. Any suggestions?

BaseType objectOfConcern = new BaseType();
XmlSerializer xserializer;
XmlTextReader xtextreader = new XmlTextReader(DEFAULT_FILENAME);

do { xtextreader.Read(); } while (xtextreader.NodeType != XmlNodeType.Element);

string objectType = xtextreader.Name;
xtextreader.Close();

FileStream fstream = new FileStream(DEFAULT_FILENAME, FileMode.Open);

switch (objectType)
    {
case "type1":
    xserializer = new XmlSerializer(typeof(DerivedType));

    objectOfConcern = (DerivedType)xserializer.Deserialize(fstream);

    //Load fields specific to that derived type here
    whatever = (objectOfConcern as DerivedType).NoOfstreamubordinates.ToString();

    case "xxx_1":
        //code here

    case "xxx_2":
        //code here

    case "xxx_n":
        //code here

        //and so forth

    case "BaseType":
    xserializer = new XmlSerializer(typeof(BaseType));
    AssignEventHandler(xserializer);
    objectOfConcern = (BaseType)xserializer.Deserialize(fstream);
}

//Assign all deserialized values from base class common to all derived classes here

//Close the FileStream
fstream.Close();
Eme answered 26/1, 2011 at 3:40 Comment(0)
I
18

Have you some root class/tag which contains that derived types? If yes, you can use XmlElementAttribute to map tag name to type:

public class RootElementClass
{
    [XmlElement(ElementName = "Derived1", Type = typeof(Derived1BaseType))]
    [XmlElement(ElementName = "Derived2", Type = typeof(Derived2BaseType))]
    [XmlElement(ElementName = "Derived3", Type = typeof(Derived3BaseType))]
    public BaseType MyProperty { get; set; }
}

public class BaseType { }
public class Derived1BaseType : BaseType { }
public class Derived2BaseType : BaseType { }
public class Derived3BaseType : BaseType { }
Ijssel answered 26/1, 2011 at 4:23 Comment(0)
B
6

I recently wrote this generic serializer\deserializer for base class T and any derived classes of T. Seems to work so far.

The Type[] array stores all the derived types of T and T itself. The deserializer tries each of them, and returns when it found the right one.

/// <summary>
/// A generic serializer\deserializer
/// </summary>
/// <typeparam name="T"></typeparam>
public static class Serializer<T>
{
    /// <summary>
    /// serialize an instance to xml
    /// </summary>
    /// <param name="instance"> instance to serialize </param>
    /// <returns> instance as xml string </returns>
    public static string Serialize(T instance)
    {
        StringBuilder sb = new StringBuilder();
        XmlWriterSettings settings = new XmlWriterSettings();

        using (XmlWriter writer = XmlWriter.Create(sb, settings))
        {
            XmlSerializer serializer = new XmlSerializer(instance.GetType());
            serializer.Serialize(writer, instance);
        }

        return sb.ToString();
    }

    /// <summary>
    /// deserialize an xml into an instance
    /// </summary>
    /// <param name="xml"> xml string </param>
    /// <returns> instance </returns>
    public static T Deserialize(string xml)
    {
        using (XmlReader reader = XmlReader.Create(new StringReader(xml)))
        {
            foreach (Type t in types)
            {
                XmlSerializer serializer = new XmlSerializer(t);
                if (serializer.CanDeserialize(reader))
                    return (T)serializer.Deserialize(reader);
            }
        }

        return default(T);
    }

    /// <summary>
    /// store all derived types of T:
    /// is used in deserialization
    /// </summary>
    private static Type[] types = AppDomain.CurrentDomain.GetAssemblies()
                                        .SelectMany(s => s.GetTypes())
                                        .Where(t => typeof(T).IsAssignableFrom(t)
                                            && t.IsClass
                                            && !t.IsGenericType)
                                            .ToArray();
}
Bainbridge answered 8/3, 2014 at 1:11 Comment(1)
Great! You saved my day!Alienor
S
5

If you're not set upon using the XmlSerializer you can use the DataContractSerializer with the KnownType attribute instead.

All you need to do is add a KnownType attribute to the parent class for each sub class and the DataContractSerializer will do the rest.

The DataContractSerializer will add type information when serializing to xml and use that type information when deserializing to create the correct type.

For example the following code:

[KnownType( typeof( C2 ) )]
[KnownType( typeof( C3 ) )]
public class C1 {public string P1 {get;set;}}
public class C2 :C1 {public string P2 {get;set;}}
public class C3 :C1 {public string P3 {get;set;}}

class Program
{
  static void Main(string[] args)
  {
    var c1 = new C1{ P1="c1"};
    var c2 = new C2{ P1="c1", P2="c2"};
    var c3 = new C3{ P1="c1", P3="c3"};

    var s = new DataContractSerializer( typeof( C1 ) );
    Test( c1, s );
    Test( c2, s );
    Test( c3, s );
  }

  static void Test( C1 objectToSerialize, DataContractSerializer serializer )
  {
    using ( var stream = new MemoryStream() )
    {
      serializer.WriteObject( stream, objectToSerialize );
      stream.WriteTo( Console.OpenStandardOutput() );
      stream.Position = 0;
      var deserialized = serializer.ReadObject( stream );
      Console.WriteLine( Environment.NewLine + "Deserialized Type: " + deserialized.GetType().FullName );              
    }
  }
}

Will output :

<C1 xmlns="http://schemas.datacontract.org/2004/07/ConsoleApplication1" xmlns:i="http://www.w3.org/2001/XMLSchema-instance">
<P1>c1</P1></C1>

Deserialized Type: ConsoleApplication1.C1

<C1 i:type="C2" xmlns="http://schemas.datacontract.org/2004/07/ConsoleApplication1" xmlns:i="http://www.w3.org/2001/XMLSchema-instance">
<P1>c1</P1><P2>c2</P2></C1>

Deserialized Type: ConsoleApplication1.C2

<C1 i:type="C3" xmlns="http://schemas.datacontract.org/2004/07/ConsoleApplication1" xmlns:i="http://www.w3.org/2001/XMLSchema-instance">
<P1>c1</P1><P3>c3</P3></C1>

Deserialized Type: ConsoleApplication1.C3

In the output you'll notice the xml for c2 and c3 contained extra type information which allowed the DataContractSerializer.ReadObject to create the correct type.

Shool answered 27/1, 2011 at 5:2 Comment(0)
F
4

You could try to use the constructor XmlSerializer(Type type, Type[] extraTypes) to create a serializer that works with all involved types.

Fold answered 26/1, 2011 at 4:1 Comment(0)
D
2

you can use XmlInclude

[XmlInclude(typeof(MyClass))]
public abstract class MyBaseClass
{
   //...
}

otherwise if you want to add the types when serializing:

Type[] types = new Type[]{ typeof(MyClass) }

XmlSerializer serializer = new XmlSerializer(typeof(MyBaseClass), types);
Diplopod answered 7/1, 2016 at 14:13 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.