3

I am using the .NET XmlSerializer class to deserialize GPX files.

There are two versions of the GPX standard:

  • <gpx xmlns="http://www.topografix.com/GPX/1/0"> ... </gpx>
  • <gpx xmlns="http://www.topografix.com/GPX/1/1"> ... </gpx>

Also, some GPX files do not specify a default namespace:

  • <gpx> ... </gpx>

My code needs to handle all three cases, but I can't work out how to get XmlSerializer to do it.

I am sure there must be a simple solution because this a common scenario, for example KML has the same issue.

MetaMapper
  • 968
  • 8
  • 22
  • If you can post more complete xml, I could help you with LinqToXml - otherwise I can't help with XmlSerializer. You could click my name and do a search of my user and `[xml]` to see all I've posted on it. – Chuck Savage Mar 26 '12 at 18:11

3 Answers3

5

I have done something similar to this a few times before, and this might be of use to you if you only have to deal with a small number of namespaces and you know them all beforehand. Create a simple inheritance hierarchy of classes, and add attributes to the different classes for the different namespaces. See the following code sample. If you run this program it gives the output:

Deserialized, type=XmlSerializerExample.GpxV1, data=1
Deserialized, type=XmlSerializerExample.GpxV2, data=2
Deserialized, type=XmlSerializerExample.Gpx, data=3

Here is the code:

using System;
using System.IO;
using System.Xml;
using System.Xml.Serialization;

[XmlRoot("gpx")]
public class Gpx {
        [XmlElement("data")] public int Data;
}

[XmlRoot("gpx", Namespace = "http://www.topografix.com/GPX/1/0")]
public class GpxV1 : Gpx {}

[XmlRoot("gpx", Namespace = "http://www.topografix.com/GPX/1/1")]
public class GpxV2 : Gpx {}

internal class Program {
    private static void Main() {
        var xmlExamples = new[] {
            "<gpx xmlns='http://www.topografix.com/GPX/1/0'><data>1</data></gpx>",
            "<gpx xmlns='http://www.topografix.com/GPX/1/1'><data>2</data></gpx>",
            "<gpx><data>3</data></gpx>",
        };

        var serializers = new[] {
            new XmlSerializer(typeof (Gpx)),
            new XmlSerializer(typeof (GpxV1)),
            new XmlSerializer(typeof (GpxV2)),
        };

        foreach (var xml in xmlExamples) {
            var textReader = new StringReader(xml);
            var xmlReader = XmlReader.Create(textReader);

            foreach (var serializer in serializers) {
                if (serializer.CanDeserialize(xmlReader)) {
                    var gpx = (Gpx)serializer.Deserialize(xmlReader);
                    Console.WriteLine("Deserialized, type={0}, data={1}", gpx.GetType(), gpx.Data);
                }
            }
        }
    }
}
John Jeffery
  • 990
  • 5
  • 19
  • Thanks John, this is quite a good solution. I wasn't aware of the CanDeserialize() method. This approach makes things nice and explicit in the code. The only downside is having to introduce the extra subclasses. But these could be hidden in the implementation of a Load method, and a result of type Gpx could be returned by upcasting. – MetaMapper Apr 05 '12 at 07:29
3

Here's the solution I came up with before the other suggestions came through:

  var settings = new XmlReaderSettings();
  settings.IgnoreComments = true;
  settings.IgnoreProcessingInstructions = true;
  settings.IgnoreWhitespace = true;
  using (var reader = XmlReader.Create(filePath, settings))
  {
    if (reader.IsStartElement("gpx"))
    {
      string defaultNamespace = reader["xmlns"];
      XmlSerializer serializer = new XmlSerializer(typeof(Gpx), defaultNamespace);
      gpx = (Gpx)serializer.Deserialize(reader);
    }
  }

This example accepts any namespace, but you could easily make it filter for a specific list of known namespaces.

MetaMapper
  • 968
  • 8
  • 22
1

Oddly enough you can't solve this nicely. Have a look at the deserialize section in this troubleshooting article. Especially where it states:

Only a few error conditions lead to exceptions during the deserialization process. The most common ones are:
•The name of the root element or its namespace did not match the expected name.
...

The workaround I use for this is to set the first namespace, try/catch the deserialize operation and if it fails because of the namespace I try it with the next one. Only if all namespace options fail do I throw the error.

From a really strict point of view you can argue that this behavior is correct since the type you deserialize to should represent a specific schema/namespace and then it doesn't make sense that it should also be able to read data from another schema/namespace. In practice this is utterly annoying though. File extenstion rarely change when versions change so the only way to tell if a .gpx file is v0 or v1 is to read the xml contents but the xmldeserializer won't unless you tell upfront which version it will be.

Eddy
  • 5,320
  • 24
  • 40
  • Thanks Eddy, that troubleshooting article is very good. Your approach is basically similar to John's, but his use of the CanDeserialize() method avoids the need for the try-catch block. – MetaMapper Apr 05 '12 at 07:37