Examples¶
Here we provide a few examples using structtype to accomplish various tasks.
GeoJSON¶
GeoJSON is a popular format for encoding geographic
data. Its specification describes nine different types a message may take
(seven “geometry” types, plus two “feature” types). Here we provide one way of
implementing that specification using structtype to handle the parsing and
validation.
The loads and dumps methods defined below work similar to the
standard library’s json.loads / json.dumps, but:
Will result in high-level
structtype.Structobjects representing GeoJSON typesWill error nicely if a field is missing or the wrong type
Will fill in default values for optional fields
Decodes and encodes significantly faster than the
jsonmodule (as well as most otherjsonimplementations in Python).
This example makes use of structtype.Struct types to define the different GeoJSON
types, and Tagged Unions to differentiate between them. See the
relevant docs for more information.
The full example source can be found here.
from __future__ import annotations
import structtype
from structtype import StructConfig
Position = tuple[float, float]
# All 9 GeoJSON types share the same configuration: they make use of a `type`
# field to disambiguate between types when decoding. Define it once on a base
# class — subclasses inherit the configuration automatically.
class TaggedBase(structtype.Struct):
struct_config = StructConfig(tag=True)
# Define the 7 standard Geometry types.
class Point(TaggedBase):
coordinates: Position
class MultiPoint(TaggedBase):
coordinates: list[Position]
class LineString(TaggedBase):
coordinates: list[Position]
class MultiLineString(TaggedBase):
coordinates: list[list[Position]]
class Polygon(TaggedBase):
coordinates: list[list[Position]]
class MultiPolygon(TaggedBase):
coordinates: list[list[list[Position]]]
class GeometryCollection(TaggedBase):
geometries: list[Geometry]
Geometry = (
Point
| MultiPoint
| LineString
| MultiLineString
| Polygon
| MultiPolygon
| GeometryCollection
)
# Define the two Feature types
class Feature(TaggedBase):
geometry: Geometry | None = None
properties: dict | None = None
id: str | int | None = None
class FeatureCollection(TaggedBase):
features: list[Feature]
# A union of all 9 GeoJSON types
GeoJSON = Geometry | Feature | FeatureCollection
# Create a decoder and an encoder to use for decoding & encoding GeoJSON types
loads = structtype.StructAdapter(GeoJSON).struct_validate_json
dumps = structtype.StructAdapter(GeoJSON).struct_dump_json
Here we use the loads method defined above to read some example GeoJSON.
In [1]: import structtype_geojson
In [2]: with open("canada.json", "rb") as f:
...: data = f.read()
In [3]: canada = structtype_geojson.loads(data)
In [4]: type(canada) # loaded as high-level, validated object
Out[4]: structtype_geojson.FeatureCollection
In [5]: canada.features[0].properties
Out[5]: {'name': 'Canada'}
DynamoDB¶
Amazon DynamoDB has no float type:
all numbers must be decimal.Decimal values. When storing structs with boto3,
declare numeric fields as decimal.Decimal and pass
builtin_types=[decimal.Decimal] to struct_dump — the values are kept
as decimal.Decimal and the result can be used directly as the Item:
>>> import decimal
>>> import boto3
>>> from structtype import Struct
>>> class Product(Struct):
... name: str
... price: decimal.Decimal
... rating: decimal.Decimal
>>> product = Product("widget", decimal.Decimal("9.99"), decimal.Decimal("4.5"))
>>> table = boto3.resource("dynamodb").Table("products")
>>> table.put_item(Item=product.struct_dump(builtin_types=[decimal.Decimal]))
Reading items back works the same way: DynamoDB returns numbers as
decimal.Decimal, which struct_validate accepts unchanged:
>>> item = table.get_item(Key={"name": "widget"})["Item"]
>>> Product.struct_validate(item)
Product(name='widget', price=Decimal('9.99'), rating=Decimal('4.5'))
Other types that struct_dump converts by default can be passed through the
same way — for example bytes fields (DynamoDB supports binary values
natively) with builtin_types=[decimal.Decimal, bytes].