Exporting ISO 19111 Metadata for Cadastral Deliverables
A cadastral deliverable that names its coordinate reference systems and the operation between them in a standards-conformant form is what lets a receiving authority reconstruct the transformation without guesswork, which is the traceability role inside compliance report generation for agency submission, part of Batch Transformation & Automation for Cadastral Coordinate Pipelines. This page serialises the CRS and coordinate-operation metadata that ISO 19111 requires — source and target CRS identifiers, the operation method and its parameters, the operation accuracy, and the area of use — into a structured metadata file built from pyproj CRS and CoordinateOperation objects.
Figure — the CRS pair and their operation serialise into a document carrying identifiers, method, accuracy, and extent.
What ISO 19111 Asks the Metadata to Carry
ISO 19111 models spatial referencing as three separable things: a source coordinate reference system, a target CRS, and a coordinate operation that maps one to the other. A conformant deliverable therefore does not collapse the transformation into a single string; it records each object with its authority identifier — the EPSG code — intact, so a reviewer can resolve it against the registry. The operation itself has attributes that must survive serialisation: the method (for example NTv2 or a seven-parameter Helmert), its parameters (the grid file, or the translations, rotations, and scale), the accuracy the operation is documented to achieve, and the extent or area of use over which it is valid.
Two of these are routinely lost. The operation accuracy is the figure a reviewer weighs against the batch RMSE, yet it is easy to export a bare CRS and forget the operation carries it. And the authority code — EPSG:6318 rather than just “NAD83(2011)” — is what makes an identifier resolvable rather than a human label; dropping it turns a traceable reference into an ambiguous name. pyproj exposes all of this: a CRS yields to_json_dict() and to_wkt(), and a CoordinateOperation (or a Transformer’s underlying operation) exposes its method, parameters, and accuracy.
Complete Runnable Implementation
The function below takes the source and target CRS and the operation, extracts the ISO 19111 fields, and writes a structured metadata document as UTF-8 JSON with the WKT2 string of each CRS embedded for reviewers that consume WKT.
from __future__ import annotations
import json
from dataclasses import dataclass, asdict
from pathlib import Path
from pyproj import CRS
from pyproj.transformer import TransformerGroup
@dataclass(frozen=True)
class Iso19111Metadata:
"""The ISO 19111 fields a cadastral deliverable must carry."""
source_crs_id: str # authority:code, e.g. 'EPSG:4269'
target_crs_id: str # authority:code, e.g. 'EPSG:6318'
source_crs_wkt: str # WKT2 of the source CRS
target_crs_wkt: str # WKT2 of the target CRS
operation_name: str # human name of the coordinate operation
operation_method: str # method, e.g. 'NTv2' or 'Helmert'
accuracy_m: float | None # documented operation accuracy (m)
area_of_use: str | None # extent / area of use description
def _crs_identifier(crs: CRS) -> str:
"""Return 'AUTHORITY:CODE' or raise if the CRS is unregistered.
ISO 19111 traceability requires a resolvable identifier, so a CRS with
no authority code is rejected rather than exported as a bare name.
"""
auth = crs.to_authority(min_confidence=100)
if auth is None:
raise ValueError(f"CRS {crs.name!r} has no authority code; not traceable")
return f"{auth[0]}:{auth[1]}"
def export_iso19111_metadata(
source: CRS,
target: CRS,
out_path: str | Path,
) -> Iso19111Metadata:
"""Serialise CRS + operation metadata to a UTF-8 JSON deliverable.
The coordinate operation is resolved between the CRS pair; its method and
documented accuracy are captured so the reviewer can weigh the accuracy
against the batch RMSE. The bare CRS is never exported without the
operation that connects the pair.
"""
group = TransformerGroup(source, target)
if not group.transformers:
raise ValueError("no coordinate operation available for this CRS pair")
op = group.transformers[0] # best-ranked coordinate operation
op_json = op.to_json_dict() # PROJJSON: method, parameters, accuracy
accuracy = op.accuracy
meta = Iso19111Metadata(
source_crs_id=_crs_identifier(source),
target_crs_id=_crs_identifier(target),
source_crs_wkt=source.to_wkt(),
target_crs_wkt=target.to_wkt(),
operation_name=op.name,
operation_method=op_json.get("method", {}).get("name", "unknown"),
accuracy_m=float(accuracy) if accuracy is not None and accuracy >= 0 else None,
area_of_use=op.area_of_use.name if op.area_of_use else None,
)
payload = {
"iso19111": asdict(meta),
"operation_projjson": op_json, # method + parameters + accuracy
"source_crs_json": source.to_json_dict(), # PROJJSON per the CRS schema
"target_crs_json": target.to_json_dict(),
}
# ensure_ascii=False keeps accented place names intact; encode UTF-8 so the
# WKT2 and area-of-use text are not mangled to escapes or a legacy codepage.
text = json.dumps(payload, indent=2, sort_keys=True, ensure_ascii=False)
Path(out_path).write_text(text, encoding="utf-8")
return meta
Parameter Reference
| Name | Type | Units | Valid range | Notes |
|---|---|---|---|---|
source |
CRS |
— | registered CRS | must carry an authority code to be traceable |
target |
CRS |
— | registered CRS | the deliverable’s target realization |
out_path |
str | Path |
— | writable path | metadata file, written UTF-8 |
accuracy_m |
float | None |
metres | ≥ 0 or None |
documented operation accuracy; None only if truly undocumented |
operation_method |
str |
— | method name | e.g. NTv2, Helmert — the algorithm class the reviewer expects |
area_of_use |
str | None |
— | extent name | the region the operation is valid over |
| return | Iso19111Metadata |
— | — | the captured fields; also written to out_path |
Worked Example
Export the metadata for a NAD83 (EPSG:4269) to NAD83(2011) (EPSG:6318) operation. The resolved operation carries a documented accuracy that lands in the metadata beside the resolvable CRS identifiers.
src = CRS.from_epsg(4269) # NAD83
dst = CRS.from_epsg(6318) # NAD83(2011)
meta = export_iso19111_metadata(src, dst, "deliverable_crs.json")
print(meta.source_crs_id, "->", meta.target_crs_id)
print(meta.operation_method, meta.accuracy_m)
# EPSG:4269 -> EPSG:6318
# <method name> <documented accuracy in metres or None>
The written deliverable_crs.json holds the Iso19111Metadata block, the WKT2 of each CRS, and the PROJJSON form of both CRS from to_json_dict(), all in UTF-8.
Validation Check
Assert the two fields most often lost — the operation accuracy and the authority codes — are present, and that the WKT survives a UTF-8 round-trip unaltered.
assert meta.source_crs_id.startswith("EPSG:"), "source lost its authority code"
assert meta.target_crs_id.startswith("EPSG:"), "target lost its authority code"
assert meta.accuracy_m is None or meta.accuracy_m >= 0.0, "invalid accuracy"
reloaded = json.loads(Path("deliverable_crs.json").read_text(encoding="utf-8"))
assert reloaded["iso19111"]["source_crs_wkt"] == meta.source_crs_wkt, "WKT drifted"
Common Mistakes
Dropping the operation accuracy
op.accuracy from the resolved coordinate operation and record it in metres; if it is genuinely undocumented, store an explicit None rather than silently leaving the field out.Exporting a bare CRS without the operation
CoordinateOperation (here via TransformerGroup) and serialise its name, method, and parameters alongside the two CRS.Losing the EPSG authority code
EPSG:6318 identifier makes the reference unresolvable against the registry. Call to_authority() and reject a CRS with no code rather than shipping an ambiguous label that a reviewer cannot look up.Writing non-UTF-8 WKT
ensure_ascii escaping mangles them and can break a downstream WKT parser. Serialise with ensure_ascii=False and write with encoding="utf-8" so the text is preserved exactly.Related
- Compliance report generation for agency submission — the parent report this metadata document is attached to.
- Generating audit hashes for transformation batches — the operation parameters here are what that digest canonicalises.
- RMSE-to-agency-submission workflow in Python — the batch RMSE a reviewer weighs against this operation accuracy.
- Handling CRS mismatches in cadastral datasets — resolving the CRS pair before its metadata is exported.
- Batch Transformation & Automation for Cadastral Coordinate Pipelines — the parent reference on automating and certifying batch transformations.