Skip to content

GeoJSON

The geojson module contains an implementation of RFC 7946: The GeoJSON Format.

See below for constructing GeoJSON objects using the DSL.

commonMain {
dependencies {
implementation("org.maplibre.spatialk:geojson:0.8.0")
}
}

The GeoJsonObject interface represents all GeoJSON objects. All GeoJSON objects can have a bbox property specified on them which is a BoundingBox that represents the bounds of that object’s geometry.

Geometry objects are a sealed hierarchy of classes that inherit from the Geometry class. This allows for exhaustive type checks in Kotlin using a when block.

val geometry: Geometry = getSomeGeometry()
val type =
when (geometry) {
is Point -> "Point"
is MultiPoint -> "MultiPoint"
is LineString -> "LineString"
is MultiLineString -> "MultiLineString"
is Polygon -> "Polygon"
is MultiPolygon -> "MultiPolygon"
is GeometryCollection<*> -> "GeometryCollection"
}

All seven types of GeoJSON geometries are implemented and summarized below. Full documentation can be found in the API pages.

Position is a DoubleArray-backed class where longitude, latitude, and optionally an altitude are accessible as properties. Coordinates follow the order specified in RFC 7946: [longitude, latitude, altitude?]. The class supports destructuring in Kotlin.

No latitude or longitude range validation is performed. Latitudes outside ±90° and longitudes outside ±180° are accepted and round-trip through serialization unchanged. Position.wrapped() normalizes longitude into [-180, 180) while leaving latitude and altitude unchanged.

val position = Position(-75.0, 45.0)
val (longitude, latitude, altitude) = position
// Access values
position.longitude
position.latitude
position.altitude

A Point is a single Position.

-75.0
val point = Point(Position(-75.0, 45.0))
println(point.coordinates.longitude)

A MultiPoint is an array of Positions.

val multiPoint = MultiPoint(Position(-75.0, 45.0), Position(-79.0, 44.0))

A LineString is a sequence of two or more Positions.

val lineString = LineString(Position(-75.0, 45.0), Position(-79.0, 44.0))

A MultiLineString is an array of LineStrings.

val multiLineString =
MultiLineString(
listOf(Position(12.3, 45.6), Position(78.9, 12.3)),
listOf(Position(87.6, 54.3), Position(21.9, 56.4)),
)

A Polygon is an array of rings. Each ring is a sequence of points with the last point matching the first point to indicate a closed area. The first ring defines the outer shape of the polygon, while all the following rings define “holes” inside the polygon.

val polygon =
Polygon(
listOf(
Position(-79.87, 43.42),
Position(-78.89, 43.49),
Position(-79.07, 44.02),
Position(-79.95, 43.87),
Position(-79.87, 43.42),
),
listOf(
Position(-79.75, 43.81),
Position(-79.56, 43.85),
Position(-79.7, 43.88),
Position(-79.75, 43.81),
),
)

A MultiPolygon is an array of Polygons.

val polygon =
listOf(
listOf(
Position(-79.87, 43.42),
Position(-78.89, 43.49),
Position(-79.07, 44.02),
Position(-79.95, 43.87),
Position(-79.87, 43.42),
),
listOf(
Position(-79.75, 43.81),
Position(-79.56, 43.85),
Position(-79.7, 43.88),
Position(-79.75, 43.81),
),
)
val multiPolygon = MultiPolygon(polygon, polygon)

A GeometryCollection contains multiple, heterogeneous geometries.

val point = Point(Position(-75.0, 45.0))
val lineString = LineString(Position(-75.0, 45.0), Position(-79.0, 44.0))
val geometryCollection = GeometryCollection(point, lineString)
// Can be iterated over and used in any way a Collection<T> can be
geometryCollection.forEach { geometry ->
// ...
}

A Feature can contain a Geometry object, as well as a set of data properties, and optionally a commonly used identifier (id).

Properties can be any object that serializes into a JSON object. For dynamic or unknown property schemas, use JsonObject. For known schemas, use a @Serializable data class. Helper methods for accessing properties are available when properties are of type JsonObject (see the API documentation for details).

val point = Point(Position(-75.0, 45.0))
val feature = Feature(point, properties = buildJsonObject { put("size", 9999) })
val size: Number? = feature.properties["size"]?.jsonPrimitive?.doubleOrNull // 9999
val geometry: Point = feature.geometry

A FeatureCollection is a collection of multiple features. It implements the Collection interface and can be used in any place that a collection can be used.

val point = Point(Position(-75.0, 45.0))
val pointFeature = Feature(point, null)
val featureCollection = FeatureCollection(pointFeature)
featureCollection.forEach { feature ->
// ...
}

GeoJSON objects may include foreign members—extra keys on a GeoJSON object beyond those defined by the specification. JSON encode flattens them as sibling keys. CBOR and Protobuf drop them.

@Serializable data class TitleMembers(val title: String)
val feature =
buildFeature(
geometry = Point(-75.0, 45.0),
properties = buildJsonObject { put("name", "Station") },
) {
foreignMembers = foreignMembersOf(TitleMembers(title = "Example Feature"))
}
val titleMembers = feature.decodeForeignMembers<TitleMembers>()
val title = titleMembers.title // "Example Feature"

The BoundingBox class is used to represent the bounding boxes that can be set for any GeoJsonObject. Like the Position class, bounding boxes are backed by a DoubleArray with each component accessible by its property (southwest and northeast). Bounding boxes also support destructuring.

No coordinate range validation or southwest/northeast ordering is enforced. Under the RFC 7946 Section 5.2 antimeridian convention, a crossing box has an east longitude less than its west longitude, such as 170° to -170°. Continuous, unwrapped bounds such as 170° to 190° are also accepted and serialized unchanged.

val bounds = BoundingBox(west = 170.0, south = -10.0, east = 190.0, north = 10.0)
val parts = bounds.splitAtAntimeridian() // 170..180 and -180..-170

splitAtAntimeridian() accepts either encoding and returns non-crossing boxes within [-180, 180], preserving latitude and altitude bounds. An already non-crossing box in that range is returned unchanged; boxes in other world copies are normalized. A continuous span of at least 360° returns one full-world box from -180° to 180°.

val bbox = BoundingBox(west = 11.6, south = 45.1, east = 12.7, north = 45.7)
val (southwest, northeast) = bbox // Two Positions

Any GeoJsonObject can be serialized to a JSON string using the toJson() method.

val point = Point(Position(-75.0, 45.0))
val feature = Feature(point, null)
val featureCollection = FeatureCollection(feature)
val json = featureCollection.toJson()
println(json)

The fromJson and fromJsonOrNull companion (or static) functions are available on each GeoJsonObject class to decode each type of object from a JSON string.

// Throws exception if the JSON cannot be deserialized to a Point
val myPoint: Point =
Point.fromJson("""{"type": "MultiPoint", "coordinates": [[-75.0, 45.0]]}""")
// Returns null if an error occurs
val nullable: Point? =
Point.fromJsonOrNull("""{"type": "MultiPoint", "coordinates": [[-75.0, 45.0]]}""")

Like with encoding, Spatial-K objects can also be decoded using kotlinx.serialization using the GeoJson serializer.

val feature: Feature<*, *> =
GeoJson.jsonFormat.decodeFromString(
serializer<Feature<Geometry, JsonObject?>>(),
"""
{
"type": "Feature",
"geometry": {
"type": "Point",
"coordinates": [102.0, 20.0]
},
"properties": {
"name": "point name"
}
}
""",
)

It’s recommended to construct GeoJSON objects in-code using builder classes. In Kotlin, these are available through a convenient DSL. In Java, use the builder classes directly.

Each geometry type more complex than Point has a corresponding DSL.

A GeoJSON object’s bbox value can be assigned in any of the DSLs.

The MultiPoint builder uses add() to add positions or Point geometries.

val myPoint = Point(88.0, 34.0)
val multiPoint = buildMultiPoint {
add(-75.0, 45.0)
add(Position(-78.0, 44.0))
add(myPoint)
}

A LineString contains two or more positions, in order. The builder uses add() to add positions. The order in which positions are added is the order that the line will follow.

val lineString = buildLineString {
add(45.0, 45.0)
add(0.0, 0.0)
}

The MultiLineString builder uses addLineString() to define line strings inline, or add() to add existing LineString objects.

val simpleLine = buildLineString {
add(45.0, 45.0)
add(0.0, 0.0)
}
val multiLineString = buildMultiLineString {
add(simpleLine)
// Inline LineString creation
addLineString {
add(44.4, 55.5)
add(55.5, 66.6)
}
}

The Polygon builder uses addRing() (Kotlin DSL) or add() with LineString objects (Java/Kotlin) to define linear rings. The first ring is the exterior ring with four or more positions. The last position must be the same as the first position. All subsequent rings represent interior rings (i.e., holes) in the polygon.

val simpleLine = buildLineString {
add(45.0, 45.0)
add(0.0, 0.0)
}
val polygon = buildPolygon {
addRing {
// LineStrings can be used as part of a ring
add(Position(45.0, 45.0))
add(Position(0.0, 0.0))
add(12.0, 12.0)
}
addRing {
add(4.0, 4.0)
add(2.0, 2.0)
add(3.0, 3.0)
}
}

The MultiPolygon builder uses addPolygon() to define polygons inline, or add() to add existing Polygon objects.

val simplePolygon = buildPolygon {
addRing {
add(45.0, 45.0)
add(0.0, 0.0)
add(12.0, 12.0)
}
addRing {
add(4.0, 4.0)
add(2.0, 2.0)
add(3.0, 3.0)
}
}
val multiPolygon = buildMultiPolygon {
add(simplePolygon)
addPolygon {
addRing {
add(12.0, 0.0)
add(0.0, 12.0)
add(-12.0, 0.0)
add(5.0, 5.0)
}
}
}

The GeometryCollection builder provides addPoint(), addLineString(), addPolygon(), addMultiPoint(), addMultiLineString(), addMultiPolygon(), and addGeometryCollection() to define geometries inline (Kotlin only), or add() to add existing geometry objects.

val simplePoint = Point(-75.0, 45.0, 100.0)
val simpleLine = buildLineString {
add(45.0, 45.0)
add(0.0, 0.0)
}
val simplePolygon = buildPolygon {
addRing {
add(45.0, 45.0)
add(0.0, 0.0)
add(12.0, 12.0)
}
addRing {
add(4.0, 4.0)
add(2.0, 2.0)
add(3.0, 3.0)
}
}
val geometryCollection = buildGeometryCollection {
add(simplePoint)
add(simpleLine)
add(simplePolygon)
}

The Feature builder constructs a Feature object with a geometry, bounding box, id, and properties. Properties can be any serializable object, such as a JsonObject built with buildJsonObject from kotlinx.serialization.

val feature =
buildFeature(geometry = Point(-75.0, 45.0)) {
setId("point1")
bbox = BoundingBox(-76.9, 44.1, -74.2, 45.7)
properties = buildJsonObject {
put("name", "Hello World")
put("value", 13)
put("cool", true)
}
}

The FeatureCollection builder uses addFeature() to define features inline (Kotlin only), or add() to add existing Feature objects.

val featureCollection = buildFeatureCollection {
addFeature {
geometry = Point(-75.0, 45.0)
properties = buildJsonObject { put("name", "Hello") }
}
}