Search code examples
c#xmlxml-serializationxml-deserialization

Trouble Deserializing/Serializing XML with Mixed Element Order in C#


I am facing an issue while deserializing/serializing XML into a C# object. The XML structure has mixed order of elements, and I am encountering problems with elements that have the same name but different purposes. Here's a simplified version of my C# class:

public class Uientry
{
    [XmlElement(ElementName = "u")]
    public string ID { get; set; }
    
    [XmlElement(ElementName = "s")]
    public List<string> Strings { get; set; }

    [XmlElement(ElementName = "i")]
    public List<string> Integer { get; set; }
    [XmlElement(ElementName = "additional_data")]
    public Additional_data Additional_data { get; set; }
}

The issue arises when the XML has elements with the same name but different purposes, and their order is not fixed. For example, the <i> elements (representing integers) can appear before and after <s> elements (representing strings), causing conflicts.

Important:I want that the order of the elements stays the same after Serializing

this is some of my xml code

<uientry>
    <u>606917248</u><!-- ID (80 d2 2c 24) -->
    <s>root</s><!-- title -->
    <s>CampaignRoot</s><!-- title2 -->
    <i>0</i><!-- x offset -->
    <i>0</i><!-- y offset -->
    <no /><!-- uientry flag 1 -->
    <yes /><!-- uientry flag 2 -->
    <byte>1</byte><!-- uientry flag 3 -->
    <no /><!-- uientry flag 4 -->
    <no /><!-- uientry flag 5 -->
    <no /><!-- uientry flag 6 -->
    <no /><!-- uientry flag 7 -->
    <yes /><!-- uientry flag 8 -->
    <no /><!-- uientry flag 9 -->
    <no /><!-- uientry flag 10 -->
    <no /><!-- uientry flag 11 -->
    <no /><!-- uientry flag 12 -->
    <yes />
    <unicode></unicode><!-- tooltip text -->
    <unicode></unicode><!-- tooltip id -->
    <i>0</i><!-- 00:00:00:00 --><!-- docking? -->
    <i>0</i><!-- 00:00:00:00 --><!-- docking x? -->
    <i>0</i><!-- 00:00:00:00 --><!-- docking y? -->
    <no />
    <i>0</i><!-- default state id -->
    <images count="1">
      <image>
        <u>606535040</u><!-- ID (80 fd 26 24) -->
        <s></s><!-- path -->
        <i>1280</i><!-- x size -->
        <i>960</i><!-- y size -->
        <no />
      </image>
    </images>
    <i>0</i><!-- 00:00:00:00 --><!-- mask image? -->
    <i>0</i>
    <states count="0">
    </states>
    <children count="0">
    </children>
    <additional_data type="none" />
</uientry>

How can I modify my C# class or use attributes to handle such a scenario? Is there a way to instruct the XML deserializer to consider the purpose of the element and handle it accordingly?


Solution

  • Your XML has a schema that includes a sequence of choice element. A choice element indicates that one of a fixed set of elements -- <u>, <i>, <yes>, <no>, <unicode> and so on -- will occur in the XML. XmlSerializer supports choice elements as is explained in Choice Element Binding Support:

    If individual choice elements' types differ along with their names, Xsd.exe applies only XmlElementAttribute attributes to a public member. If they differ only by name, Xsd.exe applies an XmlChoiceIdentifierAttribute in addition, and adds extra logic for making the choice.

    Thus, as explained in this answer to Xml Deserialization - Merging two elements into a single List<T> object, you can either model this as a list of polymorphic types, or a pair of arrays of choice types and values. Personally I think the first option is easier, so let's go with that.

    First define some base type UientryItem, then for every element that might appear in <uientry>, create a subtype corresponding to that element, indicating the name via an XmlTypeAttribute.TypeName attribute property:

    public abstract class UientryItem;
    
    [XmlType("u"), XmlRoot("u")]
    public class UientryId : UientryItem
    {
        [XmlText] public string Value { get; set; }
    }
    
    [XmlType("s"), XmlRoot("s")]
    public class UientryTitle : UientryItem
    {
        [XmlText] public string Value { get; set; }
    }
    
    [XmlType("no"), XmlRoot("no")]
    public class UientryNo : UientryItem;
    
    [XmlType("yes"), XmlRoot("yes")]
    public class UientryYes : UientryItem;
    
    [XmlType("byte"), XmlRoot("byte")]
    public class UientryByte : UientryItem
    {
        [XmlText] public byte Value { get; set; }
    }
    
    [XmlType("unicode"), XmlRoot("unicode")]
    public class UientryUnicode : UientryItem
    {
        [XmlText] public string Value { get; set; }
    }
    
    [XmlType("i"), XmlRoot("i")]
    public class UientryInt : UientryItem
    {
        [XmlText] public int Value { get; set; }
    }
    
    [XmlType("images"), XmlRoot("images")]
    public class UientryImages : UientryItem
    {
        [XmlAttribute("count")]
        public int Count { get => Images?.Count?? 0; set { } }
    
        [XmlElement("image")]
        public List<UientryImage> Images { get; set; } = new();
    }
    
    [XmlType("image"), XmlRoot("image")]
    public class UientryImage
    {
        [XmlElement(typeof(UientryId)), 
         XmlElement(typeof(UientryTitle)),
         XmlElement(typeof(UientryNo)),
         XmlElement(typeof(UientryYes)),
         XmlElement(typeof(UientryByte)),
         XmlElement(typeof(UientryUnicode)),
         XmlElement(typeof(UientryInt)),
         XmlElement(typeof(UientryImages)),
         XmlElement(typeof(UientryStates)),
         XmlElement(typeof(UientryChildren)),
         XmlElement(typeof(UientryAdditionalData))]
        public List<UientryItem> Items { get; set; } = new();
    }
    
    [XmlType("states"), XmlRoot("states")]
    public class UientryStates : UientryItem
    {
        [XmlAttribute("count")] public int Count { get; set; }
        // TODO: fill in appropriate properties
    }
    
    [XmlType("children"), XmlRoot("children")]
    public class UientryChildren : UientryItem
    {
        [XmlAttribute("count")] public int Count { get; set; }
        // TODO: fill in appropriate properties
    }
    
    [XmlType("additional_data"), XmlRoot("additional_data")]
    public class UientryAdditionalData : UientryItem
    {
        [XmlAttribute("type")] public string Type { get; set; }
        // TODO: fill in appropriate properties
    }
    

    Next define Uientry as follows:

    [XmlType("uientry"), XmlRoot("uientry")]
    public class Uientry
    {
        [XmlElement(typeof(UientryId)), 
         XmlElement(typeof(UientryTitle)),
         XmlElement(typeof(UientryNo)),
         XmlElement(typeof(UientryYes)),
         XmlElement(typeof(UientryByte)),
         XmlElement(typeof(UientryUnicode)),
         XmlElement(typeof(UientryInt)),
         XmlElement(typeof(UientryImages)),
         XmlElement(typeof(UientryStates)),
         XmlElement(typeof(UientryChildren)),
         XmlElement(typeof(UientryAdditionalData))]
        public List<UientryItem> Items { get; set; } = new();
    }
    

    And you will be able to deserialize your <uientry> using a new XmlSerializer(typeof(Uientry)) e.g. as follows:

    using var textReader = new StringReader(xmlString);
    var serializer = new XmlSerializer(typeof(Uientry));
    
    var uiEntry = (Uientry)serializer.Deserialize(textReader);
    

    And the element order will be preserved via the order of items in the public List<UientryItem> Items { get; set; } list.

    Notes:

    • If you are sure that some of the elements occur at the beginning or end of the <uientry> element -- for instance, that <additional_data> always appears last -- then you can capture that with a separate property as you did in your original Uientry class.

    Demo fiddle here.