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.

INSPIRE Directive Schema Compliance Pipeline Four-stage pipeline. A declarative YAML schema (Step 1) configures the mapping and CRS sync stage (Step 2), where source data is cast to the INSPIRE Annex application schema and reprojected to ETRS89/LAEA EPSG:3035. A conformance gate (Step 3) checks Annex II/III rules: conforming features are published to the feature store (Step 4) while violations route to a machine-readable remediation and audit report. Step 1 · schema.yaml mandatory / optional rules configures Source Data national model Step 2 · Map + Sync CRS Annex schema cast, reproject → EPSG:3035 Step 3 Conform? Annex II/III Step 4 · Publish feature store Remediation + audit report pass fail

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.

yaml
# 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 crs member 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 GeometryCollection inputs 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_ref resolves to localId deterministically rather than failing a pattern check.
  • Datetime fields ISO-normalized. Coerce national date formats to ISO 8601 before the manifest’s datetime rules run, so beginLifespanVersion comparisons 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.

python
# 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.

Mandatory vs Optional Attribute Validation Routing For each feature property the validator branches on field class. Mandatory fields: a missing value or a pattern mismatch raises an error that halts the pipeline; a present, well-formed value passes. Optional fields: a present value passes; an absent value is resolved by a declared fallback such as current_timestamp, set to null when the field is nullable, or recorded as an error when the field is non-nullable with no fallback. Field from schema read props[name] Mandatory present & pattern OK? no yes HALT pipeline raise ValueError Pass Optional value present? absent present Resolve fallback fallback? nullable? Pass Apply fallback e.g. current_timestamp Set null nullable: true Flag error non-nullable, no fallback
python
# 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:

json
{
  "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.

yaml
# .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_manifest commit in each report maps to a known specification revision.

Deeper Implementation Guides Jump to heading