FGDC Metadata Mapping: Implementation Patterns for Automated Schema Transformation Jump to heading
In production geospatial pipelines, FGDC Metadata Mapping operates as a deterministic transformation stage rather than a manual documentation exercise. This stage sits inside the Geospatial Schema Architecture & Standards Mapping discipline, where version-controlled configuration files replace ad-hoc translation scripts. Government data teams and Python ETL engineers require a config-as-code architecture that enforces strict schema alignment, applies configurable tolerance thresholds, and generates auditable compliance reports. This guide details the implementation of a metadata transformation stage, focusing on field-level mapping, validation rules, and fallback routing for non-conforming records.
This page covers the mapping of FGDC CSDGM source elements onto a target catalog schema — the parse, match, validate, and route loop for a single metadata standard. Where you need to emit a different standard from the mapped output, Converting FGDC CSDGM to ISO 19115 automatically handles cross-standard element translation; European conformance is owned by INSPIRE Directive Schema Compliance; and local synonym alignment lives in Local Government Data Dictionaries. This stage stops at producing a validated, mapped record and a compliance report — downstream publication and cross-platform emission are deliberately out of scope here.
Declarative Configuration Manifest Jump to heading
The foundation of a reliable transformation workflow is a declarative configuration layer. Hardcoded field translations introduce schema drift and break continuous integration pipelines. Instead, maintain a YAML mapping manifest that defines source FGDC CSDGM elements, target attributes, transformation functions, and compliance flags. When the pipeline initializes, a schema loader parses this manifest into a directed acyclic graph (DAG) of transformation nodes, validating that every mandatory rule resolves to a non-null XPath before a single record is processed.
# metadata_mapping.yaml — config-as-code mapping manifest
# Loaded once at pipeline init; PyYAML >=6.0 safe_load only.
mapping_rules:
- source: "idinfo/citation/citeinfo/title"
target: "dataset_title"
mandatory: true
strict_match: true
fallback_value: null
transform: "strip_whitespace"
- source: "idinfo/descript/abstract"
target: "summary"
mandatory: false
strict_match: false # enables synonym-dictionary resolution
fallback_value: "Abstract not provided."
transform: "normalize_newlines"
- source: "idinfo/citation/citeinfo/pubdate"
target: "publication_date"
mandatory: true
strict_match: true
fallback_value: null
transform: "iso8601_parse" # 8-digit YYYYMMDD → ISO 8601 date
- source: "idinfo/spdom/bounding"
target: "bbox"
mandatory: true
strict_match: true
fallback_value: null
transform: "fgdc_bounding_to_envelope"
Each directive controls one axis of the transform. The table below fixes the contract every rule must satisfy:
| Directive | Required | Type | Purpose |
|---|---|---|---|
source |
mandatory | string (XPath) | CSDGM element path resolved against the parsed tree |
target |
mandatory | string | Attribute name written to the output record |
mandatory |
mandatory | bool | If true, a null result rejects the record at the validation gate |
strict_match |
optional (default true) |
bool | false enables synonym-dictionary (fuzzy) resolution |
fallback_value |
optional (default null) |
any | Substituted only for non-mandatory fields when the source is absent |
transform |
optional (default identity) | string | Named coercion applied to the raw element text |
Explicit mandatory and optional field definitions prevent silent data loss. The strict_match flag dictates whether exact XPath resolution is required; when set to false, the loader wires the rule to the fuzzy resolver described under preprocessing.
Preprocessing Requirements Jump to heading
FGDC CSDGM in the wild is rarely XSD-clean. Before any rule fires, the source tree must be normalized into a predictable shape, otherwise XPath resolution silently returns None and pushes records into the fallback queue for the wrong reason. The execution engine expects each input to satisfy these shape guarantees:
- Encoding normalized to UTF-8. Legacy CSDGM exports are frequently Latin-1 or Windows-1252; decode and re-encode before parsing so
lxmldoes not abort on byte0x92smart quotes. - Whitespace and newline collapse. Multi-line
<abstract>blocks carry indentation from the original editor. Collapse runs of whitespace before matching so confidence scoring is not skewed by formatting. - Deprecated and vendor tags stripped. Esri and legacy NBII extensions inject non-CSDGM elements; strip unknown tags so the DAG only sees declared
sourcepaths. - Entity references resolved. Expand
&,&#xNN;and external entities up front; disable external entity loading to prevent XXE on untrusted government feeds. - Synonym dictionary loaded. For every rule with
strict_match: false, load the alignment dictionary — optionally seeded from Local Government Data Dictionaries — so fuzzy resolution has a vocabulary to score against.
During mapping, apply a confidence scoring mechanism: exact string matches receive 1.0, semantic matches via synonym dictionaries receive 0.7–0.9, and unmapped fields below the 0.7 threshold trigger the fallback router.
Execution Engine & Precision Guards Jump to heading
The extraction stage must handle heterogeneous inputs without blocking downstream processes. Implement a Python parser using lxml for XML-based CSDGM records and fiona for metadata embedded in shapefiles, GeoPackages, and GeoTIFFs. The engine never lets a malformed source crash the batch: parse errors, missing mandatory fields, and transform failures are caught individually and converted into structured rejection records rather than uncaught exceptions.
# requires: python >=3.10, lxml >=5.1, PyYAML >=6.0
from lxml import etree
from dataclasses import dataclass, field
from typing import Any, Optional
import logging
log = logging.getLogger("fgdc.map")
@dataclass
class MappedRecord:
values: dict[str, Any] = field(default_factory=dict)
scores: dict[str, float] = field(default_factory=dict)
rejected: bool = False
reason: Optional[str] = None
class MappingEngine:
SECURE_PARSER = etree.XMLParser(
resolve_entities=False, no_network=True, huge_tree=False
)
def __init__(self, config: dict[str, Any], threshold: float = 0.7):
self.rules = config["mapping_rules"]
self.threshold = threshold
def resolve(self, raw_xml: bytes) -> MappedRecord:
rec = MappedRecord()
try:
tree = etree.fromstring(raw_xml, parser=self.SECURE_PARSER)
except etree.XMLSyntaxError as exc:
rec.rejected, rec.reason = True, f"parse_error: {exc}"
log.warning("CSDGM parse failed: %s", exc)
return rec
for rule in self.rules:
node = tree.find(rule["source"])
value = node.text.strip() if node is not None and node.text else None
score = 1.0 if value is not None else 0.0
if value is None and rule["mandatory"]:
rec.rejected = True
rec.reason = f"missing_mandatory: {rule['target']}"
log.info("reject %s: %s", rule["target"], rec.reason)
return rec # fail fast — do not emit a partial record
if value is None and not rule["mandatory"]:
value, score = rule.get("fallback_value"), 0.0
rec.values[rule["target"]] = value
rec.scores[rule["target"]] = score
return rec
The SECURE_PARSER disables entity resolution and network access, closing the XXE vector that untrusted FGDC feeds can carry. Mandatory failures short-circuit the record so no partially mapped output reaches the validation gate. After mapping, a Pydantic model mirrors the target specification and applies type coercion before the record is accepted:
# requires: pydantic >=2.6
from pydantic import BaseModel, Field, ConfigDict, ValidationError
from typing import Optional
class TargetMetadata(BaseModel):
model_config = ConfigDict(populate_by_name=True, str_strip_whitespace=True)
dataset_title: str = Field(..., min_length=1)
publication_date: str # ISO 8601, already coerced by iso8601_parse
summary: Optional[str] = None
def validate(rec: "MappedRecord") -> "MappedRecord":
try:
TargetMetadata(**rec.values)
except ValidationError as exc:
rec.rejected = True
rec.reason = f"schema_violation: {exc.error_count()} error(s)"
return rec
Mandatory fields use ... (Ellipsis) to enforce presence at runtime; optional fields default to None or fallback strings. This immediate validation gate prevents non-conforming records from propagating into spatial data catalogs.
Failure Modes & Fallback Routing Jump to heading
Non-conforming records are never discarded. Every failure type maps to one deterministic recovery action, so there are no silent drops and every rejection is reproducible from the audit trail.
| Failure type | Likely cause | Deterministic recovery action |
|---|---|---|
parse_error |
Malformed XML, bad encoding, truncated export | Quarantine raw bytes, log XMLSyntaxError, flag for re-export |
missing_mandatory |
Source CSDGM omits a required element | Route to fallback queue with the missing target name attached |
schema_violation |
Value fails Pydantic type/length coercion | Quarantine with field-level error list; do not auto-substitute |
low_confidence |
All fuzzy matches scored < 0.7 |
Hold for manual review; surface candidate synonyms in the log |
xxe_blocked |
External entity reference in untrusted feed | Reject record, raise security alert, do not retry automatically |
Records that fail mandatory validation pass through a fallback router that quarantines the payload, attaches diagnostic logs, and triggers a manual review workflow. This pattern is critical when migrating legacy FGDC records toward modern standards, where historical data gaps are common — particularly when the eventual target is INSPIRE Directive Schema Compliance or an ISO 19115 conversion, both of which impose stricter cardinality than CSDGM ever required.
Compliance Reporting Output Jump to heading
The stage writes a machine-readable compliance report for every batch so audits never depend on re-running the pipeline. Each accepted and rejected record contributes a row carrying lineage and decision provenance:
{
"record_id": "csdgm/parcels_2009.xml",
"status": "rejected",
"reason": "missing_mandatory: publication_date",
"field_status": {
"dataset_title": { "value": "County Parcels", "confidence": 1.0 },
"publication_date": { "value": null, "confidence": 0.0 }
},
"lineage": {
"source_standard": "FGDC-STD-001-1998",
"mapping_manifest": "metadata_mapping.yaml@a1b9f3c",
"engine_version": "fgdc-map 2.4.0",
"processed_at": "2026-06-25T14:02:11Z"
}
}
The lineage block pins the exact manifest commit and engine version that produced each decision, which is what satisfies a federal compliance audit: any reviewer can reproduce the rejection from the recorded inputs. Confidence scores are retained per field so synonym-matched values (0.7–0.9) are traceable and can be promoted to strict rules once verified. The rejection log uses the same reason codes as the failure-mode table, so dashboards aggregate failures without parsing free-text messages.
CI Integration Jump to heading
Embed the transformation stage into continuous integration to enforce schema compliance before data publication. The job blocks merges when any mandatory field fails validation, ensuring only auditable, standards-compliant metadata reaches production catalogs.
# .github/workflows/fgdc-validate.yml
name: FGDC Metadata Validation
on:
push:
paths: ['data/metadata/**.xml', 'config/mapping.yaml']
jobs:
validate:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: '3.10'
- name: Install dependencies
# lxml: CSDGM parsing · fiona: spatial-file metadata · pydantic: validation
run: pip install "pydantic>=2.6" "lxml>=5.1" "fiona>=1.9" "PyYAML>=6.0"
- name: Run schema validation
run: python -m pipeline.validate data/metadata/ config/mapping.yaml
- name: Upload compliance report
if: always() # publish the report even on failure
uses: actions/upload-artifact@v4
with:
name: metadata-audit-report
path: reports/compliance_*.json
A matching pre-commit hook running the same pipeline.validate entry point catches missing mandatory elements before they ever reach the remote, keeping the audit trail clean. The same gate pattern is reused across INSPIRE Directive Schema Compliance and Cross-Platform Schema Translation, so a single CI convention covers every standard your catalog ingests.
Deeper Implementation Guides Jump to heading
- Converting FGDC CSDGM to ISO 19115 automatically — deterministic element translation matrices for emitting ISO 19115 from the mapped output of this stage.
Related Jump to heading
- Geospatial Schema Architecture & Standards Mapping — the parent discipline this mapping stage belongs to.
- INSPIRE Directive Schema Compliance — Annex II/III conformance for European interoperability.
- Local Government Data Dictionaries — synonym sources that feed fuzzy field resolution.
- Cross-Platform Schema Translation — emitting mapped records across catalog and database targets.
- Automated Attribute Transformation & ETL Workflows — the broader attribute-level transformation discipline these metadata rules plug into.