ISO 19115 vs DCAT-AP: When to Use Each

Use ISO 19115 when your records must feed an OGC CSW geospatial catalog or satisfy formal INSPIRE and federal dataset-metadata mandates, and use DCAT-AP when the destination is an open data portal whose harvesters consume RDF — and when you must serve both audiences, keep ISO 19115 as the authoritative source and generate DCAT-AP from it.

The two standards are frequently framed as competitors, but they were designed for different consumers and describe metadata at different depths. This comparison sits within DCAT-AP Spatial Profile Mapping and the broader Automated Metadata Generation & Schema Mapping pipeline family, because in practice the decision is rarely “either/or” — it is “which is authoritative, and which is derived.” ISO 19115 is a rich content model for geographic information, expressed as XML and harvested over CSW; DCAT-AP is a lean RDF vocabulary tuned for cross-domain open data discovery. Choosing correctly means matching the standard to the harvester, the regulatory obligation, and the level of geometry detail the dataset’s compliance profile demands.

Comparison table

Dimension ISO 19115 DCAT-AP
Primary consumer OGC CSW geospatial catalogs, spatial data infrastructures Open data portals (data.europa.eu, national CKAN/DKAN)
Encoding ISO 19139 / 19115-3 XML RDF (JSON-LD, Turtle, RDF/XML)
Data model depth Deep: resolution, reference systems, feature catalogues, lineage Lean: discovery-oriented dataset + distribution + bbox
Geometry representation EX_GeographicBoundingBox, EX_BoundingPolygon, CRS blocks dcat:bbox / dcat:centroid as WKT or GeoJSON literals
Regulatory anchor INSPIRE dataset metadata, US FGDC/GeoPlatform lineage EU Open Data Directive, cross-domain portal harvesting
Lineage / provenance First-class (LI_Lineage, process steps) Via dct:provenance / PROV-O links, less structured
Multilingual support PT_FreeText / LocalisedCharacterString rdf:langString per-property language tags
Tooling pycsw, GeoNetwork, OWSLib, lxml rdflib, pyshacl, CKAN dcat plugins
Best fit Authoritative geospatial catalog of record Wide discovery across a general-purpose data portal
Validation XSD + Schematron SHACL shapes

The single most useful way to read this table: ISO 19115 optimizes for fidelity — it can describe the coordinate reference system, spatial resolution, and processing lineage that a geospatial specialist needs. DCAT-AP optimizes for reach — it makes a spatial dataset findable alongside budgets, statistics, and registers in a general-purpose portal. When both matter, ISO is the master record and DCAT-AP the projection of it.

Automated Python Implementation

The script below encodes the decision as a rules dictionary. Given a use-case descriptor — target audience, whether an OGC catalog is involved, geometry-precision needs, and regulatory regime — it returns a recommendation and can emit a minimal record in the chosen encoding(s). It uses only the standard library plus lxml and rdflib, so it runs without a database or network.

#!/usr/bin/env python3
"""
standard_selector.py — Recommend ISO 19115, DCAT-AP, or both for a use case,
and emit a minimal record in the chosen encoding(s).
"""
from dataclasses import dataclass, field
from lxml import etree
from rdflib import Graph, Namespace, URIRef, Literal
from rdflib.namespace import RDF, DCTERMS

DCAT = Namespace("http://www.w3.org/ns/dcat#")
GMD = "http://www.isotc211.org/2005/gmd"
GCO = "http://www.isotc211.org/2005/gco"


@dataclass
class UseCase:
    """Descriptor of a publication target."""
    audience: str            # "open_data_portal" | "geospatial_catalog" | "both"
    uses_csw: bool = False   # target harvests over OGC CSW
    needs_geometry_precision: bool = False  # resolution/CRS/lineage are critical
    regime: str = "none"     # "inspire" | "eu_open_data" | "us_federal" | "none"
    metadata: dict = field(default_factory=dict)


# Ordered rules: the first matching rule decides. Later rules are fallbacks.
SELECTION_RULES = [
    (lambda u: u.audience == "both", "both"),
    (lambda u: u.uses_csw, "iso19115"),
    (lambda u: u.regime in ("inspire", "us_federal"), "iso19115"),
    (lambda u: u.needs_geometry_precision, "iso19115"),
    (lambda u: u.audience == "open_data_portal", "dcatap"),
    (lambda u: u.regime == "eu_open_data", "dcatap"),
]


def recommend(use_case: UseCase) -> str:
    """Resolve the recommended standard: 'iso19115', 'dcatap', or 'both'."""
    for predicate, choice in SELECTION_RULES:
        if predicate(use_case):
            return choice
    return "dcatap"  # discovery-first default


def emit_iso19115(meta: dict) -> bytes:
    """Emit a minimal ISO 19139 record (identification block only)."""
    nsmap = {"gmd": GMD, "gco": GCO}
    root = etree.Element(f"{{{GMD}}}MD_Metadata", nsmap=nsmap)
    ident = etree.SubElement(root, f"{{{GMD}}}identificationInfo")
    data_ident = etree.SubElement(ident, f"{{{GMD}}}MD_DataIdentification")
    citation = etree.SubElement(data_ident, f"{{{GMD}}}citation")
    ci = etree.SubElement(citation, f"{{{GMD}}}CI_Citation")
    title = etree.SubElement(ci, f"{{{GMD}}}title")
    cs = etree.SubElement(title, f"{{{GCO}}}CharacterString")
    cs.text = meta.get("title", "Untitled dataset")
    abstract = etree.SubElement(data_ident, f"{{{GMD}}}abstract")
    acs = etree.SubElement(abstract, f"{{{GCO}}}CharacterString")
    acs.text = meta.get("description", "")
    return etree.tostring(root, pretty_print=True, xml_declaration=True,
                          encoding="UTF-8")


def emit_dcatap(meta: dict) -> str:
    """Emit a minimal DCAT-AP dataset as JSON-LD."""
    g = Graph()
    g.bind("dcat", DCAT)
    g.bind("dct", DCTERMS)
    ds = URIRef(meta.get("uri", "https://data.example.gov/datasets/x"))
    g.add((ds, RDF.type, DCAT.Dataset))
    g.add((ds, DCTERMS.title, Literal(meta.get("title", "Untitled dataset"), lang="en")))
    g.add((ds, DCTERMS.description, Literal(meta.get("description", ""), lang="en")))
    if "bbox_wkt" in meta:
        gsp = Namespace("http://www.opengis.net/ont/geosparql#")
        g.add((ds, DCAT.bbox, Literal(meta["bbox_wkt"], datatype=gsp.wktLiteral)))
    return g.serialize(format="json-ld", indent=2)


def emit(use_case: UseCase) -> dict:
    """Produce whichever encodings the recommendation calls for."""
    choice = recommend(use_case)
    out: dict = {"recommendation": choice}
    if choice in ("iso19115", "both"):
        out["iso19115_xml"] = emit_iso19115(use_case.metadata).decode("utf-8")
    if choice in ("dcatap", "both"):
        out["dcatap_jsonld"] = emit_dcatap(use_case.metadata)
    return out


if __name__ == "__main__":
    sample = UseCase(
        audience="both",
        uses_csw=True,
        regime="inspire",
        metadata={
            "title": "Municipal Zoning 2025",
            "description": "Zoning polygons for the metropolitan area.",
            "uri": "https://data.example.gov/datasets/zoning-2025",
            "bbox_wkt": "POLYGON((-1 51, 0 51, 0 52, -1 52, -1 51))",
        },
    )
    result = emit(sample)
    print("Recommendation:", result["recommendation"])
    for key in ("iso19115_xml", "dcatap_jsonld"):
        if key in result:
            print(f"\n--- {key} ---")
            print(result[key])

The rules are ordered deliberately: both short-circuits first because a dual-audience mandate overrides every narrower signal, then CSW and regulatory anchors force ISO, then geometry-precision needs, and only after all fidelity signals are exhausted does the discovery-first DCAT-AP default apply. Encoding the policy as data rather than nested if statements keeps it auditable — a reviewer can read the compliance logic without reading control flow.

Validation and pipeline integration

Validate each emitted encoding with the matching tool, and keep a regression test that pins the recommendation for known use cases:

pip install "lxml>=4.9" "rdflib>=6.3"

# ISO side: structural validation against the cached 19139 XSD
xmllint --noout --schema schemas/gmd/metadataEntity.xsd out_iso.xml

# DCAT-AP side: SHACL conformance against the portal's shapes
python -c "from pyshacl import validate; from rdflib import Graph; \
  d=Graph().parse('out.jsonld', format='json-ld'); \
  s=Graph().parse('shapes/dcat-ap.ttl', format='turtle'); \
  ok,_,rpt=validate(d, shacl_graph=s); print('conforms:', ok)"
# test_standard_selector.py
from standard_selector import UseCase, recommend


def test_csw_forces_iso():
    assert recommend(UseCase(audience="open_data_portal", uses_csw=True)) == "iso19115"


def test_portal_only_is_dcat():
    assert recommend(UseCase(audience="open_data_portal")) == "dcatap"


def test_both_audiences_returns_both():
    assert recommend(UseCase(audience="both", uses_csw=True)) == "both"


def test_inspire_regime_forces_iso():
    assert recommend(UseCase(audience="open_data_portal", regime="inspire")) == "iso19115"

Long-term compliance best practices

  • Designate one authoritative record. When you publish both encodings, make ISO 19115 the master and generate DCAT-AP from it, so a single edit cannot leave the two catalogs describing the dataset differently.
  • Map, do not duplicate. Maintain the crosswalk from ISO elements to DCAT-AP properties in version control; regenerate the derived encoding rather than hand-editing it, which is how divergence creeps in.
  • Match validation to encoding. Gate ISO output with XSD and Schematron and DCAT-AP output with the target portal’s SHACL shapes — passing one does not imply the other conforms.
  • Track the regulatory driver in metadata. Record which regime (INSPIRE, EU Open Data, US federal) mandated each record, so an auditor can trace why a given standard was chosen.
  • Re-evaluate on audience change. A dataset promoted from an internal catalog to a public portal changes its target consumer; re-run the selector rather than assuming the original choice still holds.
  • Preserve geometry fidelity at the source. Even when DCAT-AP is the published face, keep the full CRS and resolution in the ISO record so precision is never lost to the leaner profile.