An open API service indexing awesome lists of open source software.

https://github.com/folt/marshmallow-geojson

Marshmallow schema validation for GeoJson
https://github.com/folt/marshmallow-geojson

geo geojson gis json json-schema linux marshmallow openapi python python3 rest swagger

Last synced: 15 days ago
JSON representation

Marshmallow schema validation for GeoJson

Awesome Lists containing this project

README

          

[![GitHub Actions status for master branch](https://github.com/folt/marshmallow-geojson/actions/workflows/main.yml/badge.svg)](https://github.com/folt/marshmallow-geojson/actions?query=workflow%3A%22Python+package%22)
[![Latest PyPI package version](https://badge.fury.io/py/marshmallow-geojson.svg)](https://pypi.org/project/marshmallow-geojson/)
[![Codecov](https://codecov.io/gh/folt/marshmallow-geojson/branch/master/graph/badge.svg?token=B5ATYXLBHO)](https://codecov.io/gh/folt/marshmallow-geojson)
[![Downloads](https://pepy.tech/badge/marshmallow-geojson)](https://pepy.tech/project/marshmallow-geojson)

# marshmallow-geojson 🌍

A schema-based, Marshmallow library for validating and working with [GeoJSON](http://geojson.org/) data according to [RFC 7946](https://tools.ietf.org/html/rfc7946) specification.

## Supported GeoJSON Objects

| GeoJSON Objects | Status | Description |
|------------------------|--------|--------------------------------------|
| Point | ✅ | A single geographic coordinate |
| MultiPoint | ✅ | Multiple points |
| LineString | ✅ | A sequence of connected points forming a line |
| MultiLineString | ✅ | Multiple line strings |
| Polygon | ✅ | A closed area, optionally with holes |
| MultiPolygon | ✅ | Multiple polygons |
| GeometryCollection | ✅ | Collection of different geometry types |
| Feature | ✅ | Geometry with properties |
| FeatureCollection | ✅ | Collection of features |
| Bbox | ✅ | Bounding box validation |

## Installation

marshmallow-geojson is compatible with Python 3.10 and up.

The recommended way to install is via [poetry](https://python-poetry.org/):

```shell
poetry add marshmallow_geojson
```

Using pip to install is also possible:

```shell
pip install marshmallow-geojson
```

## Quick Start

```python
from marshmallow_geojson import GeoJSONSchema

# Create a schema instance
schema = GeoJSONSchema()

# Load GeoJSON from JSON string
geojson_text = '{"type": "Point", "coordinates": [-105.01621, 39.57422]}'
data = schema.loads(geojson_text)
print(data)
# {'type': 'Point', 'coordinates': [-105.01621, 39.57422]}

# Load GeoJSON from dictionary
geojson_dict = {"type": "Point", "coordinates": [-105.01621, 39.57422]}
data = schema.load(geojson_dict)
print(data)
# {'type': 'Point', 'coordinates': [-105.01621, 39.57422]}

# Dump GeoJSON to JSON string
json_str = schema.dumps(geojson_dict)
print(json_str)
# '{"type": "Point", "coordinates": [-105.01621, 39.57422]}'
```

## GeoJSON Types with Visualizations

Type
Visualization
Usage

Point
A single geographic coordinate
Point

```python
from marshmallow_geojson import GeoJSONSchema

data = {
"type": "Point",
"coordinates": [-105.01621, 39.57422]
}
schema = GeoJSONSchema()
point = schema.load(data)

# Or with altitude
data_3d = {
"type": "Point",
"coordinates": [-105.01621, 39.57422, 100.5]
}
point_3d = schema.load(data_3d)

# From JSON string
json_str = '{"type": "Point", "coordinates": [-105.01621, 39.57422]}'
point = schema.loads(json_str)
```

MultiPoint
Multiple points
MultiPoint

```python
from marshmallow_geojson import GeoJSONSchema

data = {
"type": "MultiPoint",
"coordinates": [
[-105.01621, 39.57422],
[-80.666513, 35.053994]
]
}
schema = GeoJSONSchema()
multi_point = schema.load(data)

# Dump to JSON
json_output = schema.dumps(multi_point)
```

LineString
A sequence of connected points forming a line
LineString

```python
from marshmallow_geojson import GeoJSONSchema

data = {
"type": "LineString",
"coordinates": [
[-99.113159, 38.869651],
[-99.0802, 38.85682],
[-98.822021, 38.85682],
[-98.448486, 38.848264]
]
}
schema = GeoJSONSchema()
line_string = schema.load(data)

# Minimal LineString (2 points)
minimal = {
"type": "LineString",
"coordinates": [[-99.113159, 38.869651], [-99.0802, 38.85682]]
}
line = schema.load(minimal)
```

MultiLineString
Multiple line strings
MultiLineString

```python
from marshmallow_geojson import GeoJSONSchema

data = {
"type": "MultiLineString",
"coordinates": [
[[-105.019898, 39.574997],
[-105.019598, 39.574898],
[-105.019061, 39.574782]],
[[-105.017173, 39.574402],
[-105.01698, 39.574385],
[-105.016636, 39.574385]]
]
}
schema = GeoJSONSchema()
multi_line_string = schema.load(data)

# With bbox
data_with_bbox = {
"type": "MultiLineString",
"bbox": [-180.0, -90.0, 180.0, 90.0],
"coordinates": [[[-105.019898, 39.574997], [-105.019598, 39.574898]]]
}
result = schema.load(data_with_bbox)
```

Polygon
A closed area, optionally with holes
Polygon

```python
from marshmallow_geojson import GeoJSONSchema

# Simple polygon
data = {
"type": "Polygon",
"coordinates": [
[[100, 0],
[101, 0],
[101, 1],
[100, 1],
[100, 0]]
]
}
schema = GeoJSONSchema()
polygon = schema.load(data)

# Polygon with holes (interior rings)
data_with_holes = {
"type": "Polygon",
"coordinates": [
[[100, 0], [101, 0], [101, 1], [100, 1], [100, 0]], # Exterior
[[100.2, 0.2], [100.8, 0.2], [100.8, 0.8], [100.2, 0.8], [100.2, 0.2]] # Hole
]
}
polygon_with_holes = schema.load(data_with_holes)
```

MultiPolygon
Multiple polygons
MultiPolygon

```python
from marshmallow_geojson import GeoJSONSchema

data = {
"type": "MultiPolygon",
"coordinates": [
[[[107, 7], [108, 7],
[108, 8], [107, 8],
[107, 7]]],
[[[100, 0], [101, 0],
[101, 1], [100, 1],
[100, 0]]]
]
}
schema = GeoJSONSchema()
multi_polygon = schema.load(data)

# Serialize back to dict
serialized = schema.dump(multi_polygon)
```

GeometryCollection
Collection of different geometry types
GeometryCollection

```python
from marshmallow_geojson import GeoJSONSchema

data = {
"type": "GeometryCollection",
"geometries": [
{
"type": "Point",
"coordinates": [-80.660805, 35.049392]
},
{
"type": "Polygon",
"coordinates": [[[-80.664582, 35.044965],
[-80.663874, 35.04428],
[-80.662586, 35.04558],
[-80.663444, 35.046036],
[-80.664582, 35.044965]]]
},
{
"type": "LineString",
"coordinates": [[-80.662372, 35.059509],
[-80.662693, 35.059263],
[-80.662844, 35.05893]]
}
]
}
schema = GeoJSONSchema()
geometry_collection = schema.load(data)

# Empty GeometryCollection
empty = {
"type": "GeometryCollection",
"geometries": []
}
empty_collection = schema.load(empty)
```

## Features and FeatureCollections

### Feature

A geometry with properties.

```python
from marshmallow_geojson import GeoJSONSchema

# Basic Feature
data = {
"type": "Feature",
"properties": {
"name": "Dinagat Islands",
"population": 10000
},
"geometry": {
"type": "Polygon",
"coordinates": [
[
[-80.724878, 35.265454],
[-80.722646, 35.260338],
[-80.720329, 35.260618],
[-80.71681, 35.255361],
[-80.704793, 35.268397],
[-80.724878, 35.265454]
]
]
}
}
schema = GeoJSONSchema()
feature = schema.load(data)

# Feature with Point geometry
point_feature = {
"type": "Feature",
"geometry": {
"type": "Point",
"coordinates": [-74.006, 40.7128]
},
"properties": {
"name": "New York",
"population": 8336817
}
}
feature = schema.load(point_feature)

# Feature with null geometry (unlocated feature)
null_geometry_feature = {
"type": "Feature",
"geometry": None,
"properties": {
"name": "Unknown Location"
}
}
feature = schema.load(null_geometry_feature)

# Feature with ID
feature_with_id = {
"type": "Feature",
"id": "feature-123",
"geometry": {
"type": "Point",
"coordinates": [-105.01621, 39.57422]
},
"properties": {
"name": "Test Feature"
}
}
feature = schema.load(feature_with_id)

# Feature with bbox
feature_with_bbox = {
"type": "Feature",
"bbox": [-180.0, -90.0, 180.0, 90.0],
"geometry": {
"type": "Point",
"coordinates": [-105.01621, 39.57422]
},
"properties": {}
}
feature = schema.load(feature_with_bbox)
```

### FeatureCollection

A collection of features.

```python
from marshmallow_geojson import GeoJSONSchema

# Basic FeatureCollection
data = {
"type": "FeatureCollection",
"features": [
{
"type": "Feature",
"geometry": {
"type": "Point",
"coordinates": [-80.870885, 35.215151]
},
"properties": {
"name": "Location 1"
}
},
{
"type": "Feature",
"geometry": {
"type": "Polygon",
"coordinates": [
[
[-80.724878, 35.265454],
[-80.722646, 35.260338],
[-80.720329, 35.260618],
[-80.704793, 35.268397],
[-80.724878, 35.265454]
]
]
},
"properties": {
"name": "Location 2"
}
}
]
}
schema = GeoJSONSchema()
feature_collection = schema.load(data)

# Empty FeatureCollection
empty_fc = {
"type": "FeatureCollection",
"features": []
}
empty_collection = schema.load(empty_fc)

# FeatureCollection with mixed geometry types
mixed_fc = {
"type": "FeatureCollection",
"features": [
{
"type": "Feature",
"geometry": {"type": "Point", "coordinates": [-105.01621, 39.57422]},
"properties": {"type": "point"}
},
{
"type": "Feature",
"geometry": {
"type": "LineString",
"coordinates": [[-99.113159, 38.869651], [-99.0802, 38.85682]]
},
"properties": {"type": "line"}
},
{
"type": "Feature",
"geometry": {
"type": "Polygon",
"coordinates": [[[100, 0], [101, 0], [101, 1], [100, 1], [100, 0]]]
},
"properties": {"type": "polygon"}
}
]
}
mixed_collection = schema.load(mixed_fc)

# FeatureCollection with bbox
fc_with_bbox = {
"type": "FeatureCollection",
"bbox": [-180.0, -90.0, 180.0, 90.0],
"features": [
{
"type": "Feature",
"geometry": {"type": "Point", "coordinates": [-105.01621, 39.57422]},
"properties": {}
}
]
}
collection = schema.load(fc_with_bbox)

# Serialize FeatureCollection to JSON
json_output = schema.dumps(feature_collection)
```

## Universal GeoJSONSchema

marshmallow-geojson provides a universal `GeoJSONSchema` that automatically handles all GeoJSON object types. This is a unique feature not available in other GeoJSON libraries.

```python
from marshmallow_geojson import GeoJSONSchema

schema = GeoJSONSchema()

# Automatically handles Point
point_data = {"type": "Point", "coordinates": [-105.01621, 39.57422]}
point = schema.load(point_data)

# Automatically handles Feature
feature_data = {
"type": "Feature",
"geometry": {"type": "Point", "coordinates": [-105.01621, 39.57422]},
"properties": {"name": "Test"}
}
feature = schema.load(feature_data)

# Automatically handles FeatureCollection
fc_data = {
"type": "FeatureCollection",
"features": [feature_data]
}
feature_collection = schema.load(fc_data)

# Works with mixed types in many=True mode
schema_many = GeoJSONSchema(many=True)
mixed_data = [point_data, feature_data, fc_data]
results = schema_many.load(mixed_data)

# Load from JSON string - automatically detects type
json_point = '{"type": "Point", "coordinates": [-105.01621, 39.57422]}'
point = schema.loads(json_point)

json_feature = '{"type": "Feature", "geometry": {"type": "Point", "coordinates": [-105.01621, 39.57422]}, "properties": {}}'
feature = schema.loads(json_feature)

# Get specific schema for a type
point_schema_class = schema.get_schema("Point")
feature_schema_class = schema.get_schema("Feature")

# Handle multiple objects with many=True
json_array = '[{"type": "Point", "coordinates": [-105.01621, 39.57422]}, {"type": "LineString", "coordinates": [[-99.113159, 38.869651], [-99.0802, 38.85682]]}]'
schema_many = GeoJSONSchema(many=True)
results = schema_many.loads(json_array)
# Returns list of validated objects
```

## GeometriesSchema

For working with geometry objects only (excluding Feature and FeatureCollection), use `GeometriesSchema`:

```python
from marshmallow_geojson import GeometriesSchema

schema = GeometriesSchema()

# Works with all geometry types
point_data = {"type": "Point", "coordinates": [-105.01621, 39.57422]}
point = schema.load(point_data)

polygon_data = {"type": "Polygon", "coordinates": [[[100, 0], [101, 0], [101, 1], [100, 1], [100, 0]]]}
polygon = schema.load(polygon_data)

# Rejects Feature and FeatureCollection
# schema.load({"type": "Feature", ...}) # Raises ValidationError
```

## Custom Properties Schemas

You can define typed properties schemas for type-safe feature properties:

```python
from marshmallow.fields import Str, Int, Nested
from marshmallow_geojson import GeoJSONSchema, PropertiesSchema, FeatureSchema

class CityPropertiesSchema(PropertiesSchema):
name = Str(required=True)
population = Int(required=True)
country = Str(required=True)

class CityFeatureSchema(FeatureSchema):
properties = Nested(
CityPropertiesSchema,
required=True,
)

class CityGeoJSONSchema(GeoJSONSchema):
feature_schema = CityFeatureSchema

# Usage
schema = CityGeoJSONSchema()
data = {
"type": "Feature",
"properties": {
"name": "New York",
"population": 8336817,
"country": "USA"
},
"geometry": {
"type": "Point",
"coordinates": [-74.006, 40.7128]
}
}
city = schema.load(data)
print(city['properties']['name']) # "New York"
print(city['properties']['population']) # 8336817
```

## Marshmallow-Specific Features

marshmallow-geojson supports all standard Marshmallow schema features:

### Field Filtering (only/exclude)

```python
from marshmallow_geojson import GeoJSONSchema

# Include only specific fields
schema = GeoJSONSchema(only=('type', 'geometry'))
data = schema.load(feature_data)
# Only 'type' and 'geometry' fields are included

# Exclude specific fields
schema = GeoJSONSchema(exclude=('properties',))
data = schema.load(feature_data)
# 'properties' field is excluded
```

### Partial Loading

```python
from marshmallow_geojson import GeoJSONSchema

schema = GeoJSONSchema(partial=True)
# Allows partial data loading
partial_data = {
"type": "Feature",
"geometry": {"type": "Point", "coordinates": [-105.01621, 39.57422]}
}
result = schema.load(partial_data, partial=('properties',))
```

### Unknown Field Handling

```python
from marshmallow_geojson import GeoJSONSchema

# Exclude unknown fields
schema = GeoJSONSchema(unknown='exclude')
data_with_extra = {
"type": "Point",
"coordinates": [-105.01621, 39.57422],
"extra_field": "extra_value"
}
result = schema.load(data_with_extra)
# 'extra_field' is automatically excluded

# Raise error on unknown fields
schema = GeoJSONSchema(unknown='raise')
# schema.load(data_with_extra) # Raises ValidationError
```

### Many Mode

Handle multiple GeoJSON objects at once:

```python
from marshmallow_geojson import GeoJSONSchema

schema = GeoJSONSchema(many=True)

# Load multiple objects
data_list = [
{"type": "Point", "coordinates": [-105.01621, 39.57422]},
{"type": "LineString", "coordinates": [[-99.113159, 38.869651], [-99.0802, 38.85682]]},
{"type": "Feature", "geometry": {"type": "Point", "coordinates": [-80.870885, 35.215151]}, "properties": {}}
]
results = schema.load(data_list)

# Works with JSON strings too
json_str = '[{"type": "Point", "coordinates": [-105.01621, 39.57422]}]'
results = schema.loads(json_str)
```

## Validation

marshmallow-geojson automatically validates:

- **Coordinate ranges**: Longitude must be between -180 and 180, latitude between -90 and 90
- **Geometry types**: Ensures correct type strings according to RFC 7946
- **Structure**: Validates GeoJSON object structure and required fields
- **Linear Rings**: Polygon rings must have at least 4 positions and be closed
- **LineString**: Must have at least 2 positions
- **Bounding Box**: Validates bbox structure (2, 4, or 6 elements) and coordinate order
- **Type mixing**: Prevents forbidden members according to RFC 7946 Section 7.1

### Example: Invalid Coordinates

```python
from marshmallow_geojson import GeoJSONSchema
from marshmallow.exceptions import ValidationError

schema = GeoJSONSchema()
try:
data = {"type": "Point", "coordinates": [200, 50]} # Invalid longitude (> 180)
schema.load(data)
except ValidationError as e:
print(e.messages)
# {'coordinates': ['Longitude must be between -180, 180']}
```

### Example: Invalid Polygon

```python
from marshmallow_geojson import GeoJSONSchema
from marshmallow.exceptions import ValidationError

schema = GeoJSONSchema()
try:
# Polygon ring must have at least 4 positions and be closed
data = {
"type": "Polygon",
"coordinates": [[[100, 0], [101, 0], [100, 0]]] # Only 3 positions, not closed
}
schema.load(data)
except ValidationError as e:
print(e.messages)
# {'coordinates': ['Linear Ring must have at least 4 positions']}
```

## Bounding Box Validation

marshmallow-geojson includes comprehensive bounding box validation:

```python
from marshmallow.fields import Float, List
from marshmallow_geojson import PointSchema
from marshmallow_geojson.validate import Bbox

class PointWithBboxSchema(PointSchema):
bbox = List(
Float(),
required=False,
allow_none=True,
validate=Bbox()
)

schema = PointWithBboxSchema()

# Valid 2D bbox
data = {
"type": "Point",
"coordinates": [-105.01621, 39.57422],
"bbox": [-180.0, -90.0, 180.0, 90.0]
}
result = schema.load(data)

# Valid 3D bbox
data = {
"type": "Point",
"coordinates": [-105.01621, 39.57422],
"bbox": [-180.0, -90.0, -100.0, 180.0, 90.0, 100.0]
}
result = schema.load(data)
```

## Flask Integration

marshmallow-geojson works seamlessly with Flask for building GeoJSON APIs:

```python
from flask import Flask, request, jsonify
from marshmallow_geojson import GeoJSONSchema
from marshmallow.exceptions import ValidationError

app = Flask(__name__)
schema = GeoJSONSchema()

@app.route('/geojson', methods=['POST'])
def create_geojson():
try:
data = schema.loads(request.data)
# Your business logic here
return jsonify(schema.dump(data)), 201
except ValidationError as e:
return jsonify({'errors': e.messages}), 400

@app.route('/geojson/many', methods=['POST'])
def create_geojson_many():
schema_many = GeoJSONSchema(many=True)
try:
data = schema_many.loads(request.data)
return jsonify(schema_many.dump(data)), 201
except ValidationError as e:
return jsonify({'errors': e.messages}), 400
```

## Compatibility with Other Libraries

marshmallow-geojson is designed to work well with popular Python libraries:

### Marshmallow

Built on Marshmallow, so you get all the benefits:

- Schema-based validation
- JSON serialization/deserialization
- Field filtering and partial loading
- Custom validators
- Integration with Flask, FastAPI, and other frameworks

### GeoPandas

You can convert between marshmallow-geojson and GeoPandas:

```python
import geopandas as gpd
from marshmallow_geojson import GeoJSONSchema

# Convert FeatureCollection to GeoDataFrame
schema = GeoJSONSchema()
feature_collection = schema.load(fc_data)
geojson_dict = schema.dump(feature_collection)
gdf = gpd.GeoDataFrame.from_features(geojson_dict["features"])

# Convert GeoDataFrame to FeatureCollection
features = gdf.to_dict("records")
fc_data = {"type": "FeatureCollection", "features": features}
feature_collection = schema.load(fc_data)
```

### Shapely

Convert to/from Shapely geometries:

```python
from shapely.geometry import Point as ShapelyPoint
from marshmallow_geojson import GeoJSONSchema

schema = GeoJSONSchema()

# Marshmallow GeoJSON to Shapely
point_data = {"type": "Point", "coordinates": [-105.01621, 39.57422]}
point = schema.load(point_data)
shapely_point = ShapelyPoint(point['coordinates'][0], point['coordinates'][1])

# Shapely to Marshmallow GeoJSON
shapely_geom = ShapelyPoint(-105.01621, 39.57422)
point_data = {
"type": "Point",
"coordinates": [shapely_geom.x, shapely_geom.y]
}
point = schema.load(point_data)
```

## Testing

Run the test suite:

```shell
poetry run pytest
```

## Contributing

Contributions are welcome! Please see [CONTRIBUTING.md](https://github.com/folt/marshmallow-geojson/blob/master/CONTRIBUTING.md) for guidelines.

## License

This project is licensed under the MIT License - see the [LICENSE](https://github.com/folt/marshmallow-geojson/blob/master/LICENSE) file for details.

## Links

- [GitHub Repository](https://github.com/folt/marshmallow-geojson)
- [PyPI Package](https://pypi.org/project/marshmallow-geojson/)
- [GeoJSON Specification](http://geojson.org/)
- [RFC 7946](https://tools.ietf.org/html/rfc7946)
- [Marshmallow Documentation](https://marshmallow.readthedocs.io/)