Implementing Field Renaming & Type Coercion Rules in Geospatial ETL Pipelines Jump to heading

Standardizing heterogeneous spatial datasets requires deterministic attribute transformation at the ingestion boundary. Municipal agencies, environmental monitoring networks, and legacy mapping programs rarely submit data that aligns with enterprise geodatabase schemas: column names collide, parcel identifiers arrive as zero-padded strings, elevations carry inconsistent precision, and dates appear in three different formats within a single feed. This pipeline stage operates at the core of the Automated Attribute Transformation & ETL Workflows discipline, resolving that structural mismatch by enforcing strict field renaming and type coercion rules before spatial indexing or database loading. It converts variable input formats into a canonical attribute table while preserving audit trails and enforcing compliance thresholds.

This page covers the contract, execution, and audit output for renaming and casting attributes. It deliberately stops at the attribute boundary: recursive property extraction is handled upstream by Nested JSON/GeoJSON Flattening, coordinate-system alignment by CRS Normalization & Sync, and large-volume orchestration by Batch Schema Processing Pipelines. Transient faults and replay are the concern of Error Handling & Retry Logic, which this stage feeds with deterministic, classifiable errors.

Field Renaming and Type Coercion Flow A left-to-right flow showing a raw source field mapped through a rename step to its canonical name, coerced to the declared target type, then checked by a validation gate before being written to the standardized output table. Source Field PROP_ID · "1043" Rename → parcel_id Type Coerce str → int64 Valid? tolerance Canonical Table Fallback / Quarantine pass fail

Declarative Configuration Manifest Jump to heading

Pipeline operators must externalize transformation logic into version-controlled YAML or JSON manifests. Embedding mapping logic in procedural scripts breaks reproducibility and complicates regulatory audits, because the rule that produced a value can no longer be tied to a specific commit. The configuration contract defines the exact transformation behavior, separating business rules from execution engines so that the same manifest produces identical output on every runner.

Mandatory vs. Optional Field Definitions Jump to heading

Field Requirement Description
source_field Mandatory Original column name or flattened JSON path in the input dataset.
target_field Mandatory Canonical column name matching the enterprise schema.
target_type Mandatory Target data type (string, int32, int64, float32, float64, date32, geometry).
nullable Mandatory Boolean flag dictating whether missing values trigger rejection or fallback.
tolerance_threshold Optional Maximum allowed deviation for numeric/date parsing before the value is flagged as drift.
date_format Optional Explicit parsing directive (e.g., %Y-%m-%d, epoch_ms). Defaults to ISO-8601.
fallback_value Optional Default substitution when nullable: true and input is missing or non-conforming.
precision Optional Decimal places for float types. Enforces rounding or truncation guards.

A minimal YAML manifest carries one rule per canonical column. Mandatory keys appear on every rule; optional keys refine numeric and temporal behaviour:

yaml
# schema_contract.yaml — PyYAML >=6, consumed by pyarrow >=14 coercion engine
schema_contract:
  version: "1.0"
  compliance_profile: "FGDC-STD-001"
  rules:
    - source_field: "parcel_id_raw"   # zero-padded string in source
      target_field: "parcel_id"
      target_type: "int64"
      nullable: false                 # missing → halt batch, never substitute
    - source_field: "measurement_val"
      target_field: "elevation_meters"
      target_type: "float64"
      nullable: true
      tolerance_threshold: 0.01       # survey accuracy band, metres
      precision: 3                    # round to millimetre
      fallback_value: null
    - source_field: "obs_date"
      target_field: "observation_date"
      target_type: "date32"
      nullable: false
      date_format: "iso8601"

The same contract feeds both the runtime engine and the CI linter described below, so a manifest that a reviewer approves in a pull request is byte-for-byte the manifest that runs in production.

The Schema Contract as a Single Source of Truth A single version-controlled schema_contract.yaml file, pinned to a commit hash, fans out to three consumers that all read the identical manifest: the runtime coercion engine that renames and casts fields, the CI linter that gates the contract before merge, and the audit record that stamps the contract version and commit into every run so each canonical value is traceable. schema_contract.yaml version 1.0 · commit a3f91c4 version-controlled Single source of truth Runtime coercion engine renames + casts each field CI linter gates the contract before merge Audit record stamps version + commit per run identical manifest identical manifest identical manifest

Preprocessing & Schema Normalization Jump to heading

Before applying type rules, nested structures must be normalized into a flat tabular layout. Modern GeoJSON payloads frequently embed administrative codes, measurement units, or temporal metadata within hierarchical dictionaries, and a source_field that points at properties.survey.elevation cannot be cast until it is a real column. The Nested JSON/GeoJSON Flattening routine extracts these values into predictable columns, establishing a consistent row structure for downstream processing.

Once flattened, the engine iterates through each record, matching column headers against the configuration manifest and initializing an output schema buffer with the declared target columns. Adherence to RFC 7946 keeps property extraction standards-compliant across vendor implementations, so a feed exported from a CAD package and one served from an OGC API both reduce to the same flat shape before coercion runs.

Execution Engine & Precision Guards Jump to heading

Field renaming and type coercion execute as vectorized casting operations with explicit tolerance boundaries, not row-by-row Python loops. Numeric conversion requires precision guards to prevent silent truncation or overflow: casting string-encoded parcel values to float64 must reject entries containing non-numeric artifacts rather than coercing them to NaN, and rounding to the declared precision must happen inside the cast so downstream consumers never see spurious decimal digits. Date parsing accommodates ISO-8601, epoch milliseconds, and regional formats, logging deviations that fall outside the configured variance window.

The reference implementation uses pyarrow safe casting, which raises ArrowInvalid on any value that cannot be represented in the target type — the property that makes the stage deterministic rather than lossy. The Writing robust Python scripts for automated field type casting walkthrough expands this into a full per-field harness with structured logging.

python
# pyarrow >=14, Python >=3.10
import pyarrow as pa
import pyarrow.compute as pc

TYPE_MAP = {
    "int32": pa.int32(),
    "int64": pa.int64(),
    "float32": pa.float32(),
    "float64": pa.float64(),
    "string": pa.string(),
    "date32": pa.date32(),
}


def apply_coercion(table: pa.Table, rule: dict) -> pa.Table:
    """Rename `source_field` to `target_field` and coerce it to `target_type`.

    Raises on a missing or non-conforming mandatory field so the batch halts
    deterministically; applies a fallback only when `nullable` is True.
    """
    src = rule["source_field"]
    tgt = rule["target_field"]
    dtype_str = rule["target_type"]
    nullable = rule.get("nullable", False)

    arrow_type = TYPE_MAP.get(dtype_str)
    if arrow_type is None:
        raise ValueError(f"Unknown target_type '{dtype_str}' for field '{src}'")

    # Missing source column: fallback only if explicitly permitted.
    if src not in table.column_names:
        if nullable:
            return table.append_column(tgt, pa.nulls(len(table), type=arrow_type))
        raise KeyError(f"Mandatory field '{src}' missing from input schema.")

    col = table.column(src)
    try:
        if dtype_str == "float64":
            casted = pc.cast(col, pa.float64(), safe=True)
            if "precision" in rule:
                casted = pc.round(casted, ndigits=rule["precision"])
        elif dtype_str == "date32":
            # ISO-8601 date strings → timestamp → date32
            casted = pc.strptime(col, format="%Y-%m-%d", unit="s")
            casted = pc.cast(casted, pa.date32())
        else:
            casted = pc.cast(col, arrow_type, safe=True)

        src_idx = table.column_names.index(src)
        return table.set_column(src_idx, tgt, casted)

    except (pa.ArrowInvalid, pa.ArrowNotImplementedError) as exc:
        if nullable:
            fallback_col = pa.nulls(len(col), type=arrow_type)
            src_idx = table.column_names.index(src)
            return table.set_column(src_idx, tgt, fallback_col)
        raise RuntimeError(f"Coercion failed for '{src}' → '{tgt}': {exc}") from exc

Null Handling & Compliance Alignment Jump to heading

Null propagation must align with data governance mandates. When nullable: false, missing or malformed values trigger immediate pipeline failure, preventing corrupt records from entering production geodatabases. When nullable: true, the engine substitutes fallback_value or native nulls while emitting structured warning logs that name the offending field. Temporal fields require special attention: epoch timestamps must be validated against reasonable bounds (e.g., 1970-01-01 to 2100-01-01) so an out-of-range integer never casts into a plausible-looking but wrong date.

Failure Modes & Fallback Routing Jump to heading

No coercion failure may be swallowed silently. Every non-conforming value resolves to one of a small set of deterministic outcomes, and the chosen outcome is recorded so an auditor can reconstruct exactly why a record left the canonical path. The decision below shows how a single failing value is routed — the same two questions (is the field mandatory? is the defect structural?) settle every case in the table that follows; the quarantine and retry outcomes hand off cleanly to Error Handling & Retry Logic.

Deterministic Failure Routing for a Coercion Defect A decision tree. A coercion failure is first tested for whether it is a contract or schema defect, such as a missing mandatory field or an unsupported target type, which halts the whole batch. Otherwise the failing value is tested against the field's nullable flag: a nullable field receives the declared fallback value and a logged warning, while a non-nullable field with a malformed value is quarantined with a logged reason and handed to error handling and retry logic. Every path ends at the audit trail. Coercion failure value not representable Contract or schema defect? yes — missing mandatory field / unknown type Halt batch schema_violation no — per-record value nullable: true? yes Apply fallback fallback_value + warn no Quarantine record log raw value + reason Audit trail every outcome recorded with field + reason
Failure type Likely cause Deterministic recovery action
Missing mandatory field source_field absent from flattened input Halt the batch; emit schema_violation with the missing field name.
Non-numeric in numeric cast Free-text artifact ("N/A", units suffix) in a float64/int64 column Quarantine the record; log raw value and rule id.
Precision overflow Value exceeds declared precision after rounding band Round inside the safe cast; flag as precision_drift if delta > tolerance_threshold.
Unparseable date Format mismatch vs. date_format (regional vs. ISO-8601) Try declared format, then ISO-8601 fallback; quarantine on miss.
Out-of-range temporal value Epoch integer outside 1970-01-012100-01-01 Reject value; route record to quarantine, never substitute.
Null in non-nullable field Source emitted null where nullable: false Halt the batch; no fallback is permitted.
Type not in TYPE_MAP Manifest references an unsupported target_type Fail fast at load time before any record is processed.

Mandatory-field and unsupported-type failures stop the run because they indicate a contract or schema defect that retrying cannot fix; per-record value failures are quarantined so the rest of the batch proceeds and the bad records can be inspected or replayed.

Compliance Reporting Output Jump to heading

Each coercion run writes a structured audit record that ties every canonical value back to its source rule. This is the lineage evidence government data teams need to defend a published dataset under FGDC or ISO 19115 review. The audit record is JSON and includes per-field cast counts, the contract version and commit, fallback applications, drift warnings, and the quarantine log:

json
{
  "stage": "field_renaming_type_coercion",
  "contract_version": "1.0",
  "contract_commit": "a3f91c4",
  "run_id": "2026-06-25T14:02:11Z-batch-0007",
  "records_in": 41822,
  "records_out": 41790,
  "fields": {
    "parcel_id":        {"source": "parcel_id_raw",   "cast": "string→int64",  "coerced": 41822, "failed": 0},
    "elevation_meters": {"source": "measurement_val", "cast": "string→float64", "coerced": 41790, "fallback": 32, "precision_drift": 4},
    "observation_date": {"source": "obs_date",        "cast": "string→date32",  "coerced": 41790, "quarantined": 0}
  },
  "quarantine": [
    {"row": 1188, "field": "elevation_meters", "raw": "N/A", "reason": "non_numeric"},
    {"row": 9043, "field": "elevation_meters", "raw": "1e9", "reason": "out_of_range"}
  ],
  "schema_drift_warnings": 4
}

records_in minus records_out must equal the quarantine count exactly; any discrepancy means a record was lost without a logged reason and the run is treated as failed. Downstream catalog publication consumes this record as the lineage manifest for the standardized table.

Record-Conservation Ledger Gating the Run Every input row of a batch is conserved. The records_in count of 41822 splits into two accounted outputs: 41790 records_out written to the canonical table, and 32 quarantined records each carrying a logged reason. An equality gate checks that records_in equals records_out plus quarantined; when the books balance the run passes, and any discrepancy means a record was lost without a logged reason and the run is failed. records_in 41822 records_out → canonical table 41790 quarantined + logged reason 32 in = out + quarantined? Run passes balanced: 41790 + 32 = 41822 any mismatch → run failed

CI Integration Jump to heading

The schema contract is code and must be gated like code. A pre-commit hook and a GitHub Actions step lint the manifest structure and run a synthetic-row coercion test, so a malformed or semantically broken contract never reaches a runner. The lint asserts that every rule carries the four mandatory keys and that each target_type exists in the engine’s TYPE_MAP.

yaml
# .github/workflows/schema-validation.yml
name: Validate Transformation Manifest
on: [push, pull_request]
jobs:
  lint-schema:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - name: Validate YAML contract
        run: |
          pip install "pyyaml>=6"
          python - <<'PY'
          import yaml
          ALLOWED = {"string","int32","int64","float32","float64","date32","geometry"}
          MANDATORY = {"source_field","target_field","target_type","nullable"}
          with open("schema_contract.yaml") as f:
              doc = yaml.safe_load(f)
          rules = doc.get("schema_contract", {}).get("rules")
          assert rules, "Missing rules block"
          for r in rules:
              missing = MANDATORY - r.keys()
              assert not missing, f"Rule {r.get('target_field')} missing keys: {missing}"
              assert r["target_type"] in ALLOWED, f"Unsupported target_type: {r['target_type']}"
          print(f"Manifest valid: {len(rules)} rules.")
          PY

For deeper coverage, the same job can import apply_coercion and run it over a fixture table of known-good and known-bad rows, asserting that mandatory violations raise and that optional fallbacks land in the audit record. The Batch Schema Processing Pipelines stage reuses this fixture so contract changes are validated at both the field level and the batch level before merge.

Validation & Debugging Workflow Jump to heading

When coercion fails in production, systematic isolation prevents pipeline stalls. Work the failure from contract to data:

  1. Validate input types. Confirm source columns match expected formats before casting; a string→int64 cast failing usually means the flattening step emitted a nested object, not a scalar.
  2. Check precision limits. Verify float and decimal targets do not exceed hardware or database constraints, and that precision matches the destination column definition.
  3. Review tolerance configs. Adjust tolerance_threshold to match sensor accuracy or survey precision so legitimate measurements are not flagged as drift.
  4. Inspect null propagation. Trace fallback logic to ensure mandatory fields never silently absorb invalid data — the audit record’s fallback counts should be zero for every nullable: false field.

Implementing field renaming and type coercion as a hardened, configuration-driven stage keeps spatial data pipelines reproducible, auditable, and compliant across heterogeneous ingestion sources.

Frequently asked questions Jump to heading

What is the difference between precision and tolerance_threshold in a coercion rule?

precision controls how many decimal places a float value is rounded to inside the safe cast, eliminating spurious trailing digits before the value is stored. tolerance_threshold is a separate drift gate: after rounding, if the value moved by more than the threshold it is flagged as precision_drift in the audit record. In short, precision reshapes the value and tolerance_threshold decides whether that reshaping stayed inside the field’s survey accuracy band.

When should a coercion failure halt the whole batch versus quarantine a single record?

Halt the batch only for contract or schema defects that retrying cannot fix: a missing mandatory field, a null in a nullable: false field, or a target_type the engine does not support. These mean the contract no longer matches the data, so every subsequent record would fail identically. Per-record value defects — a free-text artifact in a numeric column, an out-of-range epoch integer — are quarantined with a logged reason so the rest of the batch proceeds and the bad rows can be inspected or replayed through Error Handling & Retry Logic.

Why use pyarrow safe casting instead of pandas astype for type coercion?

pandas astype silently coerces non-conforming values to NaN, which makes the stage lossy and non-deterministic: a corrupt parcel id disappears instead of raising. pyarrow safe casting raises ArrowInvalid on any value that cannot be represented in the target type, so every failure is classifiable and routable. That raise-on-fail behaviour is exactly what lets the audit record account for every input row, since records_in minus records_out must equal the quarantine count.

Should the schema contract live with the pipeline code or with the data?

The contract is code. It is version-controlled alongside the pipeline, gated in CI, and pinned to the commit hash that is written into every audit record. That ties each canonical value back to the exact rule and commit that produced it — the lineage evidence FGDC or ISO 19115 review requires.

Deeper implementation guides Jump to heading