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:

  1. Python 3.9+ with venv or conda environment isolation.
  2. lxml>=4.9.0 — XML construction, namespace management, and XSD validation.
  3. pyproj>=3.4.0 — coordinate reference system (CRS) normalization to modern OGC URIs.
  4. python-dateutil>=2.8.2 — ISO 8601 temporal formatting for dateStamp elements.
  5. ISO 19139 XSD schema files — mirror locally from the OGC schema registry to eliminate network-dependent validation failures in CI environments.
  6. Familiarity with the gmd, gco, gml, and xlink namespace 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/

ISO 19115 Template Generation Pipeline Data-flow diagram showing four stages: template construction producing a pre-validated skeleton, XSD gate, population with dataset attributes, and catalog export. Arrows connect each stage left-to-right with a branch back to construction on validation failure. Template Construction (lxml, namespaces) XSD Valid? fix & retry pass Dataset Population (dynamic attributes) Catalog Export (CSW / file store) Namespace map + mandatory fields raster: spatialRepresentationInfo vector: featureCatalogueCitation ISO 19115 Template Generation Pipeline Structural skeleton validated before dataset-specific values are injected

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 referenceSystemInfo element. Update the CRS URI using epsg_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 featureTypes and attributeDescription blocks to reflect the merged schema.
  • Lineage chain: populate LI_Lineage/statement with a machine-readable processing history string. When implementing FGDC to ISO 19115 conversion pipelines, preserve original FGDC prcstep values by mapping them into ISO 19115 LI_ProcessStep elements 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; a nilReason attribute 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

  1. Never hardcode placeholder values — use environment variables or configuration files for organisationName, electronicMailAddress, and default language. This prevents accidental credential exposure in multi-tenant deployments.
  2. 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.
  3. 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.
  4. Log XSD error paths with line numbers — the etree.DocumentInvalid.error_log object provides precise line/column/message triples. Write these to structured logs; they cut debugging time significantly when downstream catalog systems reject records.
  5. 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.