Mapping ISO 19115 to Dublin Core for CSW

A crosswalk and transform for exposing rich ISO 19115/19139 source records as the flat Dublin Core csw:Record that a CSW GetRecords response must return.

This page is the hands-on companion to CSW Catalog Schema Mapping & Validation and sits within the broader Metadata Catalog Automation & Ingestion Workflows practice; read the parent guide first for the pipeline-level view of how transformation and validation are decoupled stages. The problem addressed here is concrete: a catalog stores detailed ISO 19115 metadata, but the CSW csw:GetRecords operation in its default output schema hands clients a lossy, flat Dublin Core view. Without a deliberate, tested crosswalk, titles arrive empty, bounding boxes vanish, and multi-valued keywords collapse into one string, breaking every downstream client that queries the catalog by the OGC contract.

Prerequisites

Confirm the following before wiring a crosswalk into the catalog. Each corresponds to a common cause of an empty or malformed csw:Record.

  • A running pycsw 2.6+ catalog (or another CSW 2.0.2 / 3.0 server) whose repository holds ISO 19139-encoded source records.
  • Access to edit the catalog’s profile/output mapping and, if using XSLT, a processor such as xsltproc or saxon for standalone testing.
  • The ISO 19139 namespaces (gmd, gco, gml, srv) and the Dublin Core / CSW namespaces (dc, dct, ows, csw) to hand — namespace prefix errors are the most frequent cause of a silent empty element.
  • Source records that already cleared the ingestion gate in Validating ISO 19115 Metadata Before Ingestion, so the ISO input you transform is structurally sound.
  • Awareness of the OAI-PMH harvest path in Automated Metadata Ingestion via OAI-PMH, since harvested oai_dc records and CSW Dublin Core share the same element set and often the same mapping bugs.

CSW defines Dublin Core as its mandatory common return schema: any conformant client can request outputSchema=http://www.opengis.net/cat/csw/2.0.2 and expects a csw:Record of Dublin Core elements. ISO 19139 remains the richer output schema for capable clients, but the Dublin Core view is the interoperability floor, and a lossy crosswalk breaks the floor for everyone.

Step-by-step implementation

1. Understand why GetRecords returns Dublin Core

A CSW server advertises one or more outputSchema values. Simple and federated clients request the Dublin Core schema because it is the only one every CSW is required to support, so the server must project each stored ISO 19139 record down to a flat csw:Record. That projection is lossy by design: ISO’s nested MD_DataIdentification, EX_GeographicBoundingBox, and repeated MD_Keywords must be flattened into a handful of unqualified Dublin Core elements plus an ows:BoundingBox. Getting the projection right — not the storage — is what makes the catalog interoperable.

ISO 19115 to Dublin Core crosswalk A two-column mapping diagram. On the left are nested ISO 19139 elements: citation title, abstract, file identifier, keyword, geographic bounding box, and citation date. Arrows cross to the right column of Dublin Core and OWS targets: dc:title, dct:abstract, dc:identifier, dc:subject, ows:BoundingBox, and dc:date, all collected into a flat csw:Record. The keyword and bounding box arrows are highlighted to show many-to-one flattening. ISO 19115 / 19139 Dublin Core · csw:Record gmd:citation//gmd:title gco:CharacterString gmd:abstract gmd:fileIdentifier gmd:keyword (repeated) many values EX_GeographicBoundingBox 4 corner ordinates gmd:citation//gmd:date dc:title dct:abstract dc:identifier dc:subject (repeated) one per keyword ows:BoundingBox dc:date

2. Build the element crosswalk

Fix the mapping before writing any transform. The table below is the contract every implementation must honor; the multiplicity column is where lossy shortcuts creep in. Note that a bounding box is not a single value but four ordinates projected into one ows:BoundingBox with ows:LowerCorner and ows:UpperCorner, and that ISO’s date-with-role must collapse to a single dc:date.

ISO 19115 / 19139 source Dublin Core / OWS target Multiplicity Notes
gmd:identificationInfo//gmd:citation//gmd:title/gco:CharacterString dc:title 1 First title only if repeated
gmd:identificationInfo//gmd:abstract/gco:CharacterString dct:abstract 1 Also duplicate to dc:description for older clients
gmd:fileIdentifier/gco:CharacterString dc:identifier 1 Must be the queryable record id
gmd:identificationInfo//gmd:descriptiveKeywords//gmd:keyword dc:subject many One dc:subject per keyword — never join
gmd:EX_GeographicBoundingBox (4 ordinates) ows:BoundingBox 1 Lower = west/south, Upper = east/north; CRS urn:ogc:def:crs:EPSG::4326
gmd:citation//gmd:date/gmd:CI_Date//gmd:date dc:date 1 Prefer publication, else revision, else creation
gmd:hierarchyLevel/gmd:MD_ScopeCode/@codeListValue dc:type 1 e.g. dataset, service
gmd:distributionInfo//gmd:MD_Format/gmd:name dc:format many Distribution format labels
gmd:identificationInfo//gmd:resourceConstraints//gmd:useLimitation dct:rights many License / use constraints

3. Implement the transform in XSLT

An XSLT stylesheet is the portable way to express the crosswalk and the one most catalogs (pycsw included) accept as a pluggable output mapping. Declare both namespace families, emit each element per the table, and iterate keywords with xsl:for-each so multiplicity is preserved.

<?xml version="1.0" encoding="UTF-8"?>
<xsl:stylesheet version="1.0"
  xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
  xmlns:gmd="http://www.isotc211.org/2005/gmd"
  xmlns:gco="http://www.isotc211.org/2005/gco"
  xmlns:csw="http://www.opengis.net/cat/csw/2.0.2"
  xmlns:dc="http://purl.org/dc/elements/1.1/"
  xmlns:dct="http://purl.org/dc/terms/"
  xmlns:ows="http://www.opengis.net/ows">

  <xsl:template match="gmd:MD_Metadata">
    <csw:Record>
      <dc:identifier>
        <xsl:value-of select="gmd:fileIdentifier/gco:CharacterString"/>
      </dc:identifier>
      <dc:title>
        <xsl:value-of select="(.//gmd:citation//gmd:title/gco:CharacterString)[1]"/>
      </dc:title>
      <dc:type>
        <xsl:value-of select="gmd:hierarchyLevel/gmd:MD_ScopeCode/@codeListValue"/>
      </dc:type>
      <!-- one dc:subject per keyword: multiplicity preserved -->
      <xsl:for-each select=".//gmd:descriptiveKeywords//gmd:keyword/gco:CharacterString">
        <dc:subject><xsl:value-of select="."/></dc:subject>
      </xsl:for-each>
      <dct:abstract>
        <xsl:value-of select=".//gmd:abstract/gco:CharacterString"/>
      </dct:abstract>
      <!-- publication date preferred, else first available date -->
      <dc:date>
        <xsl:value-of select="(.//gmd:CI_Date[.//gmd:CI_DateTypeCode/@codeListValue='publication']//gmd:date/gco:Date
                              | .//gmd:CI_Date//gmd:date/gco:Date)[1]"/>
      </dc:date>
      <!-- bounding box: four ordinates into LowerCorner / UpperCorner -->
      <xsl:for-each select="(.//gmd:EX_GeographicBoundingBox)[1]">
        <ows:BoundingBox crs="urn:ogc:def:crs:EPSG::4326">
          <ows:LowerCorner>
            <xsl:value-of select="concat(gmd:westBoundLongitude/gco:Decimal,' ',gmd:southBoundLatitude/gco:Decimal)"/>
          </ows:LowerCorner>
          <ows:UpperCorner>
            <xsl:value-of select="concat(gmd:eastBoundLongitude/gco:Decimal,' ',gmd:northBoundLatitude/gco:Decimal)"/>
          </ows:UpperCorner>
        </ows:BoundingBox>
      </xsl:for-each>
    </csw:Record>
  </xsl:template>
</xsl:stylesheet>

4. Wire the mapping into the catalog

In pycsw the record-to-Dublin-Core projection is driven by the repository mapping and the queryables config, not usually by a raw stylesheet, so express the same crosswalk as configuration. Point each core queryable at the database column that holds the extracted ISO value; pycsw populates those columns from the ISO source at transaction time. Keep the config in version control alongside the stylesheet used for validation.

# pycsw.yml — repository mapping expressing the same crosswalk (valid YAML)
server:
  home: /var/lib/pycsw
  url: https://catalog.example.org/csw
  encoding: UTF-8

repository:
  source: postgresql://pycsw@db:5432/catalog
  table: records
  mappings:
    pycsw:Identifier: identifier      # -> dc:identifier
    pycsw:Title: title                # -> dc:title
    pycsw:Abstract: abstract          # -> dct:abstract
    pycsw:Keywords: keywords          # -> dc:subject (comma-split at output)
    pycsw:Type: type                  # -> dc:type
    pycsw:Modified: date_modified     # -> dc:date
    pycsw:BoundingBox: wkt_geometry   # -> ows:BoundingBox (EPSG:4326)
    pycsw:Format: format              # -> dc:format
    pycsw:AccessConstraints: accessconstraints  # -> dct:rights

5. Handle multiplicity and missing elements

Two failure modes dominate: repeated ISO elements silently collapsed to one, and required Dublin Core elements emitted empty when the ISO source omits them. Keep dc:subject, dc:format, and dct:rights as genuine repeats; guard scalar targets so a missing source produces no element rather than an empty one; and pick a deterministic date when several CI_Date roles exist. The tradeoff is explicit in config below.

; crosswalk-policy.ini — projection rules consumed by the output mapper
[multiplicity]
dc.subject   = repeat        ; one element per keyword, never join
dc.format    = repeat
dct.rights   = repeat
dc.title     = first         ; take first title if the citation repeats it

[missing]
emit_empty_elements = false  ; omit the element rather than emit an empty one
required           = dc.identifier, dc.title

[date]
role_priority = publication, revision, creation
normalize_utc = true

Verification

Test the transform standalone before trusting the live endpoint, then confirm the served csw:Record over the wire.

# 1. Apply the stylesheet to a known ISO record and inspect the projected csw:Record
xsltproc iso-to-dc.xsl sample-iso19139.xml | xmllint --format -
# -> a <csw:Record> with dc:title, dc:identifier, one dc:subject per keyword, ows:BoundingBox

# 2. Count keywords in source vs dc:subject in output — must match (multiplicity preserved)
echo "source: $(xmllint --xpath 'count(//*[local-name()=\"keyword\"])' sample-iso19139.xml)"
echo "output: $(xsltproc iso-to-dc.xsl sample-iso19139.xml | xmllint --xpath 'count(//*[local-name()=\"subject\"])' -)"

# 3. Ask the live catalog for Dublin Core output and confirm the bounding box survived
curl -sS "https://catalog.example.org/csw?service=CSW&version=2.0.2&request=GetRecords\
&typenames=csw:Record&elementsetname=full\
&outputschema=http://www.opengis.net/cat/csw/2.0.2&resulttype=results" \
  | xmllint --xpath '//*[local-name()="BoundingBox"]' -

Matching keyword counts, a non-empty dc:title and dc:identifier, and a populated ows:BoundingBox with EPSG:4326 corners together confirm the crosswalk is faithful and not silently lossy.

Troubleshooting matrix

Symptom Likely cause Fix
dc:title empty in the response Wrong XPath into the nested citation, or namespace prefix mismatch Select .//gmd:citation//gmd:title/gco:CharacterString; declare gmd/gco correctly
All keywords collapse into one dc:subject Values joined instead of iterated Emit one dc:subject per keyword with xsl:for-each; set dc.subject = repeat
ows:BoundingBox missing or corners swapped Ordinates not mapped, or lower/upper reversed Lower = west+south, Upper = east+north; iterate the first EX_GeographicBoundingBox
Client rejects the record as invalid Output namespaces wrong (dc vs dct, ows version) Match the CSW 2.0.2 profile namespaces exactly for every element
Records return empty elements for absent fields Mapper emits a node even when source is missing Set emit_empty_elements = false; guard scalar outputs before emitting
Wrong or nondeterministic dc:date Multiple CI_Date roles, no priority chosen Apply role_priority = publication, revision, creation; take the first match
GetRecords ignores the crosswalk Client requested the ISO output schema, not Dublin Core Confirm outputSchema is the CSW 2.0.2 Dublin Core URI in the request
BoundingBox present but unqueryable spatially CRS attribute omitted or non-4326 Set crs="urn:ogc:def:crs:EPSG::4326" and store geometry in that CRS

For the authoritative element set and encoding rules, see the OGC Catalogue Service (CSW) standard and the Dublin Core Metadata Terms.

Up one level: CSW Catalog Schema Mapping & Validation.