Search code examples
c#xmldeserializationxmlserializer

c# xml deserialization to object with colon and hyphen in xsi:type value


I have an issue when I try to deserialize my XML File to an object using the XmlSerializer class.

My XML file looks like this:

<fx:FIBEX xmlns:fx="http://www.asam.net/xml/fbx" xmlns:ho="http://www.asam.net/xml" xmlns:ethernet="http://www.asam.net/xml/fbx/ethernet" xmlns:it="http://www.asam.net/xml/fbx/it" xmlns:service="http://www.asam.net/xml/fbx/services" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="" VERSION="4.1.0">
  <fx:ELEMENTS>
    <fx:CLUSTERS>
      <fx:CLUSTER xsi:type="ethernet:CLUSTER-TYPE" ID="ID_CLUSTER_MAIN_1">
        <ho:SHORT-NAME>SomeIpDatabase</ho:SHORT-NAME>
        <fx:SPEED>1000000000</fx:SPEED>
        <fx:IS-HIGH-LOW-BIT-ORDER>false</fx:IS-HIGH-LOW-BIT-ORDER>
        <fx:BIT-COUNTING-POLICY>SAWTOOTH</fx:BIT-COUNTING-POLICY>
        <fx:PROTOCOL>ETHERNET</fx:PROTOCOL>
        <fx:PHYSICAL>OABR</fx:PHYSICAL>
        <fx:CHANNEL-REFS>
          <fx:CHANNEL-REF ID-REF="ID_CHANNEL_SOME_IP_1" />
        </fx:CHANNEL-REFS>
        <fx:MAX-FRAME-LENGTH>1500</fx:MAX-FRAME-LENGTH>
        <ethernet:MAC-MULTICAST-GROUPS>
          <ethernet:MAC-MULTICAST-GROUP ID="ID_CLUSTER_MAIN_1_ID_MACMULTICASTGROUP_SD_1">
            <ho:SHORT-NAME>SD</ho:SHORT-NAME>
            <ethernet:MAC-MULTICAST-ADDRESS>01:00:5E:40:FF:FB</ethernet:MAC-MULTICAST-ADDRESS>
          </ethernet:MAC-MULTICAST-GROUP>
          <ethernet:MAC-MULTICAST-GROUP ID="ID_CLUSTER_MAIN_1_ID_MACMULTICASTGROUP_BROADCAST_1">
            <ho:SHORT-NAME>BROADCAST</ho:SHORT-NAME>
            <ethernet:MAC-MULTICAST-ADDRESS>FF:FF:FF:FF:FF:FF</ethernet:MAC-MULTICAST-ADDRESS>
          </ethernet:MAC-MULTICAST-GROUP>
        </ethernet:MAC-MULTICAST-GROUPS>
      </fx:CLUSTER>
      <!--Additional CLUSTER elements omitted-->
    </fx:CLUSTERS>
  </fx:ELEMENTS>
  <!--PROJECT elements omitted-->
</fx:FIBEX>

When I try to deserialize the XML file now i receive the following error:

System.InvalidOperationException: Error in XML-Dokument (11,5). ---> System.InvalidOperationException: The specified type was not recognized: Name='CLUSTER-TYPE', Namespace='http://www.asam.net/xml/fbx/ethernet', at <CLUSTER xmlns='http://www.asam.net/xml/fbx'>.

My deserializer class looks like this:

public static T DeserializeXMLFileToObject<T>(string XmlFilename)
{
    T returnObject = default(T);
    if (string.IsNullOrEmpty(XmlFilename)) return default(T);

    try
    {
        StreamReader xmlStream = new StreamReader(XmlFilename);
        XmlSerializer serializer = new XmlSerializer(typeof(T));
        returnObject = (T)serializer.Deserialize(xmlStream);
    }
    catch (Exception ex)
    {
        Console.Write("{1} Es ist ein Fehler aufgetreten {0}", ex, DateTime.Now);
    }
    return returnObject;
}

The class that should contain the deserialized elements and attributes of the XML file looks like this:

[XmlRoot("FIBEX", Namespace = fxNameSpace)]
public class Fibextoobject
{
    [XmlElement("PROJECT", Namespace = fxNameSpace)]
    public Project project { get; set; }

    [XmlElement("ELEMENTS", Namespace = fxNameSpace)]
    public Elements elements { get; set; }

    public class Project
    {
        [XmlAttribute("OID", Namespace = hoNameSpace)]
        public string OID { get; set; }

        [XmlAttribute("ID")]
        public string ID { get; set; }

        [XmlElement("SHORT-NAME", Namespace = hoNameSpace)]
        public string shortname { get; set; }

        [XmlElement("LONG-NAME", Namespace = hoNameSpace)]
        public string longname { get; set; }
    }

    public class Elements
    {
        [XmlArray("CLUSTERS", Namespace = fxNameSpace)]
        [XmlArrayItem("CLUSTER", Namespace = fxNameSpace)]
        public List<Cluster> cluster { get; set; }

    }

    public class Cluster
    {
        [XmlAttribute("ID")]
        public string ID { get; set; }

        [XmlElement("SHORT-NAME", Namespace = hoNameSpace)]
        public string shortname { get; set; }

        [XmlElement("SPEED", Namespace = fxNameSpace)]
        public string speed { get; set; }
    }
}

How can i deserialize the XML file successfully using the xsi:type attribute with the colon and hyphen inside the value: xsi:type="ethernet:CLUSTER-TYPE" ?


Solution

  • Your problem is as follows. The xsi:type attribute, short for {http://www.w3.org/2001/XMLSchema-instance}type, is a w3c standard attribute that allows an element to explicitly assert its type, e.g. when it is a polymorphic subtype of the expected element type. XmlSerializer supports this attribute and will use it to determine the actual type of object to deserialize for such a polymorphic type. However, there are several caveats and restrictions in using this attribute:

    1. If xsi:type is present in the XML, then that element must be bound to a polymorphic type hierarchy. XmlSerializer will never just ignore the attribute. Thus you will need to introduce a derived subtype of Cluster to deserialize this XML, e.g. as follows:

      public class ClusterType : Cluster
      {
      }
      
    2. XmlSerializer requires that it be informed in advance of all possible subtypes using [XmlInclude(typeof(TDerivedType))]. Normally one would place this attribute on the base type:

      [XmlInclude(typeof(ClusterType))]
      // Add XmlInclude for all additional subtypes here.
      public class Cluster
      {
          // Remainder unchanged
      
    3. Your xsi:type value contains characters that cannot be included in a c# identifier:

      xsi:type="ethernet:CLUSTER-TYPE"
      

      In this case, XmlSerializer interprets the the value as a qualified name where the portion before the : is an XML namespace prefix of a valid namespace in scope, and the portion afterwards corresponds to the XmlTypeAttribute.TypeName of the polymorphic type. You can inform the serializer of the expected namespace and type by applying the [XmlType] attribute to the derived type as follows:

      [XmlType("CLUSTER-TYPE", Namespace = ethernetNameSpace)]
      public class ClusterType : Cluster
      {
      }
      

      Where ethernetNameSpace is defined as:

      public const string ethernetNameSpace = "http://www.asam.net/xml/fbx/ethernet";
      
    4. For some reason XmlSerializer requires the XmlTypeAttribute.Namespace to be initialized to something on the base type if it is set to anything at all on included derived types. The value of the namespace doesn't seem to matter when deserializing an instance of the derived type (though obviously it would when deserializing the base type), it just needs to be set to something. Even the empty string will work, e.g.:

      // The XmlTypeAttribute.Namespace below must be initialized to something if it is also initialized on the derived type.
      // However, the actual value does not need to be the same as the child's namespace, so modify to be something more appropriate
      // based on additional XML samples.
      [XmlType("CLUSTER", Namespace = "")] 
      [XmlInclude(typeof(ClusterType))]
      // Add XmlInclude for all additional subtypes here.
      public class Cluster
      {
          // Remainder unchanged
      

      If the base type XML namespace is not set, XmlSerializer will throw an exception with a misleading message:

      System.InvalidOperationException: There was an error generating the XML document. ---> System.InvalidOperationException: The type Fibextoobject+ClusterType was not expected. Use the XmlInclude or SoapInclude attribute to specify types that are not known statically.

      Since ClusterType is in fact included via [XmlInclude] the message is not helpful. It took a bit of experimentation to determine the actual problem.

    A working version of your Fibextoobject type which puts all of the above items into action is as follows:

    [XmlRoot("FIBEX", Namespace = fxNameSpace)]
    public partial class Fibextoobject
    {
        [XmlElement("PROJECT", Namespace = fxNameSpace)]
        public Project project { get; set; }
    
        [XmlElement("ELEMENTS", Namespace = fxNameSpace)]
        public Elements elements { get; set; }
    
        public class Project
        {
            [XmlAttribute("OID", Namespace = hoNameSpace)]
            public string OID { get; set; }
    
            [XmlAttribute("ID")]
            public string ID { get; set; }
    
            [XmlElement("SHORT-NAME", Namespace = hoNameSpace)]
            public string shortname { get; set; }
    
            [XmlElement("LONG-NAME", Namespace = hoNameSpace)]
            public string longname { get; set; }
        }
    
        public class Elements
        {
            [XmlArray("CLUSTERS", Namespace = fxNameSpace)]
            [XmlArrayItem("CLUSTER", Namespace = fxNameSpace)]
            public List<Cluster> cluster { get; set; }
        }
    
        [XmlType("CLUSTER-TYPE", Namespace = ethernetNameSpace)]
        public class ClusterType : Cluster
        {
        }
    
        // The XmlTypeAttribute.Namespace below must be initialized to something if it is also initialized on the derived type.
        // However, the actual value does not need to be the same as the child's namespace, so modify to be something more appropriate
        // based on additional XML samples.
        [XmlType("CLUSTER", Namespace = "")] 
        [XmlInclude(typeof(ClusterType))]
        // Add XmlInclude for all additional subtypes here.
        public class Cluster
        {
            [XmlAttribute("ID")]
            public string ID { get; set; }
    
            [XmlElement("SHORT-NAME", Namespace = hoNameSpace)]
            public string shortname { get; set; }
    
            [XmlElement("SPEED", Namespace = fxNameSpace)]
            public string speed { get; set; }
        }
    }
    
    public partial class Fibextoobject
    {
        public const string fxNameSpace = "http://www.asam.net/xml/fbx";
        public const string hoNameSpace = "http://www.asam.net/xml";
        public const string ethernetNameSpace = "http://www.asam.net/xml/fbx/ethernet";
        public const string itNameSpace = "http://www.asam.net/xml/fbx/it";
        public const string serviceNameSpace = "http://www.asam.net/xml/fbx/services";
    }
    

    Sample working Roslyn .Net fiddle.