DCAT-AP for Spatial Data Portals

The Data Catalog Vocabulary Application Profile (DCAT-AP) has become the de facto standard for machine-readable open data publication across European public sector portals. When applied to geospatial infrastructure, DCAT-AP bridges the gap between traditional GIS metadata models and modern, linked-data web catalogs — enabling cross-jurisdictional discovery, automated harvesting, and semantic search without requiring consumers to parse legacy ISO 19139 XML. This guide provides a production-tested workflow for publishing spatial datasets as DCAT-AP v3, covering the ISO 19115 crosswalk, CRS normalization, RDF graph construction, SHACL validation, and OGC service distribution mapping.

This page is part of the Spatial Metadata & Catalog Integration section, which covers the full lifecycle from raw metadata authoring through validated publication.


DCAT-AP Spatial Publication Pipeline

The diagram below shows how a spatial dataset moves from its native metadata representation to a DCAT-AP JSON-LD endpoint ready for EU portal harvesting.

DCAT-AP Spatial Publication Pipeline A flow diagram with two rows. Top row left to right: ISO 19115 / CSW Source, then arrow labelled "crosswalk", then DCAT Metadata Model (dct:title, dct:publisher, dcat:theme), then arrow labelled "pyproj", then CRS Transform to WGS 84 (dcat:bbox / locn:geometry). Bottom row right to left (continuing the flow): RDF Graph (rdflib), then arrow labelled "pyshacl", then SHACL Validation, then arrow labelled "serialize", then JSON-LD Endpoint (/catalog). A feedback arrow from SHACL Validation back to DCAT Metadata Model is labelled "fix violations". ISO 19115 / CSW Source gmd:MD_Metadata / GeoPackage attrs crosswalk DCAT Metadata Model dct:title · dct:publisher dcat:theme · dct:license pyproj CRS Transform → WGS 84 dcat:bbox locn:geometry (GeoJSON) RDF Graph rdflib · dcat: Dataset + Distribution pyshacl SHACL Validation mandatory props · cardinality datatype · license check fix violations serialise JSON-LD Endpoint /catalog · compact @context data.europa.eu harvest national open data hubs

Prerequisites & Architecture Context

Before implementing DCAT-AP serialisation, your environment and data pipeline must meet these baseline requirements.

Dependencies (install into an isolated virtual environment):

pip install rdflib>=6.3.0 pyshacl>=0.20.0 pyproj>=3.4.0 requests pydantic

Runtime checklist:

  • Python 3.10+ with the packages above
  • Existing metadata source: ISO 19115/19139 records from a CSW endpoint, GeoJSON attribute tables, or a PostGIS schema — see Implementing ISO 19115 Metadata Standards for field extraction patterns
  • Valid OGC service endpoints (WMS, WFS, WCS, or OGC API – Features) that respond to GetCapabilities or OpenAPI spec requests
  • Stable, resolvable base URI for your portal (e.g. https://data.example.org/) — DCAT-AP requires persistent URIs for all dcat:Dataset and dcat:Distribution resources
  • Familiarity with core RDF namespaces: dcat:, dct:, foaf:, vcard:, locn:, gsp: — the namespace binding section below lists their canonical URIs

DCAT-AP sits within the broader Spatial Metadata & Catalog Integration workflow: ISO 19115 records describe datasets at the schema level, DCAT-AP exposes them for machine harvesting, and Automated Metadata Harvesting Workflows keeps federated portals in sync. The SRS and Coordinate Reference System Handling guide covers the CRS transformation patterns this pipeline depends on for bounding box normalisation.


Specification Deep-Dive

DCAT-AP v3 Core Classes

DCAT-AP v3 (the 2023 revision) organises spatial catalog descriptions around five classes:

Six DCAT-AP classes and how many of each A six-row grid of the DCAT-AP classes used by a spatial portal. There is exactly one catalog node; it holds many datasets; each dataset carries many distributions and may reference live data services; and each dataset carries at most one spatial and one temporal coverage node. What it models Class Cardinality The portal dcat:Catalog exactly one A dataset dcat:Dataset many per catalog A file or endpoint dcat:Distribution many per dataset A live service dcat:DataService many, referenced by distributions Coverage in space dct:spatial / dct:Location one per dataset Coverage in time dct:temporal / dct:PeriodOfTime one per dataset
Class Mandatory properties Spatial-specific additions
dcat:Catalog dct:title, dct:description, dct:publisher, dcat:dataset dct:spatial for catalog coverage area
dcat:Dataset dct:title, dct:description, dcat:distribution dcat:bbox, locn:geometry, dct:spatial, dct:temporal
dcat:Distribution dcat:accessURL, dct:license, dct:format dcat:accessService linking to dcat:DataService
dcat:DataService dct:title, dcat:endpointURL, dcat:servesDataset dct:conformsTo for OGC service type URIs
dct:Location locn:geometry or dcat:bbox gsp:Geometry for complex polygon extents

ISO 19115 → DCAT-AP Crosswalk

Accurate field mapping is the most error-prone step. The following table covers the elements most commonly present in ISO 19139 records produced by Implementing ISO 19115 Metadata Standards pipelines:

ISO 19115 element ISO 19139 XPath (abbreviated) DCAT-AP target
identificationInfo/title .../gmd:title/gco:CharacterString dct:title (language-tagged Literal)
identificationInfo/abstract .../gmd:abstract/gco:CharacterString dct:description
contact/organisationName gmd:contact/.../gmd:organisationName dct:publisherfoaf:Organization
descriptiveKeywords .../gmd:keyword/gco:CharacterString dcat:keyword (one per keyword) or dcat:theme (for INSPIRE themes)
extent/geographicBoundingBox .../gmd:EX_GeographicBoundingBox dcat:bbox (WGS 84 string) + locn:geometry
distributionInfo/transferOptions gmd:MD_DigitalTransferOptions/gmd:onLine dcat:Distributiondcat:accessURL
dataQualityInfo/lineage gmd:LI_Lineage/gmd:statement dct:provenance
fileIdentifier gmd:fileIdentifier/gco:CharacterString used to mint the dcat:Dataset URI
dateStamp gmd:dateStamp/gco:DateTime dct:modified

Version Divergence: DCAT-AP v2 vs v3

Agencies targeting both national portals (some still on v2) and EU portals (v3) need to handle these breaking changes:

Concern DCAT-AP v2 DCAT-AP v3
Spatial coverage property dct:Location only Both dct:Location and dcat:bbox / locn:geometry on Dataset directly
dcat:DataService Optional, rarely validated Mandatory for service-type distributions; dcat:endpointURL replaces bare accessURL
License granularity dct:license at Dataset level acceptable Must appear at Distribution level; Dataset-level license treated as default only
dcat:theme vocabulary No controlled vocabulary enforced Must reference a URI from an established vocabulary (EU MDR, INSPIRE, or similar)
Temporal resolution dct:temporal plain Literal dct:PeriodOfTime with dcat:startDate / dcat:endDate typed as xsd:date

Python Implementation

Step 1: Spatial Extent Transformation

DCAT-AP requires all bounding box coordinates in WGS 84. Native datasets often arrive in local CRS — ETRS89/LAEA (EPSG:3035) for pan-European data, British National Grid (EPSG:27700) for UK data, or Web Mercator (EPSG:3857) from tile caches. Use pyproj for reliable reprojection; manual matrix arithmetic introduces precision drift.

from pyproj import Transformer

def normalize_bbox(
    min_x: float, min_y: float,
    max_x: float, max_y: float,
    src_crs: str = "EPSG:3035",
) -> str:
    """
    Reproject a bounding box from src_crs to WGS 84 (EPSG:4326) and
    return a dcat:bbox-compatible string: 'minLon,minLat,maxLon,maxLat'.

    always_xy=True ensures pyproj honours (longitude, latitude) order
    regardless of whether the CRS definition lists axes as (lat, lon).
    """
    transformer = Transformer.from_crs(src_crs, "EPSG:4326", always_xy=True)
    min_lon, min_lat = transformer.transform(min_x, min_y)
    max_lon, max_lat = transformer.transform(max_x, max_y)

    # Clamp to valid geographic bounds — necessary for polar projections
    # where reprojection can produce values slightly outside ±180/±90
    min_lon = max(-180.0, min(180.0, min_lon))
    max_lon = max(-180.0, min(180.0, max_lon))
    min_lat = max(-90.0, min(90.0, min_lat))
    max_lat = max(-90.0, min(90.0, max_lat))

    return f"{min_lon:.6f},{min_lat:.6f},{max_lon:.6f},{max_lat:.6f}"

def bbox_to_geojson_literal(bbox_str: str) -> str:
    """
    Convert a 'minLon,minLat,maxLon,maxLat' string to a GeoJSON Polygon
    string suitable for locn:geometry with datatype gsp:geoJSONLiteral.
    """
    min_lon, min_lat, max_lon, max_lat = (float(v) for v in bbox_str.split(","))
    return (
        f'{{"type":"Polygon","coordinates":[['
        f'[{min_lon},{min_lat}],[{max_lon},{min_lat}],'
        f'[{max_lon},{max_lat}],[{min_lon},{max_lat}],'
        f'[{min_lon},{min_lat}]]]}}'
    )

The always_xy=True flag is critical: without it, pyproj follows the CRS-defined axis order, which for EPSG:4326 is latitude-first — producing a silently transposed bounding box that passes most syntax checks but fails spatial queries.

Step 2: RDF Graph Construction

from rdflib import Graph, Namespace, Literal, URIRef
from rdflib.namespace import DCAT, DCTERMS, FOAF, RDF, XSD

# Namespaces beyond rdflib's built-ins
LOCN = Namespace("http://www.w3.org/ns/locn#")
GSP  = Namespace("http://www.opengis.net/ont/geosparql#")
VCARD = Namespace("http://www.w3.org/2006/vcard/ns#")

def build_dataset_graph(dataset_id: str, metadata: dict) -> Graph:
    """
    Construct a DCAT-AP v3-compliant RDF graph for one spatial dataset.

    metadata dict keys (all str unless noted):
        title, abstract, publisher_name, publisher_url,
        bbox (WGS 84 bbox string from normalize_bbox),
        geojson_extent (optional GeoJSON Literal string),
        license_url, theme_uri, modified (ISO 8601 date str),
        keywords (list[str])
    """
    g = Graph()
    g.bind("dcat",  DCAT)
    g.bind("dct",   DCTERMS)
    g.bind("foaf",  FOAF)
    g.bind("locn",  LOCN)
    g.bind("gsp",   GSP)
    g.bind("vcard", VCARD)

    ds_uri = URIRef(f"https://data.example.org/dataset/{dataset_id}")

    # — Dataset core —
    g.add((ds_uri, RDF.type,             DCAT.Dataset))
    g.add((ds_uri, DCTERMS.title,        Literal(metadata["title"], lang="en")))
    g.add((ds_uri, DCTERMS.description,  Literal(metadata["abstract"], lang="en")))
    g.add((ds_uri, DCTERMS.modified,     Literal(metadata["modified"], datatype=XSD.date)))
    g.add((ds_uri, DCTERMS.license,      URIRef(metadata["license_url"])))
    g.add((ds_uri, DCAT.theme,           URIRef(metadata["theme_uri"])))

    for kw in metadata.get("keywords", []):
        g.add((ds_uri, DCAT.keyword, Literal(kw, lang="en")))

    # — Publisher as foaf:Organization —
    pub_uri = URIRef(metadata["publisher_url"])
    g.add((pub_uri, RDF.type,   FOAF.Organization))
    g.add((pub_uri, FOAF.name,  Literal(metadata["publisher_name"])))
    g.add((ds_uri,  DCTERMS.publisher, pub_uri))

    # — Spatial coverage —
    loc_uri = URIRef(f"{ds_uri}/location")
    g.add((loc_uri, RDF.type,    DCTERMS.Location))
    g.add((loc_uri, DCAT.bbox,   Literal(metadata["bbox"])))
    if geojson := metadata.get("geojson_extent"):
        g.add((loc_uri, LOCN.geometry,
               Literal(geojson, datatype=GSP.geoJSONLiteral)))
    g.add((ds_uri, DCTERMS.spatial, loc_uri))

    return g

Namespace hygiene matters. Mixing unprefixed URIs or relying on rdflib’s auto-prefix generation can produce inconsistent serialisations (e.g. ns0:Dataset) that EU harvester validators reject. Always bind every prefix explicitly before adding triples.

Step 3: Distribution Mapping for OGC Service Endpoints

Each discoverable OGC endpoint becomes a dcat:Distribution node. The dcat:accessService link to a dcat:DataService is mandatory in DCAT-AP v3 for service-type distributions — omitting it causes SHACL validation failures on portals that enforce v3 shapes.

import hashlib

# IANA media types for common OGC services
OGC_MEDIA_TYPES: dict[str, str] = {
    "WMS":              "application/vnd.ogc.wms_xml",
    "WFS":              "application/vnd.ogc.wfs_xml",
    "WCS":              "application/vnd.ogc.wcs_xml",
    "OGC_API_FEATURES": "application/geo+json",
    "OGC_API_TILES":    "application/vnd.mapbox-vector-tile",
}

# OGC conformance class URIs for dct:conformsTo
OGC_CONFORMANCE: dict[str, str] = {
    "WMS": "http://www.opengis.net/spec/wms/1.3",
    "WFS": "http://www.opengis.net/spec/wfs/2.0",
    "OGC_API_FEATURES": "http://www.opengis.net/spec/ogcapi-features-1/1.0",
}

def add_ogc_distribution(
    graph: Graph,
    dataset_uri: URIRef,
    service_url: str,
    service_type: str,    # key from OGC_MEDIA_TYPES
    license_url: str,
) -> URIRef:
    """
    Attach a dcat:Distribution + dcat:DataService pair for one OGC endpoint.
    Returns the Distribution URI.
    """
    url_hash = hashlib.sha256(service_url.encode()).hexdigest()[:8]
    dist_uri    = URIRef(f"{dataset_uri}/distribution/{url_hash}")
    service_uri = URIRef(f"{dataset_uri}/service/{url_hash}")

    # Distribution
    graph.add((dataset_uri, DCAT.distribution,  dist_uri))
    graph.add((dist_uri,    RDF.type,            DCAT.Distribution))
    graph.add((dist_uri,    DCAT.accessURL,      URIRef(service_url)))
    graph.add((dist_uri,    DCTERMS.format,
               Literal(OGC_MEDIA_TYPES.get(service_type, "application/octet-stream"))))
    graph.add((dist_uri,    DCTERMS.license,     URIRef(license_url)))
    graph.add((dist_uri,    DCAT.accessService,  service_uri))

    # DataService (mandatory in DCAT-AP v3 for service distributions)
    graph.add((service_uri, RDF.type,               DCAT.DataService))
    graph.add((service_uri, DCTERMS.title,
               Literal(f"{service_type} endpoint", lang="en")))
    graph.add((service_uri, DCAT.endpointURL,       URIRef(service_url)))
    graph.add((service_uri, DCAT.servesDataset,     dataset_uri))
    if conformance := OGC_CONFORMANCE.get(service_type):
        graph.add((service_uri, DCTERMS.conformsTo, URIRef(conformance)))

    return dist_uri

Validate that each OGC endpoint responds to GetCapabilities or the OpenAPI /conformance endpoint before attaching it to the graph. Broken distributions are flagged by automated harvester health checks on data.europa.eu and trigger de-listing after repeated failures.

Step 4: SHACL Validation

from pyshacl import validate as shacl_validate
from rdflib import Graph

DCAT_AP_SHACL_URL = (
    "https://raw.githubusercontent.com/SEMICeu/DCAT-AP/"
    "master/releases/3.0.0/dcat-ap_3.0.0_shacl_shapes.ttl"
)

def validate_dcat_ap(graph: Graph, shacl_url: str = DCAT_AP_SHACL_URL) -> tuple[bool, str]:
    """
    Validate an rdflib Graph against DCAT-AP SHACL shapes.
    Returns (conforms: bool, report_text: str).

    Cache the SHACL shapes file locally in production — fetching it on every
    validation call adds latency and creates an external dependency in your
    publish pipeline. Store it as a file and load with Graph().parse(path).
    """
    shacl_graph = Graph().parse(shacl_url, format="turtle")
    conforms, _, results_text = shacl_validate(
        graph,
        shacl_graph=shacl_graph,
        inference="rdfs",
        debug=False,
    )
    return conforms, results_text

def serialize_json_ld(graph: Graph) -> str:
    """
    Serialise the validated graph to compact JSON-LD.
    rdflib's json-ld serialiser emits @graph-wrapped output by default.
    """
    return graph.serialize(format="json-ld", indent=2)

The most common SHACL violations in spatial DCAT-AP publications are: missing dct:license on dcat:Distribution (not just on the parent Dataset); missing dcat:endpointURL on dcat:DataService; and dcat:bbox literals that include whitespace or omit the comma separator. Run SHACL validation in your CI pipeline — not just on first publication — because downstream schema changes (endpoint URL updates, license migrations) can introduce regressions.


Error Handling & Edge Cases

Invalid bounding boxes. Swapped latitude/longitude, unclamped polar values, or anti-meridian-crossing extents are the most common causes of spatial harvester rejections. Always call normalize_bbox before constructing the graph, and log the original vs. normalised values so operators can trace coordinate drift in source datasets.

URI instability. Changing a dcat:Dataset URI after publication breaks all external references and harvest history. If you must change URIs (e.g. after a platform migration), implement HTTP 301 permanent redirects from the old URI to the new one, and add dct:identifier with the original URI to the new record so harvesters can correlate records across the change.

Empty dcat:Distribution sets. A dcat:Dataset with no distributions passes SHACL minimum shapes but is rejected by most portal harvesters in practice — they require at least one accessible distribution before indexing. Guard against this by raising a validation error in your pipeline if graph.triples((ds_uri, DCAT.distribution, None)) returns no results.

Namespace prefix conflicts in rdflib serialisation. If you load an external ontology into the graph (e.g. parsing a remote SHACL file into the same graph before serialising), rdflib may inherit its prefix bindings and emit unexpected prefixes in JSON-LD output. Keep validation graphs separate from publication graphs.

OGC service type detection. When extracting distributions from legacy ISO 19139 gmd:MD_DigitalTransferOptions, the gmd:protocol element value may be free text (OGC:WMS, OGC:WFS, ogc:wms, or undeclared). Normalise to upper-case keys before looking up OGC_MEDIA_TYPES to avoid missing a distribution due to case mismatch.


SHACL failures cluster into three familiar shapes A decision diamond over SHACL validation failures on a DCAT-AP graph. A dataset without a title violates a mandatory property shape; a theme expressed as a plain literal instead of a concept IRI violates a node kind constraint; a date written as free text instead of a typed xsd:date violates a datatype constraint; and once every shape is satisfied the graph is serialised as JSON-LD and published. SHACL validation failed on the generated graph. Which shape was violated? Add a title mandatory on every dataset Mint an IRI dcat:theme must be a concept Use xsd:date not a free-text string Publish serialise as JSON-LD no dct:title literal, not IRI bad date form shapes all satisfied

Testing & Compliance Verification

import unittest
from rdflib import Graph
from rdflib.namespace import DCAT, DCTERMS, RDF

class TestDCATAPGraph(unittest.TestCase):

    def _minimal_metadata(self) -> dict:
        return {
            "title": "Flood Risk Zones 2024",
            "abstract": "Delineated flood risk polygons for the Rhine basin.",
            "publisher_name": "Federal Environment Agency",
            "publisher_url": "https://data.example.org/publisher/umweltbundesamt",
            "bbox": "5.866251,47.270111,15.041751,55.058001",
            "license_url": "https://creativecommons.org/licenses/by/4.0/",
            "theme_uri": "http://inspire.ec.europa.eu/theme/nz",
            "modified": "2024-03-01",
            "keywords": ["flood risk", "Rhine basin"],
        }

    def test_mandatory_properties_present(self):
        meta = self._minimal_metadata()
        g = build_dataset_graph("flood-risk-rhine-2024", meta)
        ds_uri = URIRef("https://data.example.org/dataset/flood-risk-rhine-2024")

        self.assertTrue((ds_uri, RDF.type, DCAT.Dataset) in g)
        self.assertTrue((ds_uri, DCTERMS.title, None) in g)
        self.assertTrue((ds_uri, DCTERMS.description, None) in g)
        self.assertTrue((ds_uri, DCTERMS.publisher, None) in g)

    def test_bbox_normalisation(self):
        # EPSG:3035 corner point for Germany
        bbox_str = normalize_bbox(4200000, 2700000, 4700000, 3400000, "EPSG:3035")
        parts = [float(v) for v in bbox_str.split(",")]
        self.assertEqual(len(parts), 4)
        # All must be within valid WGS 84 ranges
        self.assertGreaterEqual(parts[0], -180.0)
        self.assertLessEqual(parts[2], 180.0)
        self.assertGreaterEqual(parts[1], -90.0)
        self.assertLessEqual(parts[3], 90.0)

    def test_distribution_attaches_data_service(self):
        meta = self._minimal_metadata()
        g = build_dataset_graph("flood-risk-rhine-2024", meta)
        ds_uri = URIRef("https://data.example.org/dataset/flood-risk-rhine-2024")
        add_ogc_distribution(
            g, ds_uri,
            "https://geoserver.example.org/wms",
            "WMS",
            meta["license_url"],
        )
        # Every distribution must link to a DataService
        distributions = list(g.objects(ds_uri, DCAT.distribution))
        self.assertEqual(len(distributions), 1)
        services = list(g.objects(distributions[0], DCAT.accessService))
        self.assertEqual(len(services), 1)

For compliance verification against EU portal requirements, submit your catalog endpoint to the DCAT-AP Validator maintained by the EU ISA² programme. The validator runs v2 and v3 SHACL shapes and produces a structured report that maps each violation to the property, severity, and the offending triple.

For production SHACL validation, cache the shapes file locally as dcat-ap-shapes.ttl and load it with Graph().parse("dcat-ap-shapes.ttl") rather than fetching it over HTTPS on every run. The remote URL above is version-pinned in the code but the upstream location may change with new releases.


Performance & Scaling Notes

Incremental publication. Re-generating the full RDF graph for a large catalog on every run wastes CPU and triggers unnecessary harvester re-indexing. Track dct:modified timestamps per dataset; only rebuild and republish graphs whose source metadata has changed since the last run. Store the last-published timestamp alongside the dataset URI in your pipeline state table.

Graph memory management. An rdflib.Graph for a catalog with 10,000 datasets can consume several GB of RAM if all graphs are merged into a single object. Process datasets in batches of 500–1000, serialise each batch to JSON-LD files on disk, and concatenate via @graph merging at publication time rather than holding everything in memory.

Connection pooling for endpoint health checks. When verifying OGC service endpoints before attaching them as distributions, reuse a single requests.Session with a configured HTTPAdapter across all checks. Spawning one session per endpoint check introduces TCP handshake overhead that dominates latency at scale.

Chunked Turtle serialisation. If you’re writing Turtle to disk for large catalog dumps, use graph.serialize(destination=file_path, format="turtle") which streams directly to disk rather than building the entire serialised string in memory first.

Synchronisation with aggregators. Use dct:modified timestamps and ETag headers when pushing records to regional aggregators like data.europa.eu or national open data hubs. Sending unchanged records on every harvest cycle degrades your portal’s trust score and may trigger rate limiting. The Automated Metadata Harvesting Workflows guide covers incremental synchronisation patterns using checkpoint timestamps and change-detection fingerprints.


Gotchas / Frequently Asked Questions

Do I need to transform CRS to WGS 84 before publishing DCAT-AP?

Yes. The dcat:bbox property and locn:geometry expect coordinates in WGS 84 (EPSG:4326). Any native CRS — ETRS89/LAEA (EPSG:3035), British National Grid (EPSG:27700), or similar — must be reprojected before serialisation. Use pyproj’s Transformer.from_crs with always_xy=True to avoid axis-order ambiguity. The SRS and Coordinate Reference System Handling guide explains why always_xy matters for CRS definitions that list latitude before longitude.

What DCAT-AP properties are mandatory for inclusion in data.europa.eu?

At minimum: dct:title, dct:description, dct:publisher, dcat:distribution on the Dataset; and dcat:accessURL, dct:license, dct:format on each Distribution. Missing dct:license at the Distribution level is the most common reason for rejection by EU portal harvesters — even if a license is declared at the Dataset level, DCAT-AP v3 requires it to be repeated on each Distribution node.

Can I mix DCAT-AP with ISO 19115 metadata in the same catalog?

Yes — and for INSPIRE compliance you typically must. Expose ISO 19139 XML records via a CSW endpoint for INSPIRE discovery, and publish DCAT-AP JSON-LD via a separate /catalog path or a conformant OGC API – Records endpoint with f=jsonld. Map between the two schemas using a documented crosswalk table; never generate one from the other on the fly without validation. See the Schema Validation for Spatial Records page for cross-schema validation strategies.

How do I attach a WMS or WFS service as a DCAT-AP distribution?

Create a dcat:Distribution node linked from the parent dcat:Dataset via dcat:distribution. Set dcat:accessURL to the service base URL, dct:format to the IANA media type (application/vnd.ogc.wms_xml for WMS, application/json for OGC API Features), and dcat:accessService pointing to a dcat:DataService node that carries dcat:endpointURL and dct:conformsTo. For WMS-specific layer-level JSON-LD, see Generating DCAT-AP Compliant JSON-LD for WMS Layers.

How do I handle datasets covering multiple non-contiguous geometries?

Use locn:geometry with a GeoJSON MultiPolygon encoded as a Literal with datatype gsp:geoJSONLiteral, rather than dcat:bbox. Keep dcat:bbox as the minimum bounding rectangle for harvesters that do not parse locn:geometry. Attach both properties simultaneously on the dct:Location node — DCAT-AP v3 permits multiple geometry representations on the same resource and recommends providing both for maximum compatibility.


Back to Spatial Metadata & Catalog Integration