Serialize/Deserialize derived class as base class
Asked Answered
B

1

2

For example I have the following classes:

public abstract class Device
{
}

public class WindowsDevice: Device
{
}

public class AndroidDevice: Device
{
}

Now I want to serialize/deserialize WindowsDevice and AndroidDevice as XML:

public static string Serialize(object o, Type[] additionalTypes = null)
    {
        var serializer = new XmlSerializer(o.GetType(), additionalTypes);

        using (var stringWriter = new StringWriterWithEncoding(Encoding.UTF8))
        {
            serializer.Serialize(stringWriter, o);
            return stringWriter.ToString();
        }
    }

This will produce the following output:

<?xml version="1.0" encoding="utf-8"?>
<WindowsDevice xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema">

</WindowsDevice>

But now I am unable to deserialize this, because in my application I don't know if the XML is WindowsDevice or AndroidDevice, so I have to deserialize as typeof(Device). But then I will get an exception that "WindowsDevice" was unexpected in the XML.

I tried XmlInclude and extraTypes without any success.

What I dont understand is, that if I have the following sample class:

public class SampleClass
{
    public List<Device> Devices {get;set}
}

and if I serialize SampleClass and use XmlInclude or extraTypes I exactly get what I want:

<Devices>
<Device xsi:type="WindowsDevice"></Device>
</Devices>

But I don't have that class and I don't have a list of Devices. I only want to serialize/deserialize WindowsDevice and AndroidDevice but on Deserialize I don't know whether it is AndroidDevice or WindowsDevice so I have to use typeof(Device) and want to get the correct sublass AndroidDevice or WindowsDevice, so instead of:

<WindowsDevice></WindowsDevice>

I want to have:

<Device xsi:type="WindowsDevice"></Device>

How can this be done?

Bohannan answered 28/4, 2017 at 8:11 Comment(4)
Doesn't the opening tag tell you what you need to know? <WindowsDevice> tells you what the object is. Couldn't you just read that tag and then you know what type it is?Polyneuritis
I have a lot of those classes and a general serialize/deserialize method. I don't want to use this "dirty" workaround. Think of renaming a class, adding new classes, etc. I prefer a clean solution if possible. As said, the XmlSerializer is able to do what I want if I use a List<>, so I'm wondering how to use that mechanism on a single instance of a class.Bohannan
I tried XmlInclude and extraTypes without any success. - what did you try? I think it should work.Toulouse
@dbc: [XmlInclude(typeof(WindowsDevice)] public abstract class Device { } And: new XmlSerializer(o.GetType(), new Type[] { typeof(WindowsDevice})Bohannan
T
3

Your problem is that you are constructing your XmlSerializer inconsistently during serialization and deserialization. You need to construct it using the same Type argument in both cases, specifically the base type typeof(Device). Thus I'd suggest you replace your existing completely general serialization method with one specific for a Device:

public static class DeviceExtensions
{
    public static string SerializeDevice<TDevice>(this TDevice o) where TDevice : Device
    {
        // Ensure that [XmlInclude(typeof(TDevice))] is present on Device.
        // (Included for clarity -- actually XmlSerializer will make a similar check.)
        if (!typeof(Device).GetCustomAttributes<XmlIncludeAttribute>().Any(a => a.Type == o.GetType()))
        {
            throw new InvalidOperationException("Unknown device type " + o.GetType());
        }
        var serializer = new XmlSerializer(typeof(Device)); // Serialize as the base class
        using (var stringWriter = new StringWriterWithEncoding(Encoding.UTF8))
        {
            serializer.Serialize(stringWriter, o);
            return stringWriter.ToString();
        }
    }

    public static Device DeserializeDevice(this string xml)
    {
        var serial = new XmlSerializer(typeof(Device));
        using (var reader = new StringReader(xml))
        {
            return (Device)serial.Deserialize(reader);
        }
    }
}

Then, apply [XmlInclude(typeof(TDevice))] to Device for all possible subtypes:

[XmlInclude(typeof(WindowsDevice))]
[XmlInclude(typeof(AndroidDevice))]
public abstract class Device
{
}

Then both types of devices can now be serialized and deserialized successfully while retaining their type, because XmlSerializer will include an "xsi:type" attribute to explicitly indicate the type:

<Device xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" 
    xsi:type="WindowsDevice" />

Or

<Device xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" 
    xsi:type="AndroidDevice" />

Sample fiddle.

Updates

So the issue was, that I serialized with typeof(WindowsDevice) instead of typeof(Device)?

Yes.

Any ideas for a solution which will work, if I have to use typeof(WindowsDevice)? Cause I have hundreds of classes and don't want to use hundreds of different XmlSerializer initializations...

This is more of an architectural question than a howto question. One possibility would be to introduce a custom attribute that you can apply to a class to indicate that any subtypes of that class should always be serialized as the attributed base type. All appropriate [XmlInclude(typeof(TDerivedType))] attributes will also be required:

[System.AttributeUsage(System.AttributeTargets.Class, AllowMultiple = false, Inherited = false)]
public class XmlBaseTypeAttribute : System.Attribute
{
}   

[XmlInclude(typeof(WindowsDevice))]
[XmlInclude(typeof(AndroidDevice))]
[XmlBaseType]
public abstract class Device
{
}

Then modify your universal XML serialization code to look up the type hierarchy of the object being serialized for an [XmlBaseType] attribute, and (de)serialize as that type:

public static class XmlExtensions
{
    static Type GetSerializedType(this Type type)
    {
        var serializedType = type.BaseTypesAndSelf().Where(t => Attribute.IsDefined(t, typeof(XmlBaseTypeAttribute))).SingleOrDefault();
        if (serializedType != null)
        {
            // Ensure that [XmlInclude(typeof(TDerived))] is present on the base type
            // (Included for clarity -- actually XmlSerializer will make a similar check.)
            if (!serializedType.GetCustomAttributes<XmlIncludeAttribute>().Any(a => a.Type == type))
            {
                throw new InvalidOperationException(string.Format("Unknown subtype {0} of type {1}", type, serializedType));
            }
        }
        return serializedType ?? type;
    }

    public static string Serialize(this object o)
    {
        var serializer = new XmlSerializer(o.GetType().GetSerializedType());
        using (var stringWriter = new StringWriterWithEncoding(Encoding.UTF8))
        {
            serializer.Serialize(stringWriter, o);
            return stringWriter.ToString();
        }
    }

    public static T Deserialize<T>(this string xml)
    {
        var serial = new XmlSerializer(typeof(T).GetSerializedType());
        using (var reader = new StringReader(xml))
        {
            return (T)serial.Deserialize(reader);
        }
    }
}

Of course this means that if your code tries to deserialize XML it expects to contain a WindowsDevice, it might actually get back an AndroidDevice depending upon the contents of the XML.

Sample fiddle #2.

Toulouse answered 28/4, 2017 at 8:56 Comment(1)
Thanks! So the issue was, that I serialized with typeof(WindowsDevice) instead of typeof(Device)? Any ideas for a solution which will work, if I have to use typeof(WindowsDevice)? Cause I have hundreds of classes and don't want to use hundreds of different XmlSerializer initializations... At runtime I have a instace of WindowsDevice which should be passed to the XmlSerializer. I also can't always use the BaseType of the passed Type, because some classes should be serialized as derived classes (because all my hundreds of classes are subclasses of other classes) and some as their base class.Bohannan

© 2022 - 2024 — McMap. All rights reserved.