GeoJSON
The geojson module contains an implementation of
RFC 7946: The GeoJSON Format.
See below for constructing GeoJSON objects using the DSL.
Installation
Section titled “Installation”commonMain { dependencies { implementation("org.maplibre.spatialk:geojson:0.8.0") }}dependencies { implementation("org.maplibre.spatialk:geojson-jvm:0.8.0")}GeoJSON Objects
Section titled “GeoJSON Objects”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
Section titled “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
Section titled “Position”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 valuesposition.longitudeposition.latitudeposition.altitudePosition position = new Position(-75.0, 45.0);
// Access valuesdouble longitude = position.getLongitude();double latitude = position.getLatitude();Double altitude = position.getAltitude();[-75.0, 45.0]A Point is a single Position.
val point = Point(Position(-75.0, 45.0))
println(point.coordinates.longitude)Point point = new Point(-75.0, 45.0);
System.out.println(point.getCoordinates().getLongitude());{ "type": "Point", "coordinates": [-75.0, 45.0]}MultiPoint
Section titled “MultiPoint”A MultiPoint is an array of Positions.
val multiPoint = MultiPoint(Position(-75.0, 45.0), Position(-79.0, 44.0))MultiPoint multiPoint = new MultiPoint(new Position(-75.0, 45.0), new Position(-79.0, 44.0));{ "type": "MultiPoint", "coordinates": [[-75.0, 45.0], [-79.0, 44.0]]}LineString
Section titled “LineString”A LineString is a sequence of two or more Positions.
val lineString = LineString(Position(-75.0, 45.0), Position(-79.0, 44.0))LineString lineString = new LineString(new Position(-75.0, 45.0), new Position(-79.0, 44.0));{ "type": "LineString", "coordinates": [[-75.0, 45.0], [-79.0, 44.0]]}MultiLineString
Section titled “MultiLineString”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)), )MultiLineString multiLineString = new MultiLineString( new LineString(new Position(12.3, 45.6), new Position(78.9, 12.3)), new LineString(new Position(87.6, 54.3), new Position(21.9, 56.4)));{ "type": "MultiLineString", "coordinates": [ [[12.3, 45.6], [78.9, 12.3]], [[87.6, 54.3], [21.9, 56.4]] ]}Polygon
Section titled “Polygon”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), ), )Polygon polygon = new Polygon( new LineString( new Position(-79.87, 43.42), new Position(-78.89, 43.49), new Position(-79.07, 44.02), new Position(-79.95, 43.87), new Position(-79.87, 43.42)), new LineString( new Position(-79.75, 43.81), new Position(-79.56, 43.85), new Position(-79.7, 43.88), new Position(-79.75, 43.81)));{ "type": "Polygon", "coordinates": [ [[-79.87, 43.42], [-78.89, 43.49], [-79.07, 44.02], [-79.95, 43.87], [-79.87, 43.42]], [[-79.75, 43.81], [-79.56, 43.85], [-79.7, 43.88], [-79.75, 43.81]] ]}MultiPolygon
Section titled “MultiPolygon”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)Polygon polygon = new Polygon( new LineString( new Position(-79.87, 43.42), new Position(-78.89, 43.49), new Position(-79.07, 44.02), new Position(-79.95, 43.87), new Position(-79.87, 43.42)), new LineString( new Position(-79.75, 43.81), new Position(-79.56, 43.85), new Position(-79.7, 43.88), new Position(-79.75, 43.81)));MultiPolygon multiPolygon = new MultiPolygon(polygon, polygon);{ "type": "MultiPolygon", "coordinates": [ [ [[-79.87, 43.42], [-78.89, 43.49], [-79.07, 44.02], [-79.95, 43.87], [-79.87, 43.42]], [[-79.75, 43.81], [-79.56, 43.85], [-79.7, 43.88], [-79.75, 43.81]] ], [ [[-79.87, 43.42], [-78.89, 43.49], [-79.07, 44.02], [-79.95, 43.87], [-79.87, 43.42]], [[-79.75, 43.81], [-79.56, 43.85], [-79.7, 43.88], [-79.75, 43.81]] ] ]}GeometryCollection
Section titled “GeometryCollection”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 begeometryCollection.forEach { geometry -> // ...}Point point = new Point(-75.0, 45.0);LineString lineString = new LineString(new Position(-75.0, 45.0), new Position(-79.0, 44.0));GeometryCollection<Geometry> geometryCollection = new GeometryCollection<>(point, lineString);{ "type": "GeometryCollection", "geometries": [ { "type": "Point", "coordinates": [-75.0, 45.0] }, { "type": "LineString", "coordinates": [[-75.0, 45.0], [-79.0, 44.0]] } ]}Feature
Section titled “Feature”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 // 9999val geometry: Point = feature.geometryPoint point = new Point(-75.0, 45.0);
JsonObjectBuilder properties = new JsonObjectBuilder();JsonElementBuildersKt.put(properties, "size", 9999);Feature<Point, JsonObject> feature = new Feature<>(point, properties.build(), null, null);
Integer size = Feature.getIntProperty(feature, "size");Point geometry = feature.getGeometry(); // point{ "type": "Feature", "geometry": { "type": "Point", "coordinates": [-75.0, 45.0] }, "properties": { "size": 9999 }}FeatureCollection
Section titled “FeatureCollection”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 -> // ...}Point point = new Point(-75.0, 45.0);Feature<Point, JsonObject> pointFeature = new Feature<>(point, null, null, null);FeatureCollection<Point, JsonObject> featureCollection = new FeatureCollection<>(pointFeature);{ "type": "FeatureCollection", "features": [ { "type": "Feature", "geometry": { "type": "Point", "coordinates": [-75.0, 45.0] }, "properties":null } ]}Foreign members
Section titled “Foreign members”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"JsonObjectBuilder properties = new JsonObjectBuilder();JsonElementBuildersKt.put(properties, "name", "Station");
JsonObjectBuilder extras = new JsonObjectBuilder();JsonElementBuildersKt.put(extras, "title", "Example Feature");
Feature<Point, JsonObject> feature = new Feature<>(new Point(-75.0, 45.0), properties.build(), null, null, extras.build());
JsonElement title = GeoJsonObject.getForeignMember(feature, "title");String titleValue = ((JsonPrimitive) title).getContent(); // "Example Feature"{ "type": "Feature", "geometry": { "type": "Point", "coordinates": [-75.0, 45.0] }, "properties": { "name": "Station" }, "title": "Example Feature"}BoundingBox
Section titled “BoundingBox”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..-170splitAtAntimeridian() 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 PositionsBoundingBox bbox = new BoundingBox(11.6, 45.1, 12.7, 45.7);Position southwest = bbox.getSouthwest();Position northeast = bbox.getNortheast();[11.6,45.1,12.7,45.7]Serialization
Section titled “Serialization”To JSON
Section titled “To JSON”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)Point point = new Point(-75.0, 45.0);Feature<Point, JsonObject> feature = new Feature<>(point, null, null, null);FeatureCollection<Point, JsonObject> featureCollection = new FeatureCollection<>(feature);String json = FeatureCollection.toJson(featureCollection);System.out.println(json);From JSON
Section titled “From 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 Pointval myPoint: Point = Point.fromJson("""{"type": "MultiPoint", "coordinates": [[-75.0, 45.0]]}""")
// Returns null if an error occursval nullable: Point? = Point.fromJsonOrNull("""{"type": "MultiPoint", "coordinates": [[-75.0, 45.0]]}""")// Throws exception if the JSON cannot be deserialized to a PointPoint myPoint = Point.fromJson("{\"type\": \"MultiPoint\", \"coordinates\": [[-75.0, 45.0]]}");
// Returns null if an error occursPoint nullable = 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" } } """, )GeoJSON Builders
Section titled “GeoJSON Builders”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.
Geometry
Section titled “Geometry”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.
MultiPoint
Section titled “MultiPoint”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)}MultiPointBuilder builder = new MultiPointBuilder();builder.add(-75.0, 45.0, null);builder.add(new Position(-78.0, 44.0));builder.add(new Point(88.0, 34.0));MultiPoint multiPoint = builder.build();{ "type": "MultiPoint", "coordinates": [ [-75.0, 45.0], [-78.0, 44.0], [88.0, 34.0] ]}LineString
Section titled “LineString”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)}LineStringBuilder builder = new LineStringBuilder();builder.add(45.0, 45.0, null);builder.add(0.0, 0.0, null);LineString lineString = builder.build();{ "type": "LineString", "coordinates": [[45.0, 45.0], [0.0, 0.0]]}MultiLineString
Section titled “MultiLineString”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) }}LineStringBuilder lineBuilder = new LineStringBuilder();lineBuilder.add(45.0, 45.0, null);lineBuilder.add(0.0, 0.0, null);LineString simpleLine = lineBuilder.build();
MultiLineStringBuilder builder = new MultiLineStringBuilder();builder.add(simpleLine);
LineStringBuilder lineBuilder2 = new LineStringBuilder();lineBuilder2.add(44.4, 55.5, null);lineBuilder2.add(55.5, 66.6, null);builder.add(lineBuilder2.build());
MultiLineString multiLineString = builder.build();{ "type": "MultiLineString", "coordinates": [ [[45.0, 45.0], [0.0, 0.0]], [[44.4, 55.5], [55.5, 66.6]] ]}Polygon
Section titled “Polygon”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) }}PolygonBuilder builder = new PolygonBuilder();
LineString ring1 = new LineString( new Position(45.0, 45.0), new Position(0.0, 0.0), new Position(12.0, 12.0), new Position(45.0, 45.0));builder.add(ring1);
LineString ring2 = new LineString( new Position(4.0, 4.0), new Position(2.0, 2.0), new Position(3.0, 3.0), new Position(4.0, 4.0));builder.add(ring2);
Polygon polygon = builder.build();{ "type": "Polygon", "coordinates": [ [[45.0, 45.0], [0.0, 0.0], [12.0, 12.0], [45.0, 45.0]], [[4.0, 4.0], [2.0, 2.0], [3.0, 3.0], [4.0, 4.0]] ]}MultiPolygon
Section titled “MultiPolygon”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) } }}PolygonBuilder polyBuilder = new PolygonBuilder();LineString ring1 = new LineString( new Position(45.0, 45.0), new Position(0.0, 0.0), new Position(12.0, 12.0), new Position(45.0, 45.0));polyBuilder.add(ring1);
LineString ring2 = new LineString( new Position(4.0, 4.0), new Position(2.0, 2.0), new Position(3.0, 3.0), new Position(4.0, 4.0));polyBuilder.add(ring2);Polygon simplePolygon = polyBuilder.build();
MultiPolygonBuilder builder = new MultiPolygonBuilder();builder.add(simplePolygon);
PolygonBuilder polyBuilder2 = new PolygonBuilder();LineString ring3 = new LineString( new Position(12.0, 0.0), new Position(0.0, 12.0), new Position(-12.0, 0.0), new Position(5.0, 5.0), new Position(12.0, 0.0));polyBuilder2.add(ring3);builder.add(polyBuilder2.build());
MultiPolygon multiPolygon = builder.build();{ "type": "MultiPolygon", "coordinates": [ [ [[45.0, 45.0], [0.0, 0.0], [12.0, 12.0], [45.0, 45.0]], [[4.0, 4.0], [2.0, 2.0], [3.0, 3.0], [4.0, 4.0]] ], [ [[12.0, 0.0], [0.0, 12.0], [-12.0, 0.0], [5.0, 5.0], [12.0, 0.0]] ] ]}GeometryCollection
Section titled “GeometryCollection”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)}Point simplePoint = new Point(-75.0, 45.0, 100.0);
LineString simpleLine = new LineString(new Position(45.0, 45.0), new Position(0.0, 0.0));
PolygonBuilder polyBuilder = new PolygonBuilder();LineString ring1 = new LineString( new Position(45.0, 45.0), new Position(0.0, 0.0), new Position(12.0, 12.0), new Position(45.0, 45.0));polyBuilder.add(ring1);
LineString ring2 = new LineString( new Position(4.0, 4.0), new Position(2.0, 2.0), new Position(3.0, 3.0), new Position(4.0, 4.0));polyBuilder.add(ring2);Polygon simplePolygon = polyBuilder.build();
GeometryCollectionBuilder<Geometry> builder = new GeometryCollectionBuilder<>();builder.add(simplePoint);builder.add(simpleLine);builder.add(simplePolygon);GeometryCollection<Geometry> geometryCollection = builder.build();{ "type": "GeometryCollection", "geometries": [ { "type": "Point", "coordinates": [-75.0, 45.0, 100.0] }, { "type": "LineString", "coordinates": [[45.0, 45.0], [0.0, 0.0]] }, { "type": "Polygon", "coordinates": [ [[45.0, 45.0], [0.0, 0.0], [12.0, 12.0], [45.0, 45.0]], [[4.0, 4.0], [2.0, 2.0], [3.0, 3.0], [4.0, 4.0]] ] } ]}Feature
Section titled “Feature”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) } }Point point = new Point(-75.0, 45.0);JsonObjectBuilder propsBuilder = new JsonObjectBuilder();JsonElementBuildersKt.put(propsBuilder, "name", "Hello World");JsonElementBuildersKt.put(propsBuilder, "value", 13);JsonElementBuildersKt.put(propsBuilder, "cool", true);
FeatureBuilder<Point, JsonObject> builder = new FeatureBuilder<>(point, propsBuilder.build());builder.setId("point1");builder.setBbox(new BoundingBox(-76.9, 44.1, -74.2, 45.7));Feature<Point, JsonObject> feature = builder.build();{ "type": "Feature", "geometry": { "type": "Point", "coordinates": [-75.0, 45.0] }, "properties": { "name": "Hello World", "value": 13, "cool": true }, "id": "point1", "bbox": [-76.9, 44.1, -74.2, 45.7]}FeatureCollection
Section titled “FeatureCollection”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") } }}Point point = new Point(-75.0, 45.0);JsonObjectBuilder propsBuilder = new JsonObjectBuilder();JsonElementBuildersKt.put(propsBuilder, "name", "Hello");
FeatureBuilder<Point, JsonObject> featureBuilder = new FeatureBuilder<>(point, propsBuilder.build());Feature<Point, JsonObject> feature = featureBuilder.build();
FeatureCollectionBuilder<Point, JsonObject> builder = new FeatureCollectionBuilder<>();builder.add(feature);FeatureCollection<Point, JsonObject> featureCollection = builder.build();{ "type": "FeatureCollection", "features": [ { "type": "Feature", "geometry": { "type": "Point", "coordinates": [-75.0, 45.0] }, "properties": { "name": "Hello" } } ]}