Data Contracts for Spatial Products
A data contract for a spatial product is the machine-checkable agreement that pins down five things at once — schema, coordinate reference system, geometry precision, service-level guarantees, and semantics — and enforces them at the exact boundary where a producing domain hands data to a consuming one. This reference sits inside Geospatial Data Mesh Fundamentals and treats the contract as executable infrastructure, not a wiki page: if a producer changes its CRS from EPSG:4326 to EPSG:3857, drops a required attribute, or coarsens geometry precision below the promised tolerance, the boundary rejects the payload before any consumer resolves it. Platform engineers and GIS data stewards should read this alongside Schema Contracts for Vector Tile Data, which applies the same discipline to the tile-serving layer, and Product Thinking for GIS Datasets, which supplies the ownership and SLA model that a contract encodes. Where the two diverge, the version stamp v1.2.0-crs:EPSG:4326-res:10m is the arbiter — it is the immutable identity a consumer pins to.
Figure — A spatial data contract mediates the producer/consumer boundary, checking schema, CRS, geometry precision, SLA, and semantics before stamping an immutable version on the payload.
Architectural Boundaries & Design Rationale
The contract exists because a federated mesh removes the single geodatabase that once forced every producer and consumer to share one implicit schema. Once each domain owns and publishes independently, “the data changed and nothing told me” becomes the dominant failure mode. A data contract restores the missing signal by making the boundary itself refuse any product that no longer matches what consumers agreed to depend on. It is enforced at ingress — the same zero-trust posture the catalog applies in Metadata Cataloging for Raster/Vector — so a non-conforming payload never reaches storage, discovery, or a downstream join.
Five surfaces make up the contract, and each maps to a distinct class of silent breakage:
- Schema — attribute names, types, nullability, and cardinality. Removing a required field or narrowing a type breaks every consumer that projected it.
- CRS — the declared
crs_epsgand axis order. A producer that flips fromEPSG:4326toEPSG:32633, or serves lat/lon where consumers expect lon/lat, corrupts every spatial predicate downstream. The governance model for this surface lives in CRS Governance and Reprojection Standards. - Geometry precision — coordinate resolution and the topology rule set. A product promising sub-metre planar topology that ships decimated geometry fails validation rather than quietly degrading a routing graph.
- SLA — freshness, availability, and latency targets. These are the guarantees a consumer plans capacity against, drawn from the same model as Product Thinking for GIS Datasets.
- Semantics — units, enumerations, null conventions, and the meaning of each field. A
populationcolumn that switches from count to density is schema-valid and semantically catastrophic.
The rationale for enforcing all five at one gate rather than scattering checks across pipelines is idempotency and single-point accountability: a payload validated once against a pinned contract version behaves identically on every retry, and a breach has exactly one place to page. Contract evolution is governed by Versioning Spatial Data Contracts with SemVer, which decides whether a change is a compatible MINOR bump or a breaking MAJOR that forces a parallel version.
Specification & Contract Reference
A spatial data contract is a versioned document, stored beside the product and referenced by its stamp. The mandatory fields below are the minimal surface every producer must publish and every consumer may rely on.
| Field | Type | Required | Constraint / Convention |
|---|---|---|---|
contract_version |
string | yes | v1.2.0-crs:EPSG:4326-res:10m site convention; immutable once published |
product_id |
UUID | yes | Stable identity across contract versions |
crs_epsg |
integer | yes | EPSG code; change is always a MAJOR bump |
axis_order |
enum | yes | lon_lat | lat_lon; explicit to avoid silent inversion |
geometry_type |
enum | yes | point | linestring | polygon | multipolygon | raster |
coordinate_precision |
integer | yes | Decimal places retained; drives precision validation |
topology_rule_set |
enum | vector only | none | planar | network | 3d_solid |
schema_fields |
array | yes | Ordered {name, type, nullable, unit} records |
freshness_sla_minutes |
integer | yes | 0 means real-time; drives drift alerts |
availability_target |
number | yes | Fractional, e.g. 0.999; consumer capacity input |
semantics |
object | yes | Per-field units, enums, and null meaning |
product_owner_id |
yes | Accountable steward for breaches and incidents |
The enforcement surface — where the contract is evaluated — is carried in transport and policy inputs rather than the payload body, so the same contract can gate a REST ingress, a stream consumer, or a CI check.
| Surface | Key | Purpose |
|---|---|---|
| HTTP header | X-Contract-Version |
Pins the exact contract the payload claims to satisfy |
| Policy input | input.contract.crs_epsg |
Validated against the CRS allowlist and the pinned version |
| Policy input | input.payload.schema_hash |
Detects drift between declared and actual schema |
| Idempotency key | SHA-256(product_id ‖ contract_version ‖ payload_hash) |
Makes re-validation of the same payload side-effect free |
| Registry entry | contract://<product_id>/<contract_version> |
Canonical, immutable storage path for the contract document |
Consumers subscribe to a specific contract_version and are notified only when a compatible MINOR/PATCH supersedes it; a MAJOR requires an explicit, tested migration rather than an automatic upgrade.
Production Implementation
The contract is authored as a machine-readable document and enforced by a policy engine at the boundary. Below is a data-contract schema in YAML (the human-authored source) and its JSON Schema projection used for structural validation, followed by an opa eval gate that checks the cross-cutting rules JSON Schema cannot express — CRS allowlisting, version pinning, and ownership. The gate is idempotent and zero-trust by default: identity is verified upstream, default allow = false denies anything not explicitly permitted, and re-evaluating the same payload against the same contract version always yields the same decision.
# contract://parcels_cadastre/v1.2.0 — authored source of truth
contract_version: "v1.2.0-crs:EPSG:4326-res:10m"
product_id: "7c9e6a1b-2f34-4d8a-9b11-0a5c2e7f4d21"
crs_epsg: 4326
axis_order: lon_lat
geometry_type: multipolygon
coordinate_precision: 6 # ~0.11 m at the equator
topology_rule_set: planar
schema_fields:
- { name: parcel_id, type: string, nullable: false, unit: none }
- { name: area_m2, type: number, nullable: false, unit: square_metre }
- { name: land_use, type: string, nullable: false, unit: enum }
freshness_sla_minutes: 1440
availability_target: 0.999
semantics:
land_use: { enum: [residential, commercial, agricultural, industrial] }
area_m2: { null_means: not_applicable }
product_owner_id: cadastre-steward@example.org
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"title": "SpatialDataContract",
"type": "object",
"required": ["contract_version", "product_id", "crs_epsg", "axis_order",
"geometry_type", "coordinate_precision", "schema_fields",
"freshness_sla_minutes", "availability_target", "product_owner_id"],
"properties": {
"contract_version": { "type": "string", "pattern": "^v\\d+\\.\\d+\\.\\d+-crs:EPSG:\\d{4,5}-res:.+$" },
"product_id": { "type": "string", "format": "uuid" },
"crs_epsg": { "type": "integer", "minimum": 1000, "maximum": 99999 },
"axis_order": { "type": "string", "enum": ["lon_lat", "lat_lon"] },
"geometry_type": { "type": "string",
"enum": ["point", "linestring", "polygon", "multipolygon", "raster"] },
"coordinate_precision": { "type": "integer", "minimum": 0, "maximum": 15 },
"topology_rule_set": { "type": "string", "enum": ["none", "planar", "network", "3d_solid"] },
"schema_fields": { "type": "array", "minItems": 1 },
"freshness_sla_minutes": { "type": "integer", "minimum": 0 },
"availability_target": { "type": "number", "exclusiveMinimum": 0, "maximum": 1 },
"product_owner_id": { "type": "string", "format": "email" }
},
"additionalProperties": false
}
package contract.boundary
import rego.v1
default allow = false
# Zero-trust: the payload is admitted only if it pins the published contract
# version, declares an allowlisted CRS, and its schema hash matches the contract.
allow if {
input.header["x-contract-version"] == input.contract.contract_version
input.contract.crs_epsg in [4326, 3857, 32610, 32611, 32612, 32633]
input.payload.schema_hash == input.contract.schema_hash
input.payload.coordinate_precision >= input.contract.coordinate_precision
}
Run the JSON Schema check and the policy together at ingress. In CI, opa eval -i request.json -d boundary.rego "data.contract.boundary.allow" fails the producer’s merge before a breaking payload is ever published; at runtime the same bundle runs as an admission sidecar so the boundary, the registry, and the consumer all enforce the identical contract independently. A false decision returns 409 Conflict with the offending field, and the producer must either fix the payload or, if the change is intentional, cut a new contract version.
Diagnostic Runbook
When a consumer reports broken or empty results downstream, isolate the failing contract surface in order from transport to semantics.
- Version pin mismatch (HTTP 409). Compare the
X-Contract-Versionheader against the registry entry the consumer subscribed to. A mismatch means a producer published a new version without a compatible migration. Remediation: re-tag the payload with the pinned version, or coordinate aMAJORmigration per Versioning Spatial Data Contracts with SemVer. - Schema-hash drift. If
input.payload.schema_hashdiffers frominput.contract.schema_hash, the producer changed columns without bumping the contract. Remediation: diff the field list, classify the change, and republish under the correct version bump. - CRS or axis-order rejection. An
allow = falsewith a matching version usually meanscrs_epsgfell outside the allowlist oraxis_orderwas inverted. Remediation: reproject at the boundary per CRS Governance and Reprojection Standards; never widen the allowlist to force a pass. - Precision violation.
coordinate_precisionbelow the contract minimum fails the policy. Remediation: stop decimating geometry upstream, or bump the contract if the coarser precision is a deliberate, communicated change. - Semantic breakage that passes schema. Values are schema-valid but wrong (units flipped, enum extended). Remediation: add the changed enum/unit to the contract
semanticsblock and treat an incompatible change asMAJOR. - Idempotency collision. The same
product_idandcontract_versionproduced two divergent payload hashes. Remediation: return the existing record for identical payloads; reject divergent ones with409and require a version bump. - SLA breach without a schema fault. Data is valid but stale or unavailable. Remediation: page the
product_owner_id, trigger a producer re-run, and check the freshness monitor againstfreshness_sla_minutes.
SLA Targets & Performance Baselines
Contract enforcement is itself a product surface with guarantees. These baselines apply per boundary and feed the alerting that governs remediation.
| Metric | Target | Alert Threshold | Remediation Action |
|---|---|---|---|
| Contract validation latency (p95) | < 30 ms | > 90 ms for 10 min | Cache compiled policy bundle; co-locate the gate sidecar |
| Breaking-change escape rate | 0 | any breach reaching a consumer | Add the missing rule; block on CI, not runtime |
| Schema-drift detection lag | < 1 publish cycle | drift found post-publish | Move the hash check into the producer’s merge gate |
| Contract-version pin coverage | 100% of consumers | any unpinned consumer | Require X-Contract-Version; deny unpinned reads |
Freshness vs freshness_sla_minutes |
within SLA | 2× SLA exceeded | Re-run producer pipeline; page owner |
| Idempotent re-validation | 100% identical decisions | any nondeterministic result | Pin policy bundle version; remove clock/random inputs |
Governance & Compliance Notes
Every contract version is immutable and append-only. Publishing v1.3.0 never mutates v1.2.0; the older document stays queryable for a defined retention window (typically 90 days) so audit and rollback can reconstruct exactly which contract a historical payload satisfied. Each boundary decision emits an audit event capturing the actor, the product_id, the resolved contract_version, the policy result, and the offending field on denial — the same evidentiary discipline the catalog applies at registration. The CRS allowlist in the Rego gate is the single enforcement point for approved projections, so an unapproved crs_epsg cannot enter the mesh regardless of which producer publishes it. Ownership is non-negotiable: a contract without an accountable product_owner_id is itself invalid, because a breach with no steward has no path to resolution. Managed this way, the contract becomes the durable interface of a spatial product — consumers plan against it, producers evolve behind it, and the version stamp guarantees the two never drift apart silently.
Related
- Geospatial Data Mesh Fundamentals — parent reference and architectural overview
- Versioning Spatial Data Contracts with SemVer — how contract versions evolve without breaking consumers
- Schema Contracts for Vector Tile Data — the same discipline applied at the tile-serving layer
- Product Thinking for GIS Datasets — the ownership and SLA model a contract encodes
- CRS Governance and Reprojection Standards — how the CRS surface of the contract is governed across domains