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
YYYYMMDDdates 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 everyprocstepsurvive 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.
Related
- FGDC to ISO 19115 Conversion Pipelines — the parent guide covering the full conversion architecture
- Validating FGDC Metadata Against XML Schemas — the input-validation step that must pass before conversion
- ISO 19115 Metadata Template Generation — building the ISO templates this crosswalk populates
- Metadata Schema Validation & Linting — validating the ISO output after conversion