INSPIRE Directive Schema Compliance: Automated Annex II/III Conformance Jump to heading
Enforcing INSPIRE Directive Schema Compliance in production means treating Annex II/III conformance as a deterministic pipeline stage, not a manual sign-off at the end of a migration. This stage sits inside the Geospatial Schema Architecture & Standards Mapping discipline, where a version-controlled manifest, a CRS synchronization step, and an attribute validation gate replace ad-hoc reprojection scripts and spreadsheet checklists. Government data teams and Python ETL engineers publishing into pan-European discovery and download services need a config-as-code architecture that enforces the INSPIRE application schemas, applies metric tolerance thresholds, routes non-conformant records deterministically, and emits an audit-ready compliance report for every batch.
This page covers the conformance loop for a single national-to-INSPIRE transform — load the Annex manifest, synchronize coordinate reference systems, validate attributes, and route or publish. Cross-standard metadata translation toward CSDGM or ISO 19115 is owned by FGDC Metadata Mapping; local synonym alignment of source field names lives in Local Government Data Dictionaries; and emitting the conformant output into multiple database or catalog targets is the job of Cross-Platform Schema Translation. The deep mechanics of materializing a theme into relational tables are handled in How to map INSPIRE Annex III to local PostgreSQL schemas. This stage stops at producing a validated, reprojected feature and a compliance artifact — downstream publication and tiling are deliberately out of scope here.
Declarative Annex II/III Manifest Jump to heading
Conformance begins with an explicit, version-controlled schema definition. Hardcoded field mappings drift silently and destroy auditability, so the contract for an INSPIRE theme lives in a YAML manifest that separates mandatory from optional attributes, fixes data types and permissible value patterns, pins the target CRS and tolerance, and declares fallback behaviour for every optional field. The validation engine parses this manifest once at pipeline init and refuses to run if any mandatory rule is malformed. The same mandatory/optional convention drives cross-agency alignment documented in Local Government Data Dictionaries.
# inspire_landuse.yaml — config-as-code Annex manifest
# Loaded once at pipeline init; PyYAML >=6.0 safe_load only.
inspire_theme: "Land Use" # Annex III theme identifier
schema_version: "4.0" # INSPIRE technical guideline version
compliance_profile: "annex_iii"
fields:
mandatory:
- name: "localId" # INSPIRE external object identifier
type: "string"
pattern: "^[A-Z]{2}_[0-9]{6,12}$"
nullable: false
- name: "geometry"
type: "geometry"
srid: 3035 # ETRS89/LAEA — pan-European default
tolerance_m: 0.5 # max simplification displacement
topology: "must_be_valid"
optional:
- name: "beginLifespanVersion"
type: "datetime"
nullable: false # required if present, but absence is recoverable
fallback: "current_timestamp"
- name: "endLifespanVersion"
type: "datetime"
nullable: true
fallback: null
validation_rules:
crs_sync: "auto_transform" # reproject silently to srid above
null_handling: "reject" # no implicit nulls for mandatory fields
metadata_sync: true # require ISO 19115/19139 sidecar
Each directive controls one axis of the conformance contract. The table below fixes what every field rule must satisfy:
| Directive | Required | Type | Purpose |
|---|---|---|---|
name |
mandatory | string | INSPIRE attribute name written to the output feature |
type |
mandatory | string | Declared type (string, geometry, datetime) for coercion |
pattern |
optional | string (regex) | Permissible-value check; a mismatch rejects the record |
srid |
geometry only | int | Target EPSG code, normally 3035 (ETRS89/LAEA) |
tolerance_m |
geometry only | float | Maximum metric displacement allowed during simplification |
nullable |
optional (default true) |
bool | If false, an absent value must resolve via fallback or it errors |
fallback |
optional (default null) |
any | Deterministic substitute for an absent optional field |
Splitting the manifest into a mandatory and optional block makes the rejection contract explicit: a missing or malformed mandatory attribute is a hard stop, while optional gaps follow declared fallback routing rather than silent nulling.
Preprocessing Requirements Jump to heading
National source models almost never arrive in INSPIRE shape. Before the manifest’s rules fire, each input must be normalized into a predictable structure, otherwise the CRS step reprojects from a wrong or absent reference and the attribute gate rejects records for the wrong reason. The execution engine expects each feature to satisfy these shape guarantees:
- Source CRS declared explicitly. Reproject only from a known authority code; a feature with no
crsmember is rejected up front rather than assumed to be EPSG:4326. Missing or chained datum shifts are resolved using the Datum Transformation Fallback Chains strategy before this stage runs. - Geometry pre-flattened. Deeply nested or
GeometryCollectioninputs are split into typed parts first; the broader Automated Attribute Transformation & ETL Workflows discipline owns that flattening so the conformance stage only sees clean Polygon/MultiPolygon members. - Attribute names pre-aligned. Map national field names onto INSPIRE attribute names before validation, seeding the alignment from local dictionaries so
parcel_refresolves tolocalIddeterministically rather than failing a pattern check. - Datetime fields ISO-normalized. Coerce national date formats to ISO 8601 before the manifest’s
datetimerules run, sobeginLifespanVersioncomparisons are unambiguous. - Encoding normalized to UTF-8. Legacy national exports are frequently Latin-1; decode and re-encode so identifier patterns match the raw characters they were written for.
With the input normalized, the conformance stage can treat CRS synchronization and attribute validation as two deterministic gates rather than a best-effort cleanup pass.
CRS Synchronization & Precision Guards Jump to heading
INSPIRE mandates ETRS89/LAEA (EPSG:3035) for pan-European area-based analysis, or the ETRS89/UTM zones for national-scale download services. During transformation the engine verifies the source CRS, applies reprojection, repairs topology, and enforces the metric tolerance from the manifest — catching pyproj.exceptions.CRSError and ProjError rather than letting a bad authority code crash the batch. The official INSPIRE Coordinate Reference Systems specification holds the authoritative zone definitions.
# requires: python >=3.10, pyproj >=3.6, shapely >=2.0
import logging
import pyproj
import shapely
from pyproj.exceptions import CRSError, ProjError
from shapely.geometry import shape
from shapely.ops import transform
log = logging.getLogger("inspire.crs")
class GeometryValidationError(Exception):
"""Raised when geometry collapses or cannot be made valid."""
def validate_and_sync_crs(feature: dict, target_srid: int = 3035, tolerance_m: float = 0.5) -> dict:
src_crs_uri = feature.get("crs", {}).get("properties", {}).get("name")
if not src_crs_uri:
# No assumed default: an undeclared CRS is a hard conformance failure.
raise ValueError("Source CRS undefined; INSPIRE conformance requires an explicit spatial reference.")
try:
src_crs = pyproj.CRS.from_string(src_crs_uri)
target_crs = pyproj.CRS.from_epsg(target_srid)
transformer = pyproj.Transformer.from_crs(src_crs, target_crs, always_xy=True)
except (CRSError, ProjError) as exc:
log.warning("CRS resolution failed for %s: %s", src_crs_uri, exc)
raise ValueError(f"unresolvable_crs: {src_crs_uri}") from exc
geom = shape(feature["geometry"])
transformed_geom = transform(transformer.transform, geom)
if not transformed_geom.is_valid:
transformed_geom = shapely.make_valid(transformed_geom) # shapely 2.x repair API
if transformed_geom.is_empty:
raise GeometryValidationError("Geometry collapsed after transformation or repair.")
# Enforce coordinate-precision tolerance; preserve_topology keeps rings closed.
if tolerance_m > 0:
transformed_geom = transformed_geom.simplify(tolerance_m, preserve_topology=True)
feature["geometry"] = transformed_geom.__geo_interface__
feature["crs"] = {"properties": {"name": f"urn:ogc:def:crs:EPSG::{target_srid}"}}
return feature
This function guarantees deterministic CRS alignment, repairs topological defects, and applies the manifest’s metric tolerance before the feature can reach the attribute gate. Because reprojection and repair are idempotent, re-running the stage on already-conformant data produces byte-identical geometry — a precondition for reproducible audits. CRS-level concerns that exceed a single transform — multi-zone harmonization, tolerance policy — belong to CRS Normalization & Sync rather than this stage.
Attribute Validation & Fallback Routing Jump to heading
Attribute conformance requires strict type enforcement, pattern matching, and controlled null handling. The validation hook processes properties against the manifest, applying fallbacks only where explicitly permitted, so there are no silent substitutions. The routing differs sharply between mandatory and optional fields: a missing or malformed mandatory field is a hard stop; an absent optional field is resolved by its declared fallback or — only if non-nullable with no fallback — collected as an error. The decision tree below maps each field class to its deterministic outcome.
# requires: python >=3.10
import re
from datetime import datetime, timezone
from typing import Any
def validate_attributes(feature: dict, schema: dict) -> dict:
errors: list[str] = []
props: dict[str, Any] = feature.get("properties", {})
for field in schema["fields"]["mandatory"]:
val = props.get(field["name"])
if val is None:
errors.append(f"missing_mandatory: {field['name']}")
continue
if field.get("pattern") and not re.match(field["pattern"], str(val)):
errors.append(f"pattern_violation: {field['name']}")
for field in schema["fields"]["optional"]:
val = props.get(field["name"])
if val is None and not field.get("nullable", True):
fallback = field.get("fallback")
if fallback == "current_timestamp":
props[field["name"]] = datetime.now(timezone.utc).isoformat()
else:
errors.append(f"unrecoverable_optional: {field['name']}")
if errors:
# One raise carries every error so the report lists all failures, not just the first.
raise ValueError(f"attribute_validation_failed: {'; '.join(errors)}")
feature["properties"] = props
return feature
Mandatory violations halt execution; optional gaps are resolved deterministically or flagged for manual review. Collecting all errors before raising means the compliance report enumerates every problem in a single pass, so an operator fixes a record once rather than re-running the pipeline per defect.
Failure Modes & Fallback Routing Jump to heading
No record is silently dropped. Every failure type maps to one deterministic recovery action, and every rejection is reproducible from the audit trail using the same reason codes the engine raises.
| Failure type | Likely cause | Deterministic recovery action |
|---|---|---|
crs_undefined |
Source feature carries no crs member |
Reject before reprojection; route to remediation with crs_undefined tag |
unresolvable_crs |
Authority code unknown to PROJ / missing grid | Quarantine record; resolve via datum fallback chain, then re-queue |
geometry_collapsed |
Simplification or make_valid emptied the geometry |
Reject; lower tolerance_m for the theme and reprocess that record |
missing_mandatory |
Annex-required attribute absent from source | Hard stop; route to remediation with the missing name attached |
pattern_violation |
localId or coded value fails its regex |
Reject; surface the offending value in the rejection log, no auto-fix |
unrecoverable_optional |
Non-nullable optional field absent with no fallback | Flag for manual review; do not substitute a value |
Records that fail are handed to a remediation router that quarantines the payload, attaches the diagnostic log, and triggers a review workflow. This matters most when migrating legacy national datasets toward INSPIRE, where historical gaps are common and the Annex schemas impose stricter cardinality than the source model ever required — the same quarantine pattern used by FGDC Metadata Mapping when CSDGM records miss mandatory elements.
Compliance Reporting Output Jump to heading
The stage writes a machine-readable compliance report for every batch so audits never depend on re-running the pipeline. Each accepted and rejected feature contributes a row carrying lineage and decision provenance:
{
"record_id": "landuse/parcel_DE_004821.json",
"status": "rejected",
"reason": "pattern_violation: localId",
"field_status": {
"localId": { "value": "de-4821", "conform": false, "rule": "^[A-Z]{2}_[0-9]{6,12}$" },
"geometry": { "value": "EPSG:3035", "conform": true, "tolerance_m": 0.5 }
},
"lineage": {
"source_standard": "national-landuse-2014",
"target_profile": "INSPIRE-LandUse-4.0/annex_iii",
"schema_manifest": "inspire_landuse.yaml@a1b9f3c",
"engine_version": "inspire-conform 3.1.0",
"processed_at": "2026-06-25T14:02:11Z"
}
}
The lineage block pins the exact manifest commit, target profile, and engine version that produced each decision — which is what satisfies an INSPIRE monitoring and reporting audit: any reviewer can reproduce the rejection from the recorded inputs. The rejection log reuses the same reason codes as the failure-mode table, so a compliance dashboard aggregates failures without parsing free-text messages. INSPIRE also requires ISO 19115/19139 discovery metadata; the report’s target_profile field is the join key that lets the metadata sidecar be validated against the same batch, a bridge that FGDC Metadata Mapping covers in depth for non-European catalogs.
CI Integration Jump to heading
Conformance must be enforced at the commit and merge stages, not discovered at publication time. The following GitHub Actions workflow runs the manifest-driven validation, emits the structured compliance report as an artifact, and blocks non-conformant merges. Validated features are then staged for relational storage following How to map INSPIRE Annex III to local PostgreSQL schemas.
# .github/workflows/inspire-validate.yml
name: INSPIRE Compliance Validation
on:
push:
paths: ['data/landuse/**.json', 'config/inspire_*.yaml']
pull_request:
jobs:
validate:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: '3.10'
- name: Install dependencies
# pyproj+shapely: CRS sync & geometry repair · pydantic: typed output gate
run: pip install "pyproj>=3.6" "shapely>=2.0" "PyYAML>=6.0" "pydantic>=2.6"
- name: Run conformance validation
# Exits non-zero on any mandatory failure, blocking the merge.
run: python -m pipeline.inspire_validate data/landuse/ config/inspire_landuse.yaml
- name: Upload compliance report
if: always() # publish the report even on failure
uses: actions/upload-artifact@v4
with:
name: inspire-compliance-report
path: reports/compliance_*.json
A matching pre-commit hook running the same pipeline.inspire_validate entry point catches CRS gaps and mandatory violations before they ever reach the remote, keeping the audit trail clean. This is the same gate convention reused across FGDC Metadata Mapping and Cross-Platform Schema Translation, so a single CI pattern covers every standard your catalog ingests.
Compliance Notes & Best Practices Jump to heading
- Deterministic validation. Never rely on implicit type coercion. Declare mandatory and optional boundaries in the manifest so the rejection contract is reviewable in version control.
- Idempotent transformations. Reprojection and topology repair must produce identical output across repeated runs. Cache transformer pipelines per source/target pair so re-processing is both fast and byte-stable.
- Metadata alignment. INSPIRE requires ISO 19115/19139 metadata synchronization; embed the metadata validation hook alongside the geometric and attribute checks so a feature and its discovery record are accepted or rejected together.
- Version control. Treat manifest YAML files as versioned artifacts and tag releases to match INSPIRE technical guideline updates, so the
schema_manifestcommit in each report maps to a known specification revision.
Deeper Implementation Guides Jump to heading
- How to map INSPIRE Annex III to local PostgreSQL schemas — flattening GML application schemas into deterministic PostGIS DDL with CRS locking and drift routing.
Related Jump to heading
- Geospatial Schema Architecture & Standards Mapping — the parent discipline this conformance stage belongs to.
- FGDC Metadata Mapping — CSDGM-to-catalog mapping and the shared quarantine and lineage pattern.
- Local Government Data Dictionaries — synonym sources that pre-align national field names to INSPIRE attributes.
- Cross-Platform Schema Translation — emitting the conformant output across database and catalog targets.
- CRS Normalization & Sync — the coordinate-reference discipline behind the reprojection and tolerance guards used here.