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.
Related
- DCAT-AP Spatial Profile Mapping — the parent guide for producing DCAT-AP RDF from spatial sources
- ISO 19115 Metadata Template Generation — building the authoritative ISO records this guide recommends as the master format
- FGDC to ISO 19115 Conversion Pipelines — migrating legacy US federal records into the ISO model
- Automated Metadata Generation & Schema Mapping — the pipeline context spanning both standards