Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Deserialize XML with multiple types

I'm trying to deserialize XML where some same name tags have different xsi types:

<user-defined-data-row>
  <field name="entity">
    <field-value xsi:type="field-text-valueType">
      <value>Test</value>
    </field-value>
  </field>
  <field name="expiry_date">
    <field-value xsi:type="field-date-valueType">
      <value>2001-10-07</value>
    </field-value>
  </field>
</user-defined-data-row>

This is easily achieved by deserializing the xml into this model:

[XmlRoot(ElementName = "field-value", Namespace = "http://www.crsoftwareinc.com/xml/ns/titanium/common/v1_0")]
[XmlType("field-text-valueType")]
public class Fieldvalue
{
    [XmlElement(ElementName = "value", Namespace = "http://www.crsoftwareinc.com/xml/ns/titanium/common/v1_0")]
    public string Value { get; set; }
}

The only thing that differs are the types in the XML:

field-text-valueType

field-date-valueType

How can I make the C# class interpret both types using something like

[XmlType("field-text-valueType")]

EDIT: deserializing not serializing

like image 592
OHMR Avatar asked May 23 '18 09:05

OHMR


People also ask

What is the correct way of using XML deserialization?

The most common usage of XML serialization is when XML data is sent from the database server to the client. Implicit serialization is the preferred method in most cases because it is simpler to code, and sending XML data to the client allows the Db2 client to handle the XML data properly.

What is XmlSerializer?

XML serialization is the process of converting an object's public properties and fields to a serial format (in this case, XML) for storage or transport. Deserialization re-creates the object in its original state from the XML output.

What is XmlSerializer in c#?

The XmlSerializer enables you to control how objects are encoded into XML, it has a number of constructors. If you use any of the constructors other than the one that takes a type then a new temporary assembly is created EVERY TIME you create a serializer, rather than only once.


1 Answers

The xsi:type attributes you are seeing in your XML are standard W3C XML Schema attributes that allow an element to explicitly specify its type; for details see here. As explained in Xsi:type Attribute Binding Support, XmlSerializer supports this mechanism for deserialization of polymorphic types, specifically by use of XmlIncludeAttribute.

First, define an abstract base class FieldValue as follows:

public static class XmlNamespaces
{
    public const string Crsoftwareinc = "http://www.crsoftwareinc.com/xml/ns/titanium/common/v1_0";
}

[XmlRoot("field-value", Namespace = XmlNamespaces.Crsoftwareinc)]
[XmlType("field-value", Namespace = XmlNamespaces.Crsoftwareinc)]
[XmlInclude(typeof(TextFieldValue)), 
XmlInclude(typeof(DateFieldValue))]
public abstract partial class FieldValue
{
    // It's not necessary to have this in the base class but I usually find it convenient.
    public abstract object GetValue();
}

Next, for each possible xsi:type="XXX" value, define a derived type of FieldValue whose XmlTypeAttribute.TypeName matches the xsi:type value. Decorate the base class with [XmlInclude(typeof(TDerivedFieldValue))] attributes for each (already shown above):

[XmlRoot("field-text-valueType", Namespace = XmlNamespaces.Crsoftwareinc)]
[XmlType("field-text-valueType", Namespace = XmlNamespaces.Crsoftwareinc)]
public class TextFieldValue : FieldValue
{
    [XmlElement("value")]
    public string Value { get; set; }

    public override object GetValue() { return Value; }
}

[XmlRoot("field-date-valueType", Namespace = XmlNamespaces.Crsoftwareinc)]
[XmlType("field-date-valueType", Namespace = XmlNamespaces.Crsoftwareinc)]
public class DateFieldValue : FieldValue
{
    [XmlElement("value", DataType = "date")]
    public DateTime Value { get; set; }

    public override object GetValue() { return Value; }
}

Then define the containing type corresponding to <field> and other, higher elements as follows:

[XmlRoot("field", Namespace = XmlNamespaces.Crsoftwareinc)]
[XmlType("field", Namespace = XmlNamespaces.Crsoftwareinc)]
public class Field
{
    [XmlAttribute("name")]
    public string Name { get; set; }

    [XmlElement("field-value")]
    public FieldValue FieldValue { get; set; }
}

[XmlRoot("user-defined-data-row", Namespace = XmlNamespaces.Crsoftwareinc)]
[XmlType("user-defined-data-row", Namespace = XmlNamespaces.Crsoftwareinc)]
public class UserDefinedDataRow
{
    [XmlElement("field")]
    public List<Field> Fields { get; set; }
}

// The XML for the root object is not shown so this is just a stub
[XmlRoot("root", Namespace = XmlNamespaces.Crsoftwareinc)]
[XmlType("root", Namespace = XmlNamespaces.Crsoftwareinc)]
public class RootObject
{
    [XmlElement("user-defined-data-row")]
    public List<UserDefinedDataRow> Rows { get; set; }
}

Notes:

  • If the base class FieldValue has a namespace specified in via XmlTypeAttribute.Namespace, then the derived classes must also, or else an error will get thrown by XmlSerializer.

    Once an [XmlType] namespace is defined, it automatically applies to all serialized properties, so it isn't necessary to specify the same namespace via [XmlElement(Namespace = "http://www.crsoftwareinc.com/xml/ns/titanium/common/v1_0")] attributes.

  • I got tired of repeatedly typing the namespace "http://www.crsoftwareinc.com/xml/ns/titanium/common/v1_0" and so I extracted it into a constant.

  • Other derived types of FieldType can be added easily, e.g.:

    [XmlRoot("field-decimal-valueType", Namespace = XmlNamespaces.Crsoftwareinc)]
    [XmlType("field-decimal-valueType", Namespace = XmlNamespaces.Crsoftwareinc)]
    public class DecimalFieldValue : FieldValue
    {
        [XmlElement("value")]
        public decimal Value { get; set; }
    
        public override object GetValue() { return Value; }
    }
    
    [XmlInclude(typeof(DecimalFieldValue))]
    public abstract partial class FieldValue { }
    

    Don't forget to add [XmlInclude(typeof(DecimalFieldValue))] when doing so.

  • If you have been given an XSD for the XML you are trying to deserialize that specifies the possible types of <field-value> via e.g. an <xsd:extension> element as shown in Generating XML Documents from XML Schemas: Abstract Types, then xsd.exe will generate classes that include an appropriate type hierarchy. But if you only have the XML, then xsd.exe and Paste XML as Classes will not generate a correct type hierarchy using whatever xsi:type attributes are present.

    For more about this limitation see xsi:type attribute messing up C# XML deserialization.

  • Your XML is not well-formed because it omits a declaration for the xsi: namespace. Also, a default namespace xmlns="http://www.crsoftwareinc.com/xml/ns/titanium/common/v1_0" is not defined so none of the elements are actually in this namespace. Thus I assume your XML is a fragment of some larger document that is valid, e.g.

    <root 
     xmlns:xsd="http://www.w3.org/2001/XMLSchema" 
     xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" 
     xmlns="http://www.crsoftwareinc.com/xml/ns/titanium/common/v1_0">
      <user-defined-data-row>
          <!-- Remainder as shown in the question -->
      </user-defined-data-row>
    </root>
    

Sample working .Net fiddle here.

like image 124
dbc Avatar answered Sep 22 '22 14:09

dbc