ISO 19115 Metadata Template Generation
Manual metadata authoring introduces structural inconsistencies, delays publication cycles, and creates long-term compliance risk for GIS data managers, open-source maintainers, and government technology teams. This page details a production-ready, code-driven approach to generating structurally valid ISO 19139 XML templates in Python — templates that enforce mandatory field presence, correct namespace declarations, and proper element ordering before any dataset-specific values are injected. This workflow is a core component of the broader Automated Metadata Generation & Schema Mapping discipline.
Prerequisites
Before implementing the template generator, ensure the following are in place:
- Python 3.9+ with
venvorcondaenvironment isolation. lxml>=4.9.0— XML construction, namespace management, and XSD validation.pyproj>=3.4.0— coordinate reference system (CRS) normalization to modern OGC URIs.python-dateutil>=2.8.2— ISO 8601 temporal formatting fordateStampelements.- ISO 19139 XSD schema files — mirror locally from the OGC schema registry to eliminate network-dependent validation failures in CI environments.
- Familiarity with the
gmd,gco,gml, andxlinknamespace prefixes — confusion between these is the single most common source of validation failures.
pip install "lxml>=4.9.0" "pyproj>=3.4.0" "python-dateutil>=2.8.2"
Recommended directory layout for a maintainable metadata engine:
metadata-engine/
├── schemas/
│ └── gmd/ # mirrored ISO 19139 XSD files
├── generators/
│ └── iso19115_template.py
└── output/
Concept & Spec Reference
ISO 19115 defines the abstract information model for describing geographic datasets, series, and services. ISO 19139 translates that model into a concrete XML encoding with strict namespace rules and XSD cardinality constraints. A compliant <gmd:MD_Metadata> record must satisfy more than 1,200 element definitions — making manual authoring both error-prone and impossible to audit reliably.
Template generation differs from full metadata population. A template establishes the structural skeleton, namespace declarations, and mandatory element placeholders in the correct XSD-required sequence. Once validated, the template is programmatically populated with dataset-specific attributes during ingestion or transformation. This decoupled approach reduces validation failures and prevents namespace collisions when downstream catalog systems receive records.
Mandatory Namespace Declarations
| Prefix | URI | Purpose |
|---|---|---|
gmd |
http://www.isotc211.org/2005/gmd |
Core metadata elements |
gco |
http://www.isotc211.org/2005/gco |
Primitive value types (CharacterString, Date, Integer) |
gml |
http://www.opengis.net/gml |
Geometry and temporal types |
xlink |
http://www.w3.org/1999/xlink |
Hyperlink attributes on code list elements |
gmx |
http://www.isotc211.org/2005/gmx |
Extended primitives and multilingual support |
srv |
http://www.isotc211.org/2005/srv |
Service metadata extensions |
Mandatory Root-Level Elements (in XSD sequence order)
| Position | Element | Type | Notes |
|---|---|---|---|
| 1 | fileIdentifier |
gco:CharacterString |
UUID or persistent identifier |
| 2 | language |
gco:CharacterString |
ISO 639-2 code (eng, fra, etc.) |
| 3 | characterSet |
MD_CharacterSetCode |
Code list value: utf8 |
| 4 | hierarchyLevel |
MD_ScopeCode |
dataset, service, series |
| 5 | contact |
CI_ResponsibleParty |
At least organisationName + role |
| 6 | dateStamp |
gco:Date |
ISO 8601 date of last metadata revision |
| 7 | metadataStandardName |
gco:CharacterString |
"ISO 19115-1 Geographic information — Metadata" |
| 8 | metadataStandardVersion |
gco:CharacterString |
"2014" |
Omitting or misordering any of these eight elements causes immediate XSD validation failure. The template generator must enforce this sequence programmatically.
Implementation Walkthrough
Step 1 — Build the namespace map and root element
Using lxml.etree with fully qualified element names prevents the namespace serialization bugs that occur when mixing default and prefixed namespaces. All element construction goes through SubElement — no string concatenation.
import uuid
from datetime import datetime
from lxml import etree
def build_namespace_map() -> dict:
"""Return the canonical ISO 19139 namespace map."""
return {
"gmd": "http://www.isotc211.org/2005/gmd",
"gco": "http://www.isotc211.org/2005/gco",
"gml": "http://www.opengis.net/gml",
"xlink": "http://www.w3.org/1999/xlink",
"gmx": "http://www.isotc211.org/2005/gmx",
"srv": "http://www.isotc211.org/2005/srv",
}
def make_root(nsmap: dict) -> etree._Element:
"""Create the gmd:MD_Metadata root element."""
return etree.Element(
"{http://www.isotc211.org/2005/gmd}MD_Metadata",
nsmap=nsmap,
)
Step 2 — Helper functions for element types
ISO 19139 uses three main primitive patterns: gco:CharacterString, gco:Date, and MD_*Code code list references. Centralizing these as helpers guarantees consistent structure.
GMD = "http://www.isotc211.org/2005/gmd"
GCO = "http://www.isotc211.org/2005/gco"
GMX_CODELIST = "http://www.isotc211.org/2005/resources/Codelist/gmxCodelists.xml"
def add_char_string(parent: etree._Element, tag: str, value: str) -> None:
"""Append a gmd:<tag><gco:CharacterString> element."""
wrapper = etree.SubElement(parent, f"{{{GMD}}}{tag}")
cs = etree.SubElement(wrapper, f"{{{GCO}}}CharacterString")
cs.text = value
def add_date(parent: etree._Element, tag: str, value: str) -> None:
"""Append a gmd:<tag><gco:Date> element (value: YYYY-MM-DD)."""
wrapper = etree.SubElement(parent, f"{{{GMD}}}{tag}")
d = etree.SubElement(wrapper, f"{{{GCO}}}Date")
d.text = value
def add_codelist(
parent: etree._Element,
wrapper_tag: str,
code_tag: str,
codelist_name: str,
codelist_value: str,
) -> None:
"""Append a code list element with codeList and codeListValue attributes."""
wrapper = etree.SubElement(parent, f"{{{GMD}}}{wrapper_tag}")
code = etree.SubElement(wrapper, f"{{{GMD}}}{code_tag}")
code.set("codeList", f"{GMX_CODELIST}#{codelist_name}")
code.set("codeListValue", codelist_value)
Step 3 — Assemble the mandatory baseline template
This function produces the eight-element structural skeleton in the XSD-required order. All placeholder values are safe to overwrite during the population phase.
def build_contact_block(root: etree._Element) -> None:
"""Append a minimal but XSD-valid gmd:contact block."""
contact_elem = etree.SubElement(root, f"{{{GMD}}}contact")
rp = etree.SubElement(contact_elem, f"{{{GMD}}}CI_ResponsibleParty")
add_char_string(rp, "organisationName", "Placeholder Organisation")
add_char_string(rp, "positionName", "GIS Data Manager")
role_wrapper = etree.SubElement(rp, f"{{{GMD}}}role")
role_code = etree.SubElement(role_wrapper, f"{{{GMD}}}CI_RoleCode")
role_code.set("codeList", f"{GMX_CODELIST}#CI_RoleCode")
role_code.set("codeListValue", "pointOfContact")
def generate_iso19115_template(output_path: str = "metadata_template.xml") -> None:
"""
Generate a structurally valid ISO 19139 XML skeleton.
The output passes XSD validation before any dataset-specific values
are injected, guaranteeing that the population stage cannot introduce
element misordering or namespace collisions.
"""
nsmap = build_namespace_map()
root = make_root(nsmap)
# Mandatory elements in XSD sequence order
add_char_string(root, "fileIdentifier", str(uuid.uuid4()))
add_char_string(root, "language", "eng")
add_codelist(root, "characterSet", "MD_CharacterSetCode",
"MD_CharacterSetCode", "utf8")
add_codelist(root, "hierarchyLevel", "MD_ScopeCode",
"MD_ScopeCode", "dataset")
build_contact_block(root)
add_date(root, "dateStamp", datetime.now().strftime("%Y-%m-%d"))
add_char_string(root, "metadataStandardName",
"ISO 19115-1 Geographic information — Metadata")
add_char_string(root, "metadataStandardVersion", "2014")
tree = etree.ElementTree(root)
tree.write(output_path, xml_declaration=True,
encoding="UTF-8", pretty_print=True)
print(f"Template generated: {output_path}")
if __name__ == "__main__":
generate_iso19115_template()
Step 4 — Extend for raster and vector dataset types
The base template covers the eight mandatory root elements. Dataset-specific blocks are appended after the base template passes validation.
For raster datasets, inject spatialRepresentationInfo using grid dimension metadata. Teams implementing generating ISO 19115 metadata from GeoTIFF headers can parse TIFF tags with rasterio and map them directly to ISO 19115 elements.
For vector datasets, append featureCatalogueCitation and geometryObject definitions describing attribute schemas and geometry types.
Always normalize coordinate reference systems with pyproj before inserting them into the template. Converting legacy EPSG integer codes to modern OGC URIs (urn:ogc:def:crs:EPSG::4326) prevents catalog interoperability failures:
from pyproj import CRS
def epsg_to_ogc_uri(epsg_code: int) -> str:
"""Convert an EPSG integer code to a fully qualified OGC CRS URI."""
crs = CRS.from_epsg(epsg_code)
authority, code = crs.to_authority()
return f"urn:ogc:def:crs:{authority}::{code}"
# Example: epsg_to_ogc_uri(4326) → "urn:ogc:def:crs:EPSG::4326"
Validation & CI Integration
Generating a template is only half the workflow. Every output must pass XSD validation before entering production. Integrate validation directly alongside the generator:
def validate_template(xml_path: str, xsd_path: str) -> bool:
"""
Assert that xml_path is valid against the ISO 19139 XSD at xsd_path.
Returns True on success; prints structured error paths on failure.
"""
with open(xsd_path, "rb") as f:
schema = etree.XMLSchema(etree.parse(f))
with open(xml_path, "rb") as f:
doc = etree.parse(f)
try:
schema.assertValid(doc)
return True
except etree.DocumentInvalid as exc:
for err in exc.error_log:
print(f"[INVALID] line {err.line}: {err.message}")
return False
Pair validation with automated linting rules that flag missing spatial extents, empty distribution blocks, or deprecated code list values — consistent with metadata schema validation and linting best practices.
CI/CD Integration
Template generation and validation should run as a deterministic, idempotent step in every pipeline that touches metadata files.
Pre-commit hook — fail the commit on XSD validation failure:
# .pre-commit-config.yaml
repos:
- repo: local
hooks:
- id: iso19115-validate
name: Validate ISO 19115 templates
language: python
entry: python -c "
import sys, glob
from generators.iso19115_template import validate_template
failures = [p for p in glob.glob('output/*.xml')
if not validate_template(p, 'schemas/gmd/gmd.xsd')]
sys.exit(1 if failures else 0)"
pass_filenames: false
GitHub Actions — generate and validate on every push that modifies metadata files:
name: ISO 19115 Metadata Gate
on:
push:
paths:
- "output/**/*.xml"
- "generators/**"
jobs:
validate:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: "3.11"
- run: pip install "lxml>=4.9.0" "pyproj>=3.4.0" "python-dateutil>=2.8.2"
- name: Generate template
run: python generators/iso19115_template.py
- name: Validate template
run: |
python -c "
from generators.iso19115_template import validate_template
import sys
ok = validate_template('output/metadata_template.xml', 'schemas/gmd/gmd.xsd')
sys.exit(0 if ok else 1)"
Containerized execution — package the generator with pinned dependencies and local schema mirrors to eliminate environment drift across staging and production. Reproducible builds require the XSD files to be committed to the repository; never rely on network retrieval in CI.
Derivative & Lineage Management
Template generation interacts with the broader data transformation chain. When source datasets are reprojected, clipped, joined, or rasterized, the resulting derivative’s metadata must accurately reflect those transformations:
- Reprojection changes the
referenceSystemInfoelement. Update the CRS URI usingepsg_to_ogc_uri()immediately after the transformation step, not in a separate post-process. - Clipping modifies the bounding box in
geographicElement/EX_GeographicBoundingBox. Derive new extents programmatically from the clipped geometry rather than inheriting the parent record’s extents. - Joins and dissolves change feature count and attribute schema. Update
featureTypesandattributeDescriptionblocks to reflect the merged schema. - Lineage chain: populate
LI_Lineage/statementwith a machine-readable processing history string. When implementing FGDC to ISO 19115 conversion pipelines, preserve original FGDCprcstepvalues by mapping them into ISO 19115LI_ProcessStepelements rather than discarding them. - Cross-standard publishing: organizations publishing to open data portals typically also need DCAT-AP spatial profile mapping alongside their ISO 19115 records. Generate both formats from the same canonical attribute store to avoid divergence.
- nilReason discipline: when a mandatory element’s value is genuinely unavailable (e.g., acquisition date of a legacy dataset), use
gco:nilReason="unknown"rather than omitting the element. Omission triggers XSD failure; anilReasonattribute is schema-valid and auditable.
Pitfalls & Resolution Table
| Pitfall | Root Cause | Resolution Strategy |
|---|---|---|
gco:CharacterString missing inside gmd:language |
language wraps a gco:CharacterString, not a bare text node |
Always use the add_char_string helper; never set .text directly on the wrapper element |
Namespace prefix mismatch (gmd2, ns0) in serialized output |
lxml auto-generates prefixes when constructing elements without a complete nsmap on the root |
Pass the full nsmap to etree.Element() at root creation; never add new prefixed elements after the root is created |
MD_CharacterSetCode validation failure |
Code list URI uses the wrong fragment anchor (e.g. #MD_Character_SetCode) |
Copy the exact URI from the mirrored XSD codelist file; the fragment is case-sensitive |
| XSD validation passes locally but fails in CI | CI environment fetches the schema from the network; a different schema version or timeout causes drift | Commit the XSD files to the repository under schemas/ and use only local paths in XMLSchema() calls |
dateStamp XSD failure on datetime strings |
ISO 19139 gco:Date expects YYYY-MM-DD; gco:DateTime expects YYYY-MM-DDTHH:MM:SS |
Use datetime.now().strftime("%Y-%m-%d") for gco:Date; use .isoformat() only for gco:DateTime elements |
| Derivative record has incorrect bounding box | Parent record’s bounding box is inherited without recalculation after clip or reproject | Recalculate extents from the output geometry using fiona or geopandas bounds; never copy spatial extent from source to derivative |
CI_ResponsibleParty missing role sub-element |
XSD requires role as the final child of CI_ResponsibleParty; appending it before positionName causes sequence errors |
Always append role last; the XSD sequence enforces individualName?, organisationName?, positionName?, contactInfo*, role |
Production Best Practices
- Never hardcode placeholder values — use environment variables or configuration files for
organisationName,electronicMailAddress, and defaultlanguage. This prevents accidental credential exposure in multi-tenant deployments. - Version-control template generators as infrastructure code — track namespace URI updates, schema version bumps, and code list changes in the same Git history as the pipelines that depend on them.
- Separate generation from population — keep the template generator stateless; use a distinct population layer to inject dataset attributes. This ensures structural compliance is never compromised by malformed input data.
- Log XSD error paths with line numbers — the
etree.DocumentInvalid.error_logobject provides precise line/column/message triples. Write these to structured logs; they cut debugging time significantly when downstream catalog systems reject records. - Test against multiple schema versions — the OGC schema registry hosts both the 2005 ISO 19139 XSD and the newer 2007 corrigendum. Some catalog systems validate against one, some against the other. Use matrix CI jobs to confirm compatibility with both.
Related
- Automated Metadata Generation & Schema Mapping — parent section covering the full metadata pipeline architecture
- Generating ISO 19115 Metadata from GeoTIFF Headers — extend the base template with raster-specific spatialRepresentationInfo populated from
rasterio - FGDC to ISO 19115 Conversion Pipelines — normalize legacy FGDC records before applying ISO 19139 structural rules
- DCAT-AP Spatial Profile Mapping — map the same canonical attribute store to DCAT-AP for cross-catalog publishing
- Metadata Schema Validation and Linting — CI gate patterns that enforce structural and semantic correctness on every commit