78

Can I serialize a generic list of serializable objects without having to specify their type.

Something like the intention behind the broken code below:

List<ISerializable> serializableList = new List<ISerializable>();

XmlSerializer xmlSerializer = new XmlSerializer(serializableList.GetType());

serializableList.Add((ISerializable)PersonList);

using (StreamWriter streamWriter = System.IO.File.CreateText(fileName))
{
    xmlSerializer.Serialize(streamWriter, serializableList);
}

Edit:

For those who wanted to know detail: when I try to run this code, it errors on the XMLSerializer[...] line with:

Cannot serialize interface System.Runtime.Serialization.ISerializable.

If I change to List<object> I get "There was an error generating the XML document.". The InnerException detail is "{"The type System.Collections.Generic.List1[[Project1.Person, ConsoleFramework, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null]] may not be used in this context."}"

The person object is defined as follows:

[XmlRoot("Person")]
public class Person
{
    string _firstName = String.Empty;
    string _lastName = String.Empty;

    private Person()
    {
    }

    public Person(string lastName, string firstName)
    {
        _lastName = lastName;
        _firstName = firstName;
    }

    [XmlAttribute(DataType = "string", AttributeName = "LastName")]
    public string LastName
    {
        get { return _lastName; }
        set { _lastName = value; }
    }

    [XmlAttribute(DataType = "string", AttributeName = "FirstName")]
    public string FirstName
    {
        get { return _firstName; }
        set { _firstName = value; }
    }
}

The PersonList is just a List<Person> .

This is just for testing though, so didn't feel the details were too important. The key is I have one or more different objects, all of which are serializable. I want to serialize them all to one file. I thought the easiest way to do that would be to put them in a generic list and serialize the list in one go. But this doesn't work.

I tried with List<IXmlSerializable> as well, but that fails with

System.Xml.Serialization.IXmlSerializable cannot be serialized because it does not have a parameterless constructor.

Sorry for the lack of detail, but I am a beginner at this and don't know what detail is required. It would be helpful if people asking for more detail tried to respond in a way that would leave me understanding what details are required, or a basic answer outlining possible directions.

Also thanks to the two answers I've got so far - I could have spent a lot more time reading without getting these ideas. It's amazing how helpful people are on this site.

Dima
  • 6,721
  • 4
  • 24
  • 43
Simon D
  • 4,150
  • 5
  • 39
  • 47

10 Answers10

89

I have an solution for a generic List<> with dynamic binded items.

class PersonalList it's the root element

[XmlRoot("PersonenListe")]
[XmlInclude(typeof(Person))] // include type class Person
public class PersonalList
{
    [XmlArray("PersonenArray")]
    [XmlArrayItem("PersonObjekt")]
    public List<Person> Persons = new List<Person>();

    [XmlElement("Listname")]
    public string Listname { get; set; }

    // Konstruktoren 
    public PersonalList() { }

    public PersonalList(string name)
    {
        this.Listname = name;
    }

    public void AddPerson(Person person)
    {
        Persons.Add(person);
    }
}

class Person it's an single list element

[XmlType("Person")] // define Type
[XmlInclude(typeof(SpecialPerson)), XmlInclude(typeof(SuperPerson))]  
        // include type class SpecialPerson and class SuperPerson
public class Person
{
    [XmlAttribute("PersID", DataType = "string")]
    public string ID { get; set; }

    [XmlElement("Name")]
    public string Name { get; set; }

    [XmlElement("City")]
    public string City { get; set; }

    [XmlElement("Age")]
    public int Age { get; set; }

    // Konstruktoren 
    public Person() { }

    public Person(string name, string city, int age, string id)
    {
        this.Name = name;
        this.City = city;
        this.Age = age;
        this.ID = id;
    }
}

class SpecialPerson inherits Person

[XmlType("SpecialPerson")] // define Type
public class SpecialPerson : Person
{
    [XmlElement("SpecialInterests")]
    public string Interests { get; set; }

    public SpecialPerson() { }

    public SpecialPerson(string name, string city, int age, string id, string interests)
    {
        this.Name = name;
        this.City = city;
        this.Age = age;
        this.ID = id;
        this.Interests = interests;
    }
}

class SuperPerson inherits Person

[XmlType("SuperPerson")] // define Type
public class SuperPerson : Person
{
    [XmlArray("Skills")]
    [XmlArrayItem("Skill")]
    public List<String> Skills { get; set; }

    [XmlElement("Alias")]
    public string Alias { get; set; }

    public SuperPerson() 
    {
        Skills = new List<String>();
    }

    public SuperPerson(string name, string city, int age, string id, string[] skills, string alias)
    {
        Skills = new List<String>();

        this.Name = name;
        this.City = city;
        this.Age = age;
        this.ID = id;
        foreach (string item in skills)
        {
            this.Skills.Add(item);   
        }
        this.Alias = alias;
    }
}

and the main test Source

static void Main(string[] args)
{
    PersonalList personen = new PersonalList(); 
    personen.Listname = "Friends";

    // normal person
    Person normPerson = new Person();
    normPerson.ID = "0";
    normPerson.Name = "Max Man";
    normPerson.City = "Capitol City";
    normPerson.Age = 33;

    // special person
    SpecialPerson specPerson = new SpecialPerson();
    specPerson.ID = "1";
    specPerson.Name = "Albert Einstein";
    specPerson.City = "Ulm";
    specPerson.Age = 36;
    specPerson.Interests = "Physics";

    // super person
    SuperPerson supPerson = new SuperPerson();
    supPerson.ID = "2";
    supPerson.Name = "Superman";
    supPerson.Alias = "Clark Kent";
    supPerson.City = "Metropolis";
    supPerson.Age = int.MaxValue;
    supPerson.Skills.Add("fly");
    supPerson.Skills.Add("strong");

    // Add Persons
    personen.AddPerson(normPerson);
    personen.AddPerson(specPerson);
    personen.AddPerson(supPerson);

    // Serialize 
    Type[] personTypes = { typeof(Person), typeof(SpecialPerson), typeof(SuperPerson) };
    XmlSerializer serializer = new XmlSerializer(typeof(PersonalList), personTypes); 
    FileStream fs = new FileStream("Personenliste.xml", FileMode.Create); 
    serializer.Serialize(fs, personen); 
    fs.Close(); 
    personen = null;

    // Deserialize 
    fs = new FileStream("Personenliste.xml", FileMode.Open); 
    personen = (PersonalList)serializer.Deserialize(fs); 
    serializer.Serialize(Console.Out, personen);
    Console.ReadLine();
}

Important is the definition and includes of the diffrent types.

slavoo
  • 5,798
  • 64
  • 37
  • 39
Damasch
  • 906
  • 7
  • 2
23

See Introducing XML Serialization:

Items That Can Be Serialized

The following items can be serialized using the XmlSerializer class:

  • Public read/write properties and fields of public classes
  • Classes that implement ICollection or IEnumerable
  • XmlElement objects
  • XmlNode objects
  • DataSet objects

In particular, ISerializable or the [Serializable] attribute does not matter.


Now that you've told us what your problem is ("it doesn't work" is not a problem statement), you can get answers to your actual problem, instead of guesses.

When you serialize a collection of a type, but will actually be serializing a collection of instances of derived types, you need to let the serializer know which types you will actually be serializing. This is also true for collections of object.

You need to use the XmlSerializer(Type,Type[]) constructor to give the list of possible types.

Community
  • 1
  • 1
John Saunders
  • 160,644
  • 26
  • 247
  • 397
  • http://msdn.microsoft.com/en-us/library/182eeyhh%28VS.85%29.aspx - link got mangled, thanks for this I'm understanding more...lots to read. – Simon D Jul 31 '09 at 15:04
  • 2
    Also thanks for the advice on how to ask questions, it's appreciated. – Simon D Jul 31 '09 at 15:09
  • +1. Yes, they don't matter for XML, but if he ever wants to use other types of serialization, they will. Also, since he was casting to ISerializable, it seemed like a perfectly basic question for him to check that the different types implemented that interface. I hope it wasn't a dig at my comment, and I think this was helpful in clarifying some of these details. – Erich Mirabal Jul 31 '09 at 15:12
  • @Erich: don't be so sensitive. It's a fact, not a dig. I'm constantly seeing XML Serialization examples with `[Serializable]`, which is just plain wrong. – John Saunders Jul 31 '09 at 15:19
  • @Fred: Glad to help, but it's self-defense. I hope to see many more questions from you - and I hope they include details! BTW, when you post about an exception causing you problems, catch it, then post the result of `ex.ToString()`. That tells us everything. – John Saunders Jul 31 '09 at 15:20
  • 1
    I was just saying that there is a difference in calling an object serializable and serializing it in XML. I didn't take it too personally or sensitively; I actually up-voted your answer as it brought up some good points about serializing in XML that are never mentioned. Still, they may not be *required* for XML, but it usually is good practice so that you can easily support other serializations. – Erich Mirabal Jul 31 '09 at 17:28
  • 1
    @Erich: I have to disagree. The various serialization technologies are too different. It makes no sense to support those you're not going to use. Consider: did you plan to _test_ the ones you're not using? In my opinion, if you didn't test it, then it doesn't work. Why produce code that you know you're not going to test? – John Saunders Jul 31 '09 at 17:55
  • Well, for example `xsd.exe` marks its generated classes with `[Serializable]` to give you more flexibility (since you are already serializing to XML, you may also want to serialize to binary, for example). I think "not test -> not working" is a good philosophy and rule of thumb, but I've had plenty of cases (for reasons outside of my control) where I've had to produce code that I could not test other than running it as an end-product. Most of the time, if I've serialized it as XML, I have also had to support it as binary (or vice-versa). That's the nature of serialization in my experience. – Erich Mirabal Jul 31 '09 at 18:19
  • I would not count xsd.exe as an especially good example. In fact, it's the .NET Framework code it's calling that sets the attributes, and its entire _purpose_ is to be general. That has no bearing on how anyone else should design their code. – John Saunders Jul 31 '09 at 18:51
  • For reference, it says "Classes that implement `ICollection` or `IEnumerable`", not "`ICollection` or `IEnumerable`". That means that the serializer will throw a fit if your field or property is declared as `ICollection` or `IEnumerable` as opposed to a `ListOfBar` – BrainStorm.exe Dec 21 '20 at 17:26
  • @BrainStorm.exe do you have a reproducer for that? It seems unlikely. The serializer shouldn't care about the class that implements the iterfaces(s). Anything it needs, it can get from `System.Object`. – John Saunders Jan 05 '21 at 02:59
  • @JohnSaunders I am not sure what you mean by it can get everything from `System.Object`. The `XmlSerializer` grabs the default constructor for the item's type (which isn't possible for interfaces) and makes an instance using that. Then it calls `Add(T)` to add new items to the collection. I also mostly mentioned that for those who are likely wondering why it explodes when handed an interface as opposed to a class. A quick example is `new XmlSerializer(typeof(IList))` vs `new XmlSerializer(typeof(List))`. The former explodes while the later doesn't. – BrainStorm.exe Jan 08 '21 at 18:35
  • @BrainStorm.exe You're right. I was thinking about it the wrong way. It will use the default constructor for the type specified in the `XmlSerializer` constructor when deserializing. – John Saunders Jan 13 '21 at 07:03
9

You can't serialize a collection of objects without specifying the expected types. You must pass the list of expected types to the constructor of XmlSerializer (the extraTypes parameter) :

List<object> list = new List<object>();
list.Add(new Foo());
list.Add(new Bar());

XmlSerializer xs = new XmlSerializer(typeof(object), new Type[] {typeof(Foo), typeof(Bar)});
using (StreamWriter streamWriter = System.IO.File.CreateText(fileName))
{
    xs.Serialize(streamWriter, list);
}

If all the objects of your list inherit from the same class, you can also use the XmlInclude attribute to specify the expected types :

[XmlInclude(typeof(Foo)), XmlInclude(typeof(Bar))]
public class MyBaseClass
{
}
Tareq
  • 1,397
  • 27
  • 28
Thomas Levesque
  • 286,951
  • 70
  • 623
  • 758
  • Hmm, that looks interesting. I'm assuming though it still won't deserialize? – Ian Jul 31 '09 at 15:05
  • It *will* deserialize. Actually, during serialization, an extra attribute `xsi:type` is written on the element that represents the object. This allows XmlSerializer to know the actual type of the object – Thomas Levesque Jul 31 '09 at 15:47
  • 1
    +1 Great answer, the only that answers the question as far as I can see. Based on my own testing the first type you pass to the XmlSerializer ctor should be List not object, as edited. – jwg Mar 22 '13 at 15:25
  • How would you do this if you didn't know all the types that are derived from the base class? For example I'm writing an plugin framework which has a base class which will be extended by 3rd parties. – Dasith Wijes Jan 11 '16 at 05:57
  • @Dasiths, can you discover the types at runtime? If you can, try using [XmlAttributeOverrides](https://msdn.microsoft.com/en-us/library/system.xml.serialization.xmlattributeoverrides.aspx) to add the types dynamically. – Thomas Levesque Jan 11 '16 at 09:19
4

I think it's best if you use methods with generic arguments, like the following :

public static void SerializeToXml<T>(T obj, string fileName)
{
    using (var fileStream = new FileStream(fileName, FileMode.Create))
    { 
        var ser = new XmlSerializer(typeof(T)); 
        ser.Serialize(fileStream, obj);
    }
}

public static T DeserializeFromXml<T>(string xml)
{
    T result;
    var ser = new XmlSerializer(typeof(T));
    using (var tr = new StringReader(xml))
    {
        result = (T)ser.Deserialize(tr);
    }
    return result;
}
Andreas Grech
  • 105,982
  • 98
  • 297
  • 360
  • OK perhaps, but this still doesn't do what I want - serialize a list of objects, possibly of different types. I want to be able to add a bunch of objects I want to save to a list, then serialize the entire list in one go (and to one file). – Simon D Jul 31 '09 at 14:20
  • I don't like this option, because creating XmlSerializers is expensive. – Ian Jul 31 '09 at 14:32
  • Ian, Would you please let me know how do you find XmlSerializers expensive? – paradisonoir Jul 31 '09 at 14:50
  • This will cause a memory leak. – Rex M Jul 31 '09 at 14:53
  • 1
    paradisonoir, construction of them creates temporary serialization assemblies for the given type (and all referenced types that need to be deserialized). If you have a large object graoh that needs serializing or just a large number of objects and you're repeatedly calling XmlSerializer then it can be slow. We found that it was taking 10-13 seconds to serialize and deserialize some settings, which was made almost instant by pre-building these serialization assemblies. – Ian Jul 31 '09 at 14:55
  • Why will this cause a memory leak? – Simon D Jul 31 '09 at 14:56
  • 1
    @Fred the XmlSerializer(Type) constructor creates a temp assembly to handle the serialization for that type and adds it to the AppDomain, but if you call it a second time for the same type, it does not re-use the same temp assembly, it creates another one. Since assemblies cannot be removed from the AppDomain once they are loaded, every time you invoke the constructor your memory footprint will increase slightly. – Rex M Jul 31 '09 at 14:59
  • 1
    In an application like the one I work on daily, if each new temp assembly represented 1kb, we would be leaking 200 gigabytes per day. By carefully managing the XmlSerializers in a type cache, we keep the same set of serializers in memory, at a total cost of about 1 megabyte of cache. – Rex M Jul 31 '09 at 15:05
  • Thanks Rex M interesting stuff. – Simon D Jul 31 '09 at 15:10
  • @Dreas: you declared the filestream after you used it, and didn't enclose it in a `using` block. – John Saunders Jul 31 '09 at 15:22
  • 2
    @RexM - I know this is an old post so things may have changed but I thought that the `XmlSerializer(Type)` overload was one of the safe to use constructors as it does reuse the cached assemblies? (the other being public `XmlSerializer(Type type, string defaultNamespace)`) http://support.microsoft.com/kb/886385/en-us – keyboardP Mar 25 '13 at 12:57
3

I think Dreas' approach is ok. An alternative to this however is to have some static helper methods and implement IXmlSerializable on each of your methods e.g an XmlWriter extension method and the XmlReader one to read it back.

public static void SaveXmlSerialiableElement<T>(this XmlWriter writer, String elementName, T element) where T : IXmlSerializable
{
   writer.WriteStartElement(elementName);
   writer.WriteAttributeString("TYPE", element.GetType().AssemblyQualifiedName);
   element.WriteXml(writer);
   writer.WriteEndElement();
}

public static T ReadXmlSerializableElement<T>(this XmlReader reader, String elementName) where T : IXmlSerializable
{
   reader.ReadToElement(elementName);

   Type elementType = Type.GetType(reader.GetAttribute("TYPE"));
   T element = (T)Activator.CreateInstance(elementType);
   element.ReadXml(reader);
   return element;
}

If you do go down the route of using the XmlSerializer class directly, create serialization assemblies before hand if possible, as you can take a large performance hit in constructing new XmlSerializers regularly.

For a collection you need something like this:

public static void SaveXmlSerialiazbleCollection<T>(this XmlWriter writer, String collectionName, String elementName, IEnumerable<T> items) where T : IXmlSerializable
{
   writer.WriteStartElement(collectionName);
   foreach (T item in items)
   {
      writer.WriteStartElement(elementName);
      writer.WriteAttributeString("TYPE", item.GetType().AssemblyQualifiedName);
      item.WriteXml(writer);
      writer.WriteEndElement();
   }
   writer.WriteEndElement();
}
Ian
  • 33,605
  • 26
  • 118
  • 198
  • This looks interesting, but I'm not sure how to use it exactly. I'll try to play with it a bit and see if I can get it to work. – Simon D Jul 31 '09 at 14:32
  • Ok, firstly it all the objects in the collection need to be strongly typed to the same base type. The objects need to implement IXmlSerializable (not too hard). For a collection e.g. List you'll then call something like: XmlWriter.SaveXmlSerializableCollection("item", this.collectionOfTs) – Ian Jul 31 '09 at 14:37
  • btw, do you need to deserialize them too? If not I can probably provide a fuller example for you... – Ian Jul 31 '09 at 14:41
  • I probably will deserialize, but that's already a fair bit to be getting on with. I'll try and put what you've given me already into practice. Thanks very much. – Simon D Jul 31 '09 at 14:55
2

Below is a Util class in my project:

namespace Utils
{
    public static class SerializeUtil
    {
        public static void SerializeToFormatter<F>(object obj, string path) where F : IFormatter, new()
        {
            if (obj == null)
            {
                throw new NullReferenceException("obj Cannot be Null.");
            }

            if (obj.GetType().IsSerializable == false)
            {
                //  throw new 
            }
            IFormatter f = new F();
            SerializeToFormatter(obj, path, f);
        }

        public static T DeserializeFromFormatter<T, F>(string path) where F : IFormatter, new()
        {
            T t;
            IFormatter f = new F();
            using (FileStream fs = File.OpenRead(path))
            {
                t = (T)f.Deserialize(fs);
            }
            return t;
        }

        public static void SerializeToXML<T>(string path, object obj)
        {
            XmlSerializer xs = new XmlSerializer(typeof(T));
            using (FileStream fs = File.Create(path))
            {
                xs.Serialize(fs, obj);
            }
        }

        public static T DeserializeFromXML<T>(string path)
        {
            XmlSerializer xs = new XmlSerializer(typeof(T));
            using (FileStream fs = File.OpenRead(path))
            {
                return (T)xs.Deserialize(fs);
            }
        }

        public static T DeserializeFromXml<T>(string xml)
        {
            T result;

            var ser = new XmlSerializer(typeof(T));
            using (var tr = new StringReader(xml))
            {
                result = (T)ser.Deserialize(tr);
            }
            return result;
        }


        private static void SerializeToFormatter(object obj, string path, IFormatter formatter)
        {
            using (FileStream fs = File.Create(path))
            {
                formatter.Serialize(fs, obj);
            }
        }
    }
}
SwDevMan81
  • 48,814
  • 22
  • 151
  • 184
ligaoren
  • 1,043
  • 1
  • 9
  • 10
  • 1
    The use of `IFormatter` is confusing as an answer to a question on XML serialization. Also, you should throw `ArgumentNullExeption`, not `NullReferenceException`. You should never throw `NullReferenceException`. – John Saunders Apr 09 '15 at 20:01
2

The easiest way to do it, that I have found.. Apply the System.Xml.Serialization.XmlArray attribute to it.

[System.Xml.Serialization.XmlArray] //This is the part that makes it work
List<object> serializableList = new List<object>();

XmlSerializer xmlSerializer = new XmlSerializer(serializableList.GetType());

serializableList.Add(PersonList);

using (StreamWriter streamWriter = System.IO.File.CreateText(fileName))
{
    xmlSerializer.Serialize(streamWriter, serializableList);
}

The serializer will pick up on it being an array and serialize the list's items as child nodes.

Nate Barbettini
  • 51,256
  • 26
  • 134
  • 147
Lee
  • 21
  • 2
0

If the XML output requirement can be changed you can always use binary serialization - which is better suited for working with heterogeneous lists of objects. Here's an example:

private void SerializeList(List<Object> Targets, string TargetPath)
{
    IFormatter Formatter = new BinaryFormatter();

    using (FileStream OutputStream = System.IO.File.Create(TargetPath))
    {
        try
        {
            Formatter.Serialize(OutputStream, Targets);
        } catch (SerializationException ex) {
            //(Likely Failed to Mark Type as Serializable)
            //...
        }
}

Use as such:

[Serializable]
public class Animal
{
    public string Home { get; set; }
}

[Serializable]
public class Person
{
    public string Name { get; set; }
}


public void ExampleUsage() {

    List<Object> SerializeMeBaby = new List<Object> {
        new Animal { Home = "London, UK" },
        new Person { Name = "Skittles" }
    };

    string TargetPath = Path.Combine(
        Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData),
        "Test1.dat");

    SerializeList(SerializeMeBaby, TargetPath);
}
slavoo
  • 5,798
  • 64
  • 37
  • 39
Robert Venables
  • 5,943
  • 1
  • 23
  • 35
  • Nice example. This is more what I was thinking - a number of different objects and I want to put them into one file. I prefer XML to binary though, but will try this. – Simon D Jul 31 '09 at 15:24
0

knowTypeList parameter let serialize with DataContractSerializer several known types:

private static void WriteObject(
        string fileName, IEnumerable<Vehichle> reflectedInstances, List<Type> knownTypeList)
    {
        using (FileStream writer = new FileStream(fileName, FileMode.Append))
        {
            foreach (var item in reflectedInstances)
            {
                var serializer = new DataContractSerializer(typeof(Vehichle), knownTypeList);
                serializer.WriteObject(writer, item);
            }
        }
    }
Sharunas Bielskis
  • 1,033
  • 1
  • 16
  • 25
0

Let Serialize and Deserialize be nameless object lists in C#:

// create a list of Shapes to serialize.
List<Shape> listOfShapes = new()
{
    new Circle { Colour = "Red", Radius = 2.5 },
    new Rectangle { Colour = "Blue", Height = 20.0, Width = 10.0 },
    new Circle { Colour = "Green", Radius = 8.0 },
    new Circle { Colour = "Purple", Radius = 12.3 },
    new Rectangle { Colour = "Blue", Height = 45.0, Width = 18.0 }
};
    
string filepath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.Desktop), "Testfile.xml");
    
//Serialize 
List<Type> types = new();
foreach(Shape s in listOfShapes)
{
   types.Add(s.GetType());
}
XmlSerializer serializerXml = new XmlSerializer(listOfShapes.GetType(),types.ToArray());
FileStream fsxml = File.Create(filepath);
serializerXml.Serialize(fsxml, listOfShapes);
fsxml.Close();
    
//Deserialize.
FileStream fsxmlr= File.Open(filepath, FileMode.Open);
List<Shape> loadedShapesXml = serializerXml.Deserialize(fsxmlr) as List<Shape>;
foreach (Shape item in loadedShapesXml)
{
    WriteLine("{0} is {1} and has an area of {2:N2}",
    item.GetType().Name, item.Colour, item.Area);
}
fsxmlr.Close();
vasilyrud
  • 825
  • 2
  • 10
  • 18