Search code examples
c#jsonjson.netopenstreetmapjson-deserialization

Deserialize Open Street Map JSON in C#


If I have this JSON where there is a header with the tags version, generator, om3s and elements. elements can be of type node or way and the associated JSON keys vary based on type. I am trying to use JsonSubTypes to convert each element type to a C# class.

Example JSON:

[
  {
    "version": 0.6,
    "generator": "Overpass API 0.7.55.7 8b86ff77",
    "osm3s": {
      "timestamp_osm_base": "2019-05-21T18:03:02Z",
      "copyright": "The data included in this document is from www.openstreetmap.org. The data is made available under ODbL."
    },
    "elements": [
      {
        "type": "node",
        "id": 4949106384,
        "lat": 32.2686857,
        "lon": -107.738218,
        "tags": {
          "highway": "turning_circle"
        }
      },
      {
        "type": "way",
        "id": 14527404,
        "nodes": [
          142882281,
          3048075541,
          1598998260
        ],
        "tags": {
          "highway": "residential",
          "name": "West Apple Street",
          "tiger:cfcc": "A41",
          "tiger:county": "Luna, NM",
          "tiger:name_base": "Apple",
          "tiger:name_direction_prefix": "W",
          "tiger:name_type": "St",
          "tiger:reviewed": "no"
        }
      }
    ]
  }
]

I am trying to deserialize it using:

var json = JsonConvert.DeserializeObject<OSMdata>(jsonText);

Where OSMdata looks like:

[JsonConverter(typeof(JsonSubtypes), "type")]
[JsonSubtypes.KnownSubType(typeof(Element.Node), "node")]
[JsonSubtypes.KnownSubType(typeof(Element.Edge), "way")]

public abstract class OSMdata
{
    public float version { get; set; }
    public string generator { get; set; }
    public Osm3s osm3s { get; set; }
    public Element[] elements { get; set; }
}

public class Osm3s : OSMdata
{
    public DateTime timestamp_osm_base { get; set; }
    public string copyright { get; set; }
}

public class Element : OSMdata
{
    public class Node : Element
    {
        public string type { get; } = "node";
        public long id { get; set; }
        public float lat { get; set; }
        public float lon { get; set; }
        public NodeTags tags { get; set; }
    }

    public class NodeTags : Node
    {
        public string highway { get; set; }
        public string _ref { get; set; }
    }

    public class Edge : Element
    {
        public string type { get; } = "way";
        public long id { get; set; }
        public long[] nodes { get; set; }
        public EdgeTags tags { get; set; }
    }

    public class EdgeTags : Edge
    {
        public string highway { get; set; }
        public string name { get; set; }
        public string cfcc { get; set; }
        public string county { get; set; }
        public string oneway { get; set; }
    }
}

Which returns:

Unhandled Exception: System.ArgumentNullException: Value cannot be null.
   at System.RuntimeType.MakeGenericType(Type[] instantiation)
   at JsonSubTypes.JsonSubtypes.CreateCompatibleList(Type targetContainerType, Type elementType)
   at JsonSubTypes.JsonSubtypes.ReadArray(JsonReader reader, Type targetType, JsonSerializer serializer)
   at JsonSubTypes.JsonSubtypes.ReadJson(JsonReader reader, Type objectType, JsonSerializer serializer)
   at Newtonsoft.Json.Serialization.JsonSerializerInternalReader.DeserializeConvertable(JsonConverter converter, JsonReader reader, Type objectType, Object existingValue)
   at Newtonsoft.Json.Serialization.JsonSerializerInternalReader.Deserialize(JsonReader reader, Type objectType, Boolean checkAdditionalContent)
   at Newtonsoft.Json.JsonSerializer.DeserializeInternal(JsonReader reader, Type objectType)
   at Newtonsoft.Json.JsonConvert.DeserializeObject(String value, Type type, JsonSerializerSettings settings)
   at Newtonsoft.Json.JsonConvert.DeserializeObject[T](String value, JsonSerializerSettings settings)
   at newapp.Program.Main(String[] args) in C:\Users\RDCRLDDH\source\repos\newapp\newapp\Program.cs:line 23

While I don't understand this error and am looking for a solution, here are a few questions I would like clarity on:

Questions

Am I properly constructing the class OSMdata? I think I am following the examples correctly, but am not sure if I am properly assigning the classes Node and Edge to the parent class OSMdata.

How will the deserializer know to assign the tag "tiger:cfcc" to the Cfcc property in EdgeTags?


Solution

  • I couldn't figure out how to deserialize directly from the JSON, but found a suitable workaround that converts the JSON to JArray and iterates though each element and converting to a C# class that is composed of nodes and edges.

    Using the class Element:

        [JsonConverter(typeof(JsonSubtypes), "type")]
        [JsonSubtypes.KnownSubType(typeof(Element.Node), "node")]
        [JsonSubtypes.KnownSubType(typeof(Element.Edge), "way")]
    
        public class Element
        {
            public class Node : Element
    
            {
                public string type { get; } = "node";
                public long id { get; set; }
                public float lat { get; set; }
                public float lon { get; set; }
                public NodeTags tags { get; set; }
            }
    
        public class NodeTags : Node
        {
            public string highway { get; set; }
            public string _ref { get; set; }
        }
    
        public class Edge : Element
        {
            public string type { get; } = "way";
            public long id { get; set; }
            public long[] nodes { get; set; }
            public EdgeTags tags { get; set; }
        }
    
        public class EdgeTags : Edge
        {
            [JsonProperty("highway")]
            public string Highway { get; set; }
    
            [JsonProperty("name")]
            public string Name { get; set; }
    
    
            [JsonProperty("tiger:cfcc")]
            public string cfcc { get; set; }
    
            [JsonProperty("tiger:county")]
            public string County { get; set; }
    
            [JsonProperty("oneway")]
            public string Oneway { get; set; }
        }
    

    You can parse thje json using:

    JArray jsonSearch = JArray.Parse(jsonText);
    

    You can then create a list consisting of each element using:

    IList<JToken> results = jsonSearch[0]["elements"].Children().ToList();
    

    Then you can iterate through results and convert the data to C# objects using:

    var element_list = new List<Element>();
    
    foreach (JObject element in results)
    {
        Element myelement = element.ToObject<Element>();
        element_list.Add(myelement);
    }
    

    Of the questions I posed, the second is still relevant.

    You can assign a property with an invalid name to a C# class using JsonProperty before you create the property name within the class.

    [JsonProperty("tiger:cfcc")]
    public string cfcc { get; set; }
    

    Thanks @dbc and other helpful comments for pointing me in the right direction!