Sinaesthetic
Sinaesthetic

Reputation: 12192

How can I deserialize xml with 2 different types of the same xml element and property name?

In the database, we have an xml field that contains 2 validation schemas; the old one does not have a namespace, the new one does. The reason for this is that we had to version one of the properties. Here is an example of the differences:

Version 1

<PropertyA>
  <PropertyA1>false</PropertyA1>
  <PropertyA2>3.23</PropertyA2>
</PropertyA>

Version 2

<ts:PropertyA xmlns:ts="http://www.example.com/v2">
  <ts:PropertyA1>false</ts:PropertyA2>
  <ts:PropertyA2>
    <ts:PropertyA2a>
        <ts:PropertyA2a1>City 1</ts:PropertyA2a1>
        <ts:PropertyA2a2>3.23</ts:PropertyA2a2>
    </ts:PropertyA2a>
    <ts:PropertyA2b>
        <ts:PropertyA2b1>City 2</ts:PropertyA2b1>
        <ts:PropertyA2b2>1.21</ts:PropertyA2b2>
    </ts:PropertyA2b>
  </ts:PropertyA2>
</ts:PropertyA>

Basically, we just create multiple options for PropertyA2...

So now the isue is deserialization. This object needs to be deserialized into the same data object in the app code and the problem is that the element name is the same so the serializer is obviously having trouble figuring out which object to deserialize into since sometimes the database will return Version 1 and sometimes it will return Version 2.

Here is an example of the data class being used for serialization and my current approach that isn't quite working:

[Serializable]
public class MyDataClass
{
    // ... other stuff

    [XmlElement(Name = "PropertyA", typeof(V1.PropertyA), Namespace = "")]
    public V1.PropertyA PropertyAV1 { get ;set; }

    [XmlElement(Name = "PropertyA", typeof(V2.PropertyA), Namespace = "http://www.example.com/v2")]
    public V2.PropertyA PropertyAV2 { get; set; }
}

[Serializable]
public class V1.PropertyA
{
    public bool PropertyA1 { get; set; }

    public decimal PropertyA2 { get; set; }
}

[Serializable]
public class V2.PropertyA
{
    public bool PropertyA1 { get; set; }

    public List<SomeOtherThing> PropertyA2 { get; set; }
}

When I go to deserialize V1, it works fine. When I go to deserialize V2, i get an error Did not expect <ts:PropertyA xmlns:ts="http://www.example.com/v2"> so I'm thinking there's a parameter I'm missing in the deserialize method:

public MyDataClass Deserialize(string xml)
{
    var s = new XmlSerializer(typeof (MyDataClass));
    MyDataClass info = null;
    using (var r = new StringReader(xml))
    {
        info = (MyDataClass) s.Deserialize(r);
    }
    return info;
}

I believe you can set the expected namespace in the serializer, but since I don't know what the namespace is going to be until I actually inspect the xml document, I'm not sure how to proceed.

So my question is this: Is what I'm trying to do even possible? Am I on the right track? Is there a better solution that is maybe less contrived? How can I have the serializer deal with the new namespace and deserialize to the correct properties?

Upvotes: 2

Views: 3710

Answers (2)

Sinaesthetic
Sinaesthetic

Reputation: 12192

It seems that what I was trying to do was either unachievable or no one with the right level of xml fu saw this thread :(.

So anyway, what I ended up doing was adding an extra column to the database with the version number of the xml contract. Since everything in there was the same, I just called it V1.

I then read that info out into app code and used the version number to drive a factory. Basically, if v1, then deserialize to this, if v2, deserialize to this other thing.

And of course, to support that, I simply created a new data object that had the appropriate structure to support v2. I'm not happy with it, but it works and is flexible enough :/

Upvotes: 1

Angle.Bracket
Angle.Bracket

Reputation: 1520

You can't.

The problem here is that you have to hardcode MyDataClass according to a single XMLSchema. If the XMLSchema alters, MyDataClass is no longer a valid target for the XMLSerializer's deserialize method, which is why you're getting the 'Did not expect ...' error message. In this case, when reading the V2 xml data stream, the deserialize method tries to fill MyDataClass#PropertyAV1 with the content of <ts:PropertyA2> and there is no way of telling it to instead fill MyDataClass#PropertyAV2. Even if there was a way to achieve this, you'd be stuck with an undefined value for MyDataClass#PropertyAV1 in the object of type MyDataClass.

So there are two solutions to the problem at hand :

a) Stick with XMLSerializer and define class MyDataClass like so

    public class MyDataClass 
    {    
        // only one Property here, as there's only one root element in the xml
        // and this single Property is not bound to a specific XML element
        [XmlAnyElement]    
        public PropertyA PropertyA { get ;set; }
    }

You then have to analyze the contents of PropertyA yourself and build some logic around it, see here for more details :

http://msdn.microsoft.com/en-us/library/system.xml.serialization.xmlanyelementattribute.aspx

b) Dispense the XMLSerializer, read the XML data stream with XMLReader and do the all the parsing of the xml yourself, also add logic to create the according C# objects, depending on the the type of xml you've read.

Obviously, both solutions require more coding on the C# side, but with solution b) you'll have the chance of gaining a performance benefit, as XMLSerializer#deserialize most probably builds a DOM tree to create the C# object from, which the XMLReader doesn't do.

Upvotes: 1

Related Questions