Skip to content
Ingestion & Parsing

Automated Field Mapping & Data Normalization

Every Driver Vehicle Inspection Report (DVIR) that reaches the compliance store must present the same field names, the same types, and the same controlled vocabularies regardless of whether it originated on a native mobile app, a scanned paper form, or a telematics feed — because 49 CFR § 396.11(a) attaches recordkeeping obligations to specific fields (driver identity, VIN, preparation timestamp, per-component condition, and the certification signature) that downstream logic cannot enforce if a vendor labels them unitNo, unit_number, or Truck # interchangeably. Field mapping and normalization is the stage inside the DVIR Ingestion & Digital/Paper Parsing Workflows pipeline that folds every vendor-specific payload into one canonical record. When it is done loosely — coercing a missing timestamp to “now”, silently dropping an unrecognized defect code — the carrier loses the ability to prove a § 396.11© certification chain, and a mislabeled safety defect can slip past the Critical vs Non-Critical Routing Logic gate that decides whether a truck is legally allowed to move.

Field mapping and normalization data flow Three heterogeneous inputs enter on the left: a mobile app JSON payload with keys unitNumber and odo, an OCR extraction with the vendor label Truck number and per-token confidence, and a telematics event with keys vehicle_id and hubometer. Each carries vendor-specific keys and converges on a rule-based field mapper that resolves aliases against a versioned YAML table. Mapped fields pass through a three-stage normalizer — lexical cleaning, semantic mapping, then type coercion — into a canonical-schema validation gate. The gate forks two ways: an accept path writes the validated record to the write-once WORM store, and a reject path sends missing-signature, out-of-range, or unresolved-token payloads to the dead-letter queue. A parallel branch tees the original raw value and its SHA-256 hash from every stage into the audit log so each decision can be reproduced. Heterogeneous inputs Mobile app JSON unitNumber, odo near-canonical keys OCR extraction "Truck #" + conf scanned / handwritten Telematics event vehicle_id, hubometer feed payload Field mapper resolve_key() alias → canonical rule_version pinned Three-stage normalizer 1 · lexical cleaning 2 · semantic mapping 3 · type coercion Validation gate Pydantic contract WORM store append-only SHA-256 chain accept Dead-letter queue failure manifest held for correction reject missing sig · out-of-range · bad token Raw-value preservation → audit log original bytes + raw_payload_sha256

Prerequisites and Environment Setup

Anchor link to "Prerequisites and Environment Setup"

The mapper is a deterministic transform that runs immediately after channel decoding and before the validation gate. It does not fetch, guess, or enrich from external services during a single record’s transformation — every mapping decision must be reproducible from the payload plus a versioned rule set. Target Python 3.10+ (the code uses match statements, StrEnum, and structural typing) with:

  • Pydantic 2.x — the canonical output contract and runtime validation of the normalized record.
  • rapidfuzz — fuzzy alias resolution and OCR artifact correction against the controlled vocabularies.
  • python-dateutil / zoneinfo (stdlib) — timezone-aware parsing of driver-entered and vendor timestamps.
  • polars — high-throughput batch normalization when end-of-shift bursts are drained from the queue.
  • PyYAML — externalized mapping tables so compliance officers can revise aliases and thresholds without a code deploy.

The normalized output is not a free-form dict: it must satisfy the canonical extracted-field contract owned by the Standardized DVIR JSON Schema Design reference. This page adds only the alias-resolution, cleaning, and coercion logic that gets a raw payload to that contract; defect codes themselves resolve against the controlled taxonomy in Defect Taxonomy Mapping for Heavy Trucks.

The mapper operates on two records: the loosely-typed inbound payload it consumes (an arbitrary key/value envelope from one channel) and the canonical record it emits. The table below is the canonical output contract this layer targets; field names and enumerations are held identical to the schema and defect-classification pages so a single record can be reconstructed across the pipeline during a DOT audit.

Field Type Enumeration / Range Compliance tag
dvir_id string (UUID) client-generated Deduplication key; one inspection event → one record
driver_id string validated against roster § 396.11(a) driver identity
asset_id string (17-char VIN) ISO 3779 VIN or fleet unit ID Asset-level OOS / recall cross-reference
prepared_at ISO8601 UTC, timezone-aware § 396.11(a) completion-of-work timestamp
odometer integer (miles) 0 – 3_000_000 Maintenance-interval and PM scheduling
component_code string SAE J1939 SPN / OEM fault tree Maps to OOS component tables
defect_severity enum minor, major, critical Feeds severity scoring and routing
signature_present bool true / false § 396.11(a) certification; false rejects
source_channel enum mobile, paper, telematics Provenance for confidence weighting
raw_payload_sha256 string (hex) Evidentiary hash of the original bytes

Normalization must be a pure function: identical inputs yield identical outputs with no side effects. Each raw field passes through three sequential stages before it is admitted to a canonical column.

Stage Purpose Example: odometer
Lexical cleaning Strip units, separators, and OCR noise; collapse whitespace "142,305 mi""142305"
Semantic mapping Resolve aliases and shorthand into a controlled vocabulary "S", "Minor", "cosmetic"defect_severity=minor
Type coercion Cast to the declared type; reject if it cannot validate "142305"142305 (int, in-range)

The raw value is never discarded. Every record carries raw_payload_sha256 and the original field is preserved in the audit envelope, so a normalization decision can be reproduced and challenged during an audit.

The mapper resolves vendor keys against an externalized alias table, cleans and coerces each value, and constructs a Pydantic record that either validates or is rejected. Fold no clocks, network calls, or randomness into the transform — resolve those before or after so the mapping is replayable from the audit log.

Normalization state machine for a single field A left-to-right state machine tracing one field through the transform. It starts at RAW, advances to ALIASED when resolve_key matches the vendor key to a canonical field, then to CLEANED after lexical cleaning strips units and OCR noise, then to COERCED after casting to the declared type. The COERCED state feeds the VALIDATED decision, which forks two ways. If all mandatory fields are present and typed, it transitions to ADMITTED and the record is written to the WORM store. If the signature is missing, a value is out of range, or a defect code is unresolved, it transitions to REJECTED, which routes to the dead-letter queue carrying a structured failure manifest for correction. RAW inbound key/value ALIASED resolve_key() CLEANED strip units / noise COERCED cast to type VALID? contract ADMITTED → WORM store all fields present + typed REJECTED → dead-letter queue missing signature · out-of-range unresolved defect code structured failure manifest
python
from __future__ import annotations

import re
from datetime import datetime
from enum import StrEnum
from zoneinfo import ZoneInfo

from dateutil import parser as dateparser
from pydantic import BaseModel, Field, ValidationError, field_validator
from rapidfuzz import process, fuzz


class DefectSeverity(StrEnum):
    MINOR = "minor"
    MAJOR = "major"
    CRITICAL = "critical"


# Externalized in YAML in production; inlined here for clarity. The mapper
# NEVER hard-codes vendor keys in Python — compliance officers own this table.
FIELD_ALIASES: dict[str, tuple[str, ...]] = {
    "asset_id": ("vin", "unit", "unit_no", "unitNumber", "truck", "vehicle_id"),
    "odometer": ("odo", "mileage", "miles", "hubometer"),
    "prepared_at": ("timestamp", "inspection_time", "completed_at", "date"),
    "driver_id": ("driver", "operator", "emp_id", "driver_code"),
}

SEVERITY_VOCAB: dict[str, DefectSeverity] = {
    "s": DefectSeverity.MINOR, "minor": DefectSeverity.MINOR, "cosmetic": DefectSeverity.MINOR,
    "m": DefectSeverity.MAJOR, "major": DefectSeverity.MAJOR, "needs repair": DefectSeverity.MAJOR,
    "c": DefectSeverity.CRITICAL, "critical": DefectSeverity.CRITICAL, "oos": DefectSeverity.CRITICAL,
}


def resolve_key(raw_key: str) -> str | None:
    """Map a vendor key to its canonical field, tolerating case and punctuation."""
    needle = raw_key.strip().lower().replace(" ", "_")
    for canonical, aliases in FIELD_ALIASES.items():
        if needle == canonical or needle in {a.lower() for a in aliases}:
            return canonical
    return None


def clean_odometer(raw: str) -> str:
    """Lexical stage: drop thousands separators and trailing unit tokens."""
    return re.sub(r"[^\d]", "", raw.split(".")[0])  # "142,305 mi" -> "142305"


def map_severity(raw: str) -> DefectSeverity:
    """Semantic stage: resolve driver shorthand to the controlled enum, with a
    fuzzy fallback so 'crtical' (an OCR/typo miss) still lands on CRITICAL."""
    token = raw.strip().lower()
    if token in SEVERITY_VOCAB:
        return SEVERITY_VOCAB[token]
    match = process.extractOne(token, SEVERITY_VOCAB.keys(), scorer=fuzz.ratio, score_cutoff=80)
    if match is None:
        raise ValueError(f"unresolved severity token: {raw!r}")  # never default silently
    return SEVERITY_VOCAB[match[0]]

The canonical record is a frozen Pydantic model. Coercion that cannot validate — a VIN of the wrong length, an odometer outside the plausible range, an absent signature — raises rather than admitting a guess.

python
class CanonicalDVIR(BaseModel, frozen=True):
    dvir_id: str
    driver_id: str
    asset_id: str = Field(min_length=11, max_length=17)
    prepared_at: datetime
    odometer: int = Field(ge=0, le=3_000_000)
    defect_severity: DefectSeverity
    signature_present: bool
    source_channel: str
    raw_payload_sha256: str

    @field_validator("prepared_at")
    @classmethod
    def must_be_tz_aware(cls, v: datetime) -> datetime:
        # § 396.11(a) completion timestamp must be unambiguous in UTC.
        if v.tzinfo is None:
            raise ValueError("prepared_at must be timezone-aware")
        return v

    @field_validator("signature_present")
    @classmethod
    def certification_required(cls, v: bool) -> bool:
        if not v:
            raise ValueError("missing § 396.11(a) certification signature")
        return v


def normalize(raw: dict, *, default_tz: str = "UTC") -> CanonicalDVIR:
    """Pure transform: raw channel payload -> validated canonical record."""
    mapped: dict[str, object] = {}
    for k, v in raw.items():
        canonical = resolve_key(k)
        if canonical is None:
            continue  # unknown keys are preserved in the raw envelope, not the record
        match canonical:
            case "odometer":
                mapped["odometer"] = int(clean_odometer(str(v)))
            case "prepared_at":
                dt = dateparser.parse(str(v))
                mapped["prepared_at"] = dt.replace(tzinfo=ZoneInfo(default_tz)) if dt.tzinfo is None else dt
            case _:
                mapped[canonical] = v
    return CanonicalDVIR(**mapped)  # raises ValidationError if any contract is violated

When a payload arrives pre-structured from a driver-facing app, the Mobile App DVIR Export Integration usually delivers near-canonical keys, so resolve_key is a near-identity pass and only timezone harmonization is required. Scanned and handwritten submissions are the hard case: the PDF & Image OCR Pipeline Setup emits raw text plus per-token confidence, and the fuzzy fallback in map_severity is where OCR artifacts like crtical or a transposed VIN character are reconciled — or, below the confidence floor, escalated to human review rather than admitted as a guess. Free-text defect descriptions and driver shorthand that need context-aware handling are covered in depth by Normalizing Inconsistent Driver Input Fields.

Compliance Thresholding and Routing

Anchor link to "Compliance Thresholding and Routing"

Normalization is a compliance gate, not a formatting convenience: each outcome maps to a concrete FMCSA obligation, and the fail-closed default is rejection, never admission. The table below is the authoritative mapping compliance officers should reference when reconciling the dead-letter queue against inspection records.

Computed condition Mapping action DOT / FMCSA obligation
All mandatory fields present, typed, and in range Admit to WORM store 49 CFR § 396.11(a): complete written report captured
signature_present is false Reject the payload; return to client for correction 49 CFR § 396.11(a): report must be signed by the preparing driver
Severity token unresolved after fuzzy match Reject; route to the dead-letter queue with a failure manifest Never default a safety token to minor — an unclassified defect is a compliance gap
OCR field below the confidence floor (e.g. VIN, signature) Hold for human verification Admitting a guessed VIN breaks asset-level OOS and recall cross-reference
prepared_at not timezone-aware or in the future Reject; escalate for reconciliation An ambiguous completion timestamp cannot anchor the § 396.11© certification clock

The governing imperative: when a value is ambiguous, unresolved, or below the confidence floor, reject the payload and preserve it for correction rather than coercing it into a canonical column. A false positive here — a mislabeled critical defect normalized down to minor — is exactly what the routing gate cannot recover from.

Production Integration and Platform Synchronization

Anchor link to "Production Integration and Platform Synchronization"

The normalized record is a fact other systems subscribe to, not a call the mapper blocks on. Emit each CanonicalDVIR to the broker as a single event keyed by a deterministic identifier so retries during cellular dropouts or broker redelivery do not create duplicate compliance records:

python
import hashlib


def idempotency_key(record: CanonicalDVIR) -> str:
    # Deterministic across retries: same inspection event => same key.
    material = f"{record.dvir_id}:{record.raw_payload_sha256}"
    return hashlib.sha256(material.encode()).hexdigest()

High-volume end-of-shift bursts are drained through the machinery described in Async Batching for High-Volume Ingestion, where polars normalizes a frame of records in one vectorized pass rather than row-by-row. Downstream, the Computerized Maintenance Management System (CMMS) consumes normalized defect records to open work orders, ELD and telematics platforms (Samsara, Geotab) consume asset_id and prepared_at to reconcile inspection state against the vehicle, and the audit store consumes every record. Chain audit entries cryptographically — each stored record includes the SHA-256 hash of the previous record for the same asset — so a deleted or reordered inspection is detectable during a DOT audit. Reconciliation jobs periodically diff the normalized ledger against the raw envelope store to catch any field that was dropped or coerced without a preserved original.

  • Schema validation — every emitted record satisfies the canonical Pydantic contract; a record that cannot validate is rejected, never partially admitted.
  • Deterministic execution — mapping is a pure function of the payload plus a versioned rule set; no clocks, network calls, or randomness inside normalize().
  • Externalized rules — alias tables and controlled vocabularies live in YAML that compliance officers revise without a code deploy; the mapper pins a rule_version.
  • Raw preservation — the original bytes and their SHA-256 hash are retained alongside every normalized record for audit reproduction.
  • Fail-closed defaults — ambiguous, unresolved, or low-confidence values are rejected and preserved for correction, never coerced.
  • Idempotent emission — every published record carries a deterministic key derived from stable fields.
  • Cryptographic chaining — hash-link consecutive records per asset for a tamper-evident audit package.
How do I map vendor field names that differ across DVIR sources?

Resolve every inbound key against an externalized alias table rather than hard-coding vendor keys in Python. Normalize the raw key (lowercase, replace spaces with underscores), match it against the canonical field’s alias set, and drop unrecognized keys into the preserved raw envelope instead of the canonical record. Because the alias table lives in YAML, compliance officers can add a new vendor’s labels without a code deploy.

What should happen when a driver-entered field cannot be normalized?

Reject the payload and route it to the dead-letter queue with a structured failure manifest — never coerce an unresolved value into a canonical column. Defaulting an unclassified severity token to minor, or admitting a guessed VIN below the OCR confidence floor, is the failure mode that lets an unsafe vehicle slip past the routing gate. Unresolvable fields escalate for human correction.

Why must normalization be a pure, deterministic function?

DOT audits require that any stored record be reproducible from its original bytes. If normalize() folded in clocks, network lookups, or randomness, the same payload could produce different canonical records on replay, breaking the § 396.11© certification chain. Pinning a versioned rule set and preserving the raw SHA-256 hash lets an auditor replay the exact transformation that produced a given record.

How are OCR artifacts corrected without admitting wrong data?

Cleaned tokens are matched against the controlled vocabulary with a fuzzy scorer and a hard score cutoff, so a near-miss like crtical resolves to critical while genuine noise falls through. Anything below the cutoff — or any structurally critical field such as the VIN or signature below the OCR confidence floor — is held for human verification rather than admitted, preserving the evidentiary value of the record.

Back to DVIR Ingestion & Digital/Paper Parsing Workflows.