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.

The CSDGM sections and where each lands in ISO 19115-3The five top-level CSDGM sections and the ISO 19115-3 packages that receive their content.CSDGM documentseven top-level sectionsidinfoidentificationMD_DataIdentificationtitle, abstract, extentMD_Constraintsuse and access limitsdataqualdata qualityDQ_DataQualitylineage, reportsspdoinfo + sprefspatial + referenceMD_SpatialRepresentationvector or gridMD_ReferenceSystemCRS identifiermetainfoabout the metadataMD_Metadata rootcontact, date, standard
The shape of the crosswalk in one picture: five sections in, four packages out, and one section with nowhere to go.
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.

How one crosswalk row is appliedA crosswalk row names a source path, an optional transform, a target path and a policy for what to do when the source is absent.Source pathXPath into the CSDGM treereadTransformdate parse, code lookup, identitywriteTarget pathISO element to createif absentAbsence policydefault, omit, or fail
The fourth column is the one that stops a crosswalk from failing silently.
#!/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:

Where a first pass over a CSDGM archive actually failsDistribution of conversion failures by cause across a first run over a legacy CSDGM archive.Unparseable date format38%of 460 recordsCode list value unmapped27%Missing mandatory contact16%Bounding box out of range11%Malformed source XML8%
Two thirds of first-run failures are dates and code lists, not structure.
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.