FGDC Field-Mapping Reference Table for ISO Conversion

Converting FGDC CSDGM metadata to ISO 19115-3 comes down to one artifact: an explicit, element-by-element crosswalk that maps each FGDC path to its ISO counterpart, handles date and code-list normalization along the way, and flags constructs that have no direct ISO equivalent. This page provides that reference table and a runnable lxml crosswalk that consumes it.

The mapping is the crux of any FGDC to ISO 19115 conversion pipeline, and this guide belongs to the broader Automated Metadata Generation & Schema Mapping pipeline family. Large volumes of US federal geospatial data remain documented in the 1998 CSDGM format, and migrating those records to the international standard without a rigorous field map risks silently dropping mandatory elements — bounding coordinates, contact responsibility, and lineage steps that ISO harvesters and INSPIRE portals require. A published crosswalk turns that migration from an ad-hoc translation into a repeatable, auditable transform.

FGDC to ISO 19115-3 field-mapping reference

The table groups mappings by FGDC section. “Notes” flags normalization steps and cardinality changes. Paths are shown as FGDC element names and ISO 19115-3 element paths.

FGDC CSDGM element ISO 19115-3 element Notes
idinfo/citation/citeinfo/title MD_DataIdentification/citation/CI_Citation/title Direct string move
idinfo/citation/citeinfo/origin CI_Citation/citedResponsibleParty/CI_Responsibility with role originator Wrap in CI_Organisation; set role code
idinfo/citation/citeinfo/pubdate CI_Citation/date/CI_Date with dateType=publication Normalize YYYYMMDD → YYYY-MM-DD
idinfo/descript/abstract MD_DataIdentification/abstract Direct string move
idinfo/descript/purpose MD_DataIdentification/purpose Direct string move
idinfo/keywords/theme/themekey MD_DataIdentification/descriptiveKeywords/MD_Keywords/keyword One MD_Keywords block per thesaurus
idinfo/keywords/theme/themekt MD_Keywords/thesaurusName/CI_Citation/title Controlled-vocabulary source name
idinfo/status/progress MD_DataIdentification/status (MD_ProgressCode) Map “Complete” → completed
idinfo/spdom/bounding/westbc EX_GeographicBoundingBox/westBoundLongitude Decimal degrees, WGS84 assumed
idinfo/spdom/bounding/eastbc EX_GeographicBoundingBox/eastBoundLongitude Decimal degrees
idinfo/spdom/bounding/northbc EX_GeographicBoundingBox/northBoundLatitude Decimal degrees
idinfo/spdom/bounding/southbc EX_GeographicBoundingBox/southBoundLatitude Decimal degrees
idinfo/timeperd/timeinfo/rngdates EX_TemporalExtent/extent/TimePeriod Begdate/Enddate → begin/end positions
idinfo/accconst MD_DataIdentification/resourceConstraints/MD_LegalConstraints/accessConstraints Map free text to MD_RestrictionCode where possible
idinfo/useconst MD_LegalConstraints/useConstraints Preserve full text under otherConstraints
idinfo/ptcontac/cntinfo MD_DataIdentification/pointOfContact/CI_Responsibility Role pointOfContact
spdoinfo/direct MD_SpatialRepresentationTypeCode “Vector”/“Raster” → vector/grid
spref/horizsys/geograph MD_ReferenceSystem/referenceSystemIdentifier Emit EPSG code as RS_Identifier
spref/horizsys/planar/gridsys referenceSystemInfo/MD_ReferenceSystem Projected CRS → EPSG lookup
distinfo/distrib/cntinfo MD_Distribution/distributor/MD_Distributor/distributorContact Role distributor
distinfo/stdorder/digform/digtinfo/formname MD_Distribution/distributionFormat/MD_Format/name Format name string
distinfo/stdorder/digform/digtopt/onlinopt/computer/networka/networkr MD_DigitalTransferOptions/onLine/CI_OnlineResource/linkage Access URL
dataqual/lineage/procstep/procdesc LI_Lineage/processStep/LI_ProcessStep/description One LI_ProcessStep per procstep
dataqual/lineage/procstep/procdate LI_ProcessStep/dateTime Normalize to ISO 8601
dataqual/lineage/srcinfo/srccite LI_Lineage/source/LI_Source/sourceCitation Source dataset citation
dataqual/attracc/attraccr MD_DataQuality (DQ_QuantitativeAttributeAccuracy) Quality report element
dataqual/posacc/horizpa DQ_AbsoluteExternalPositionalAccuracy Positional accuracy value
eainfo/detailed/enttyp ISO 19110 FC_FeatureType (not 19115) Requires feature-catalogue output; flag for review

Automated Python Implementation

The script encodes the reference table as a mapping dictionary and drives an lxml transform. It parses an FGDC document, reads each source path, normalizes dates and code lists, and builds the corresponding ISO 19115-3 elements. Elements whose FGDC source has no direct 19115 target (the feature catalogue) are collected into a flagged list rather than dropped.

#!/usr/bin/env python3
"""
fgdc_to_iso.py — Crosswalk FGDC CSDGM XML to ISO 19115-3 elements via a mapping dict.

Usage:
    python fgdc_to_iso.py <fgdc.xml> <out_iso.xml>
"""
import sys
from lxml import etree

GMD = "http://www.isotc211.org/2005/gmd"
GCO = "http://www.isotc211.org/2005/gco"
NSMAP = {"gmd": GMD, "gco": GCO}

# FGDC XPath -> handler key. Handlers build the matching ISO element.
FIELD_MAP = {
    "idinfo/citation/citeinfo/title": "title",
    "idinfo/descript/abstract": "abstract",
    "idinfo/descript/purpose": "purpose",
    "idinfo/citation/citeinfo/pubdate": "pubdate",
    "idinfo/spdom/bounding/westbc": "west",
    "idinfo/spdom/bounding/eastbc": "east",
    "idinfo/spdom/bounding/northbc": "north",
    "idinfo/spdom/bounding/southbc": "south",
    "dataqual/lineage/procstep/procdesc": "lineage",
}

# FGDC constructs with no direct ISO 19115 target.
NO_DIRECT_TARGET = {"eainfo/detailed/enttyp"}


def normalize_date(raw: str) -> str:
    """Convert FGDC YYYYMMDD (or partial) to ISO 8601 YYYY-MM-DD."""
    digits = (raw or "").strip()
    if len(digits) == 8 and digits.isdigit():
        return f"{digits[0:4]}-{digits[4:6]}-{digits[6:8]}"
    if len(digits) == 6 and digits.isdigit():
        return f"{digits[0:4]}-{digits[4:6]}"
    if len(digits) == 4 and digits.isdigit():
        return digits
    return digits  # already normalized or free text; leave for validation to catch


def first_text(root, xpath: str):
    """Return stripped text of the first FGDC element at xpath, or None."""
    nodes = root.xpath(xpath + "/text()")
    return nodes[0].strip() if nodes else None


def _child(parent, tag_ns, tag):
    return etree.SubElement(parent, f"{{{tag_ns}}}{tag}")


def _char_string(parent, ns, tag, value):
    """Attach <ns:tag><gco:CharacterString>value</></> to parent."""
    wrapper = _child(parent, ns, tag)
    cs = _child(wrapper, GCO, "CharacterString")
    cs.text = value
    return wrapper


def build_iso(fgdc_root) -> tuple[etree._Element, list[str]]:
    """Build an ISO 19115-3 MD_Metadata tree from an FGDC root; return (tree, flagged)."""
    md = etree.Element(f"{{{GMD}}}MD_Metadata", nsmap=NSMAP)
    id_info = _child(md, GMD, "identificationInfo")
    data_ident = _child(id_info, GMD, "MD_DataIdentification")

    # Citation + title
    title = first_text(fgdc_root, "idinfo/citation/citeinfo/title")
    citation = _child(data_ident, GMD, "citation")
    ci = _child(citation, GMD, "CI_Citation")
    if title:
        _char_string(ci, GMD, "title", title)

    # Publication date
    pubdate = first_text(fgdc_root, "idinfo/citation/citeinfo/pubdate")
    if pubdate:
        date_el = _child(ci, GMD, "date")
        ci_date = _child(date_el, GMD, "CI_Date")
        d = _child(ci_date, GMD, "date")
        gd = _child(d, GCO, "Date")
        gd.text = normalize_date(pubdate)

    # Abstract + purpose
    abstract = first_text(fgdc_root, "idinfo/descript/abstract")
    if abstract:
        _char_string(data_ident, GMD, "abstract", abstract)
    purpose = first_text(fgdc_root, "idinfo/descript/purpose")
    if purpose:
        _char_string(data_ident, GMD, "purpose", purpose)

    # Geographic bounding box
    bounds = {k: first_text(fgdc_root, f"idinfo/spdom/bounding/{tag}")
              for k, tag in (("west", "westbc"), ("east", "eastbc"),
                             ("north", "northbc"), ("south", "southbc"))}
    if all(bounds.values()):
        extent = _child(data_ident, GMD, "extent")
        ex = _child(extent, GMD, "EX_Extent")
        geo = _child(ex, GMD, "geographicElement")
        bbox = _child(geo, GMD, "EX_GeographicBoundingBox")
        for iso_tag, key in (("westBoundLongitude", "west"),
                             ("eastBoundLongitude", "east"),
                             ("southBoundLatitude", "south"),
                             ("northBoundLatitude", "north")):
            wrapper = _child(bbox, GMD, iso_tag)
            dec = _child(wrapper, GCO, "Decimal")
            dec.text = bounds[key]

    # Lineage process steps
    lineage_steps = fgdc_root.xpath("dataqual/lineage/procstep/procdesc/text()")
    if lineage_steps:
        dq = _child(md, GMD, "dataQualityInfo")
        li = _child(dq, GMD, "LI_Lineage")
        for step_text in lineage_steps:
            src = _child(li, GMD, "processStep")
            ps = _child(src, GMD, "LI_ProcessStep")
            _char_string(ps, GMD, "description", step_text.strip())

    # Flag constructs with no direct ISO 19115 target
    flagged = [path for path in NO_DIRECT_TARGET if fgdc_root.xpath(path)]
    return md, flagged


def main() -> int:
    if len(sys.argv) != 3:
        print("Usage: python fgdc_to_iso.py <fgdc.xml> <out_iso.xml>", file=sys.stderr)
        return 1
    tree = etree.parse(sys.argv[1])
    iso_root, flagged = build_iso(tree.getroot())
    etree.ElementTree(iso_root).write(sys.argv[2], pretty_print=True,
                                      xml_declaration=True, encoding="UTF-8")
    if flagged:
        print("Flagged FGDC constructs needing feature-catalogue (ISO 19110) output:",
              file=sys.stderr)
        for path in flagged:
            print(f"  - {path}", file=sys.stderr)
    print(f"Wrote ISO 19115-3 record to {sys.argv[2]}")
    return 0


if __name__ == "__main__":
    raise SystemExit(main())

Two behaviors keep the crosswalk honest. The bounding box is only emitted when all four coordinates are present, so a partial extent never produces an ISO record that validates structurally but describes the wrong footprint. And eainfo entity/attribute detail is flagged, not silently discarded — that content belongs in an ISO 19110 feature catalogue, and losing it during migration is a common, hard-to-detect compliance regression.

Validation and pipeline integration

Confirm the emitted ISO record is well-formed and schema-valid before it enters the catalog. Structural validation should always precede any semantic review:

pip install "lxml>=4.9"

# Well-formedness plus schema validation against the cached ISO 19139 XSD
xmllint --noout --schema schemas/gmd/metadataEntity.xsd out_iso.xml && \
  echo "ISO record is schema-valid"
# test_fgdc_to_iso.py
from lxml import etree
from fgdc_to_iso import build_iso, normalize_date

FGDC = b"""<metadata><idinfo>
  <citation><citeinfo><title>Soils 2024</title><pubdate>20240517</pubdate></citeinfo></citation>
  <descript><abstract>County soil survey.</abstract></descript>
  <spdom><bounding><westbc>-2</westbc><eastbc>-1</eastbc>
    <northbc>52</northbc><southbc>51</southbc></bounding></spdom>
</idinfo></metadata>"""


def test_normalize_date():
    assert normalize_date("20240517") == "2024-05-17"
    assert normalize_date("202405") == "2024-05"


def test_title_and_bbox_emitted():
    root = etree.fromstring(FGDC)
    iso, flagged = build_iso(root)
    gmd = "http://www.isotc211.org/2005/gmd"
    titles = iso.xpath("//gmd:title/gco:CharacterString/text()",
                       namespaces={"gmd": gmd, "gco": "http://www.isotc211.org/2005/gco"})
    assert titles == ["Soils 2024"]
    assert iso.xpath("//gmd:EX_GeographicBoundingBox", namespaces={"gmd": gmd})
    assert flagged == []

Long-term compliance best practices

  • Treat the mapping table as versioned source. Store the crosswalk in the repository and change it through review; an undocumented mapping tweak can shift the meaning of every migrated record.
  • Flag, never drop, unmapped constructs. Route FGDC elements with no 19115 target (entity/attribute detail, unusual spatial reference forms) to a review queue and a feature-catalogue emitter rather than discarding them.
  • Normalize dates and code lists at the boundary. Convert FGDC YYYYMMDD dates and free-text status/restriction values to ISO 8601 and controlled-vocabulary codes during the transform, not afterward.
  • Validate FGDC input before converting. Run structural validation on the source CSDGM first, since malformed input produces malformed ISO that passes shallow checks but fails downstream.
  • Preserve responsibility and lineage. Confirm origin, contacts, and every procstep survive the crosswalk — these are the fields most often lost and the ones auditors check for provenance.
  • Round-trip test on representative samples. Keep a corpus of real agency records and assert the converted output stays schema-valid whenever the mapping changes.