Loading...

How can I be notified if I deserialize XML to C# and there's no corresponding C# property?


I'm working with some third party XML that has no formally defined schema, only example XML. I have several thousand XML files from this third party. There is no guarantee that every possible element lies within one or more of these files. The third party service could send me a new file with a new element!

I can view these files and reverse engineer types relatively easily.

For example:

<MyObject>
    <MyProperty>Some value</MyProperty>
</MyObject>

Could deserialize to

public class MyObject
{
    public string MyProperty { get; set; }
}

No problems so far.

But what if I attempt to deserialize this:

<MyObject>
    <MyProperty>Some value</MyProperty>
    <MyOtherProperty>Some value</MyOtherProperty>
</MyObject>

into my class above? I want it to throw an exception, so I can be notified that my class does not accommodate MyOtherProperty.

Is there a way to do this?

I'd like to share the code I wrote using the accepted answer. Using the below utility method, I can deserialize without checks for unknown stuff, and with checks, by setting strict=true. I hope readers find this useful!

public static T XmlDeserialize<T>(string xml, bool strict = false)
{
    using (var stringReader = new StringReader(xml))
    {
        using (var xmlTextReader = new XmlTextReader(stringReader))
        {
            var xmlSerializer = new XmlSerializer(typeof(T));
            if (strict)
            {
                var options = new XmlDeserializationEvents();
                options.OnUnknownElement += (sender, args) =>
                {
                    throw new XmlDeserializationException(xml, $"Unexpected Element {args.Element.LocalName} on line {args.LineNumber}.");
                };
                options.OnUnknownAttribute += (sender, args) =>
                {
                    throw new XmlDeserializationException(xml, $"Unexpected Element: {args.Attr.LocalName} on line {args.LineNumber}.");
                };
                options.OnUnknownNode += (sender, args) =>
                {
                    throw new XmlDeserializationException(xml, $"Unexpected Element: {args.LocalName} on line {args.LineNumber}.");
                };
                return (T)xmlSerializer.Deserialize(xmlTextReader, options);
            }
            return (T)xmlSerializer.Deserialize(xmlTextReader);
        }
    }
}

And that exception class I'm throwing looks like this:

public class XmlDeserializationException : Exception
{
    public string Xml { get; private set; }

    public XmlDeserializationException(
        string xml, string message) : base (message)
    {
        Xml = xml;
    }
}

I can check my logs and look up the line number in the actual xml. Works perfectly. Thanks, pfx.

- - Source

Answers

answered 1 week ago pfx #1

Extend your class with an XmlAnyElement container.
Any unknown elements will end up in that array.
After deserialization check whether that array is empty.

public class MyObject
{
    [XmlAnyElement]
    public XmlElement[] UnknownElements;

    public string MyProperty { get; set; }
}

answered 1 week ago D.R. #2

One way of doing so would be to use your current object model and create an XSD out of it. You can then check new files against that XSD and throw if it doesn't validate.

answered 1 week ago pfx #3

The XmlSerializer has a Deserialize overload allowing to pass in an options element by which to hook to some events; eg. OnUnknownElement.

XmlDeserializationEvents options = new XmlDeserializationEvents();
    options.OnUnknownElement += (sender, args) => {
        XmlElement unknownElement = args.Element;
        // throw an Exception with this info.
    } ;

var o = serializer.Deserialize(xml, options) as MyObject;

The OnUnknowElement takes nested elements into account.

With the classes below

public class MyObject
{
    public string MyProperty { get; set; }

    public MyOtherObject Other { get; set; }
}

public class MyOtherObject
{
    public string SomeProperty { get; set; }
}

and the following xml

<MyObject>
    <MyProperty>Some value</MyProperty>
    <Other>
        <SomeProperty>...</SomeProperty>
        <UFO>...</UFO>
    </Other>
</MyObject>

The OnUnknowElement handler will trigger for the UFO element.

comments powered by Disqus