Dondey Dondey - 1 month ago 10
C# Question

Unable to deserialize polymorphic list from xml

I deserialize from an xml file using XmlSerializer over classes generated by Xsd2Code from an xsd file with elements extending a base element.

Here is a simplified example:

<?xml version="1.0" encoding="utf-8"?>
<xs:schema attributeFormDefault="unqualified" elementFormDefault="qualified" xmlns:xs="http://www.w3.org/2001/XMLSchema">
<xs:complexType name="Vehicle" abstract="true">
<xs:sequence>
<xs:element name="Manufacturer" type="xs:string" nillable="false" />
</xs:sequence>
</xs:complexType>
<xs:complexType name="Car">
<xs:complexContent>
<xs:extension base="Vehicle">
<xs:sequence>
<xs:element name="Configuration" type="xs:string" nillable="false" />
</xs:sequence>
</xs:extension>
</xs:complexContent>
</xs:complexType>
<xs:complexType name="Truck">
<xs:complexContent>
<xs:extension base="Vehicle">
<xs:sequence>
<xs:element name="Load" type="xs:int" nillable="false" />
</xs:sequence>
</xs:extension>
</xs:complexContent>
</xs:complexType>
<xs:element name="Garage">
<xs:complexType>
<xs:sequence>
<xs:element name="Vehicles" type="Vehicle" minOccurs="0" maxOccurs="unbounded" nillable="false" />
</xs:sequence>
</xs:complexType>
</xs:element>
</xs:schema>


Generated code:

public partial class Garage
{
public Garage()
{
Vehicles = new List<Vehicle>();
}

public List<Vehicle> Vehicles { get; set; }
}
[System.Xml.Serialization.XmlIncludeAttribute(typeof(Truck))]
[System.Xml.Serialization.XmlIncludeAttribute(typeof(Car))]
public partial class Vehicle
{
public string Manufacturer { get; set; }
}
public partial class Truck : Vehicle
{
public int Load { get; set; }
}
public partial class Car : Vehicle
{
public string Configuration { get; set; }
}


The XML:

<?xml version="1.0" encoding="utf-8" ?>
<Garage>
<Vehicles>
<Vehicle>
<Manufacturer>Honda</Manufacturer>
<Configuration>Sedan</Configuration>
</Vehicle>
<Vehicle>
<Manufacturer>Volvo</Manufacturer>
<Load>40</Load>
</Vehicle>
</Vehicles>
</Garage>


And the deserializing code:

var serializer = new XmlSerializer(typeof(Garage));

using (var reader = File.OpenText("Settings.xml"))
{
var garage = (Garage)serializer.Deserialize(reader);
var car = garage.Vehicles[0] as Car;
Console.WriteLine(car.Configuration);
}


I get an exception
The specified type is abstract: name='Vehicle', namespace='', at <Vehicle xmlns=''>.
on the deserializing line.

If I remove the abstract attribute from the Vehicle element in XSD I get a null reference exception because
garage.Vehicles[0]
cannot be cast to
Car
.

I want to be able to deserialize and then cast into
Car
and
Truck
. How can I make this work?

dbc dbc
Answer

The basic problem is that your XML does not match your XSD. If you try to validate the XML using .Net (sample fiddle) you will see the following errors:

The element 'Vehicles' is abstract or its type is abstract.
The element 'Vehicles' has invalid child element 'Vehicle'. List of possible elements expected: 'Manufacturer'.

These errors have the following meaning:

  • The element 'Vehicles' has invalid child element 'Vehicle'. List of possible elements expected: 'Manufacturer'.

    The problem here is that your XSD specifies that the <Vehicles> list does not have a container element. Instead, it should just be a repeating set of elements like so:

    <Garage xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
      <Vehicles>
        <Manufacturer>Honda</Manufacturer>
        <Configuration>Sedan</Configuration>
      </Vehicles>
      <Vehicles>
        <Manufacturer>Volvo</Manufacturer>
        <Load>40</Load>
      </Vehicles>
    </Garage>
    

    For which the corresponding c# class is:

    public partial class Garage
    {
        public Garage()
        {
            Vehicles = new List<Vehicle>();
        }
    
        [XmlElement]
        public List<Vehicle> Vehicles { get; set; }
    }
    

    To specify an outer wrapper element with inner elements named <Vehicle>, your XSD would have to have an extra intermediate element ArrayOfVehicle:

    <?xml version="1.0" encoding="utf-8"?> 
    <xs:schema attributeFormDefault="unqualified" elementFormDefault="qualified" xmlns:xs="http://www.w3.org/2001/XMLSchema">   
    <xs:complexType name="Vehicle" abstract="true">
      <xs:sequence>
        <xs:element name="Manufacturer" type="xs:string" nillable="false" />
      </xs:sequence>   
    </xs:complexType>   
    <xs:complexType name="Car">
      <xs:complexContent>
        <xs:extension base="Vehicle">
          <xs:sequence>
            <xs:element name="Configuration" type="xs:string" nillable="false" />
          </xs:sequence>
        </xs:extension>
      </xs:complexContent>
    </xs:complexType>
    <xs:complexType name="Truck">
      <xs:complexContent>
        <xs:extension base="Vehicle">
          <xs:sequence>
            <xs:element name="Load" type="xs:int" nillable="false" />
          </xs:sequence>
        </xs:extension>
      </xs:complexContent>
    </xs:complexType>
    <!-- BEGIN CHANGES HERE -->
    <xs:complexType name="ArrayOfVehicle">
    <xs:sequence>
      <xs:element minOccurs="0" maxOccurs="unbounded" name="Vehicle" nillable="true" type="Vehicle" />
    </xs:sequence>
    </xs:complexType>
    <xs:element name="Garage">
      <xs:complexType>
        <xs:sequence>
          <xs:element minOccurs="0" maxOccurs="1" name="Vehicles" type="ArrayOfVehicle" />
        </xs:sequence>
      </xs:complexType>
    </xs:element>
    <!-- END CHANGES HERE -->
    </xs:schema>
    

    In your question, you wrote Here is a simplified example. Is there a chance the extra level of nesting in the XSD was manually omitted when writing the question?

  • The element 'Vehicles' is abstract or its type is abstract.

    You are trying to serialize a polymorphic list by specifying possible subtypes using XmlIncludeAttribute.

    When doing this, it is necessary for each polymorphic element in the XML to assert its actual type using the w3c standard attribute xsi:type (short for {http://www.w3.org/2001/XMLSchema-instance}type), as is explained in Xsi:type Attribute Binding Support. Only then will XmlSerializer know the correct, concrete type to which to deserialize each element. Thus your final XML should look like:

    <Garage xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
      <Vehicles xsi:type="Car">
        <Manufacturer>Honda</Manufacturer>
        <Configuration>Sedan</Configuration>
      </Vehicles>
      <Vehicles xsi:type="Truck">
        <Manufacturer>Volvo</Manufacturer>
        <Load>40</Load>
      </Vehicles>
    </Garage>
    

    Or, if you prefer to have an outer wrapper element for your vehicle collection:

    <Garage xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
      <Vehicles>
        <Vehicle xsi:type="Car">
          <Manufacturer>Honda</Manufacturer>
          <Configuration>Sedan</Configuration>
        </Vehicle>
        <Vehicle xsi:type="Truck">
          <Manufacturer>Volvo</Manufacturer>
          <Load>40</Load>
        </Vehicle>
      </Vehicles>
    </Garage>
    

    The latter XML can be successfully deserialized into your current Garage class without errors, as is shown here.

Incidentally, an easy way to debug this sort of problem is to create an instance of your Garage class in memory, populate its vehicle list, and serialize it. Having done so, you would have seen inconsistencies with the XML you were trying to deserialize.

Comments