GeoJSON and Go

Exploring GeoJSON with Go and its lack of generics

fires

GeoJSON is an open format for geographical data and is used in a wide range of software products and libraries. If we want to build a data source for mapping, having our API provide GeoJSON is beneficial as, most of the time, it can be directly used in any client-side product that is accessing data.

Golang is a strictly typed language, which means that expressing the data model of GeoJSON can be challenging, as the data structure will change depending on what geometry type you want to represent. However, there is a way to deal with this well, which will be demonstrated here.

GeoJSON describes geometries as Points, Lines or Polygons. There can also be collections of these, called MultiPoint, MultiLine, and MultiPolygon.

A Point is a pair of coordinates, or [2]float64

A Line, or a MultiPoint, is a list of points, or [][2]float64

A MultiLine is a list of Lines, or [][][2]float64

A polygon is also a list of lines, but with the additional requirement that each line must end at its beginning, so it is technically a ring, rather than a line. The first ring in the list is the outer part of the polygon, with the following rings being cut-outs of that original shape. Polygon can be expressed in Go as [][][2]float64

A MultiPolygon is simply a list of Polygons, or [][][][2]float64

Here is how you would describe these types in Go:

go
type GeoJSONPoint struct {
Type string `json:"type"`
Coordinates [2]float64 `json:"coordinates"`
}
// line or multipoint
type GeoJSONLine struct {
Type string `json:"type"`
Coordinates [][2]float64 `json:"coordinates"`
}
// polygon or multiline
type GeoJSONPolygon struct {
Type string `json:"type"`
Coordinates [][][2]float64 `json:"coordinates"`
}
type GeoJSONMultiPolygon struct {
Type string `json:"type"`
Coordinates [][][][2]float64 `json:"coordinates"`
}

The Type variable should always be set to the geometry type: "Point", "Line", "Polygon", "MultiPoint", "MultiLine", or "MultiPolygon"

GeoJSON can also define a "Geometry Collection" which is a list of any one of the above geometry types. So you could have a list with several Points, and a MultiPolygon, or any other combination.

This creates a problem in Go, as we don't have generics. We cannot just define a struct with a list of generic "geometries". This doesn't mean we can't work with GeoJSON. We just have to get creative.

The one thing structs all have in common is that they can be serialised as JSON. This means we can use json.RawMessage to create a generic list:

go
type GeoJSONGeometryCollection struct {
Type string `json:"type"` // will always be "GeometryCollection"
Geometries []json.RawMessage `json:"geometries"`
}

So how do we unmarshal this? We'll need to add this type first:

go
type GeoJSONGenericGeometry struct {
Type string `json:"type"`
Coordinates json.RawMessage `json:"coordinates"`
}

Now we have list of geometries as JSON, but we don't know what type they are until we unmarshal each one of them. So we need a generic geometry type to unmarshal those into first, so we can get the Type variable. Once we have that type variable, we can unmarshal it again into the actual type.

go
// Assume the variable geoJSON is a []byte with valid geojson
geoJSONGeometryCollection := GeoJSONGeometryCollection{}
json.Unmarshal(geoJSON, &geoJSONGeometryCollection)
for _, geometryJSON := range geoJSONGeometryCollection.Geometries {
generic := GeoJSONGenericGeometry{}
json.Unmarshal(geometryJSON, &generic)
switch generic.Type {
case "MultiPolygon":
multiPolygon := GeoJSONMultiPolygon{}
json.Unmarshal(geometryJSON, &multiPolygon)
// You can now use the multiPolygon
case "Polygon", "MultiLine":
polygon := GeoJSONPolygon{}
json.Unmarshal(geometryJSON, &polygon)
// You can now use the polygon
case "Line", "MultiPoint":
line := GeoJSONLine{}
json.Unmarshal(geometryJSON, &line)
// You can now use the line
case "Point":
point := GeoJSONPoint{}
json.Unmarshal(geometryJSON, &point)
// You can now use the point
default:
return nil, fmt.Errorf("invalid datatype")
}
}

Once you have the data in the correct structs, you are free to convert your GeoJSON into any other format you need, or do your own logic and predictions on the data.