How to Map DVIR Fields to FMCSA 396.11 Requirements
A mobile inspection payload is not a compliant record until every field it carries is bound to a specific obligation in 49 CFR § 396.11, and a single unmapped field — a missing repair certification, an ambiguous timestamp, an empty defect array read as “no data” instead of “no defects” — is the difference between a clean report and a Vehicle Maintenance BASIC violation surfacing at a roadside inspection. This page answers one focused question: given a raw JSON payload from a driver’s device, exactly which fields must you extract, what must each become, and which § 396.11 clause justifies rejecting or holding the record when a field is absent. Getting the mapping wrong does not fail loudly at ingestion; it fails months later during a DOT audit when the carrier cannot produce the signature chain § 396.11©(2) demands. This how-to sits under the clause-level FMCSA DVIR Rule 396.11 Breakdown reference and implements the field-level contract that breakdown specifies.
Prerequisites
Anchor link to "Prerequisites"Before implementing the mapping layer you need Python 3.10+ (for match statements and X | Y union syntax) and the following:
pydantic>=2.6— runtime validation and strict type coercion for the ingestion contract.python-dateutil>=2.9— timezone-aware ISO 8601 parsing for offline timestamp reconciliation.hashlib(stdlib) — SHA-256 hashing for signature tamper-evidence and idempotency keys.- A low-latency store (Redis or equivalent) for the idempotency key cache described below.
Two upstream contracts feed this page. The canonical record shape you coerce every payload into is defined by the Standardized DVIR JSON Schema Design, and the defect vocabulary each defect_code must resolve against comes from Defect Taxonomy Mapping for Heavy Trucks. Field-level cleanup of driver-entered strings (odometer formatting, unit-number casing) is handled upstream by Automated Field Mapping & Data Normalization, so this layer receives clean strings and concerns itself only with binding them to regulation.
The § 396.11 Field Contract
Anchor link to "The § 396.11 Field Contract"Each mapping decision traces to an exact clause. Keep this table as the authoritative source; the code below implements it field for field.
| Raw payload field | Maps to | Type / constraint | CFR clause | Action if absent or invalid |
|---|---|---|---|---|
driver_id |
driver_cdl_hash |
str (SHA-256) |
§ 396.11(a) | Reject the payload; return for authentication |
vin / unit |
vehicle_vin, unit_number |
17-char VIN, str |
§ 396.11(a) | Reject; an unresolved VIN cannot enter the pipeline |
submitted_at |
inspection_timestamp_utc |
UTC datetime, offset ±12h |
§ 396.11(a) | Reject; ambiguous local time is not audit-defensible |
defects |
defects[] |
list[Defect] |
§ 396.11(a)(3) | Coerce empty list to no_defects attestation |
defects[].code |
defect_code |
controlled vocabulary | § 396.11(a)(3) | Reject the defect; unknown code cannot be scored |
defects[].severity |
severity_band |
0–34 / 35–69 / 70–100 |
§ 396.11(a)(3) | Route by band; 70–100 triggers an OOS hold |
signature |
certification_signature |
base64 blob, SHA-256 hash | § 396.11(a) | Reject; unsigned report is not a report |
repair |
repair_certification |
RepairCertification | None |
§ 396.11©(2) | Block return-to-service transition |
review_sig |
review_signature |
Signature | None |
§ 396.11©(2)(iii) | Block finalization of the prior report |
The single most-missed row is the empty defects array. A driver submitting no defects is making an affirmative attestation under § 396.11(a) that the vehicle is safe — not sending null data. Coerce an empty list into an explicit certification_status: "no_defects" flag so the record reads as a signed clean report, not an incomplete one.
Step-by-Step Implementation
Anchor link to "Step-by-Step Implementation"Step 1 — Resolve identity and normalize the timestamp
Anchor link to "Step 1 — Resolve identity and normalize the timestamp"Every payload must resolve to a hashed CDL and a fleet-registered VIN, and its timestamp must be normalized to UTC before persistence. § 396.11(a) requires an unambiguous per-driver, per-vehicle, per-day record, and auditors reject records with unnormalized local offsets.
import hashlib
from datetime import datetime, timezone
from dateutil import parser as dtparser
def hash_cdl(cdl_number: str) -> str:
"""Bind driver identity per 49 CFR § 396.11(a) without storing the raw CDL."""
return hashlib.sha256(cdl_number.strip().upper().encode()).hexdigest()
def normalize_timestamp(raw: str) -> datetime:
"""Parse an ISO 8601 string and convert to UTC. Reject impossible offsets."""
ts = dtparser.isoparse(raw) # preserves the incoming tz offset
if ts.tzinfo is None:
# § 396.11(a): a naive local time is not audit-defensible.
raise ValueError("timestamp missing timezone offset")
offset_hours = ts.utcoffset().total_seconds() / 3600
if not -12 <= offset_hours <= 12:
raise ValueError(f"timezone_offset {offset_hours}h outside ±12h range")
return ts.astimezone(timezone.utc)
Step 2 — Coerce the defect array
Anchor link to "Step 2 — Coerce the defect array"Map each raw defect to the canonical triple, and turn an empty array into an explicit attestation rather than treating it as missing.
from enum import Enum
from pydantic import BaseModel, Field
class SeverityBand(str, Enum):
MINOR = "minor" # 0-34 -> scheduled maintenance
MAJOR = "major" # 35-69 -> regulated repair window
CRITICAL = "critical" # 70-100 -> immediate OOS hold, § 396.11(c)(2)
def band_for(score: int) -> SeverityBand:
if score >= 70:
return SeverityBand.CRITICAL
if score >= 35:
return SeverityBand.MAJOR
return SeverityBand.MINOR
class Defect(BaseModel):
defect_code: str = Field(min_length=1) # § 396.11(a)(3)
component_classification: str
severity_score: int = Field(ge=0, le=100)
@property
def severity_band(self) -> SeverityBand:
return band_for(self.severity_score)
def map_defects(raw_defects: list[dict]) -> tuple[list[Defect], str]:
"""Return (defects, certification_status). Empty -> affirmative no_defects."""
if not raw_defects:
# § 396.11(a): "no defects" is a signed attestation, not null data.
return [], "no_defects"
return [Defect(**d) for d in raw_defects], "defects_listed"
Step 3 — Validate and hash the signature
Anchor link to "Step 3 — Validate and hash the signature"The driver certification signature must arrive as a base64 blob paired with a capture method, be size- and MIME-checked, and be SHA-256 hashed for tamper-evidence. A frequent failure is the mapping layer expecting base64 but receiving a raw CDN URL — normalize that before hashing.
import base64
MAX_SIGNATURE_BYTES = 2 * 1024 * 1024 # 2 MB ceiling
ALLOWED_METHODS = {"touchscreen", "stylus", "biometric"}
def map_signature(sig: dict) -> dict:
"""Validate, size-check and hash the § 396.11(a) driver attestation."""
method = sig.get("capture_method")
if method not in ALLOWED_METHODS:
raise ValueError(f"unknown signature capture method: {method}")
blob = sig.get("data", "")
if blob.startswith(("http://", "https://")):
# Fetch-and-re-encode belongs in a middleware step; never hash a URL.
raise ValueError("signature is a URL, not a base64 payload")
decoded = base64.b64decode(blob, validate=True)
if len(decoded) > MAX_SIGNATURE_BYTES:
raise ValueError("signature exceeds 2 MB ceiling")
return {
"capture_method": method,
"signature_sha256": hashlib.sha256(decoded).hexdigest(),
}
Step 4 — Gate the repair-certification state transition
Anchor link to "Step 4 — Gate the repair-certification state transition"Carrier acknowledgment is a state machine, not a free-text field. § 396.11©(2) requires the carrier to certify a safety-affecting defect was repaired (or repair was unnecessary) before dispatch, and § 396.11©(2)(iii) requires the next driver to review and sign. Model the transitions explicitly and block dispatch until the certification chain is complete. The band routing here must match the Critical vs Non-Critical Routing Logic engine downstream.
class DVIRState(str, Enum):
PENDING_REVIEW = "pending_review"
DEFECT_CERTIFIED = "defect_certified" # driver signed, repair outstanding
REPAIR_COMPLETED = "repair_completed" # § 396.11(c)(2) certification present
RELEASED = "released" # next-driver review signed
def can_dispatch(state: DVIRState, has_critical_defect: bool) -> bool:
"""A unit with an unremediated critical defect must not be released."""
if not has_critical_defect:
return state in {DVIRState.PENDING_REVIEW, DVIRState.RELEASED}
# § 396.11(c)(2): repair certified AND § 396.11(c)(2)(iii) review signed.
return state is DVIRState.RELEASED
Any dispatch attempt against a unit in DEFECT_CERTIFIED with no repair_completed transition must return a 423 Locked (or 409 Conflict) with a machine-readable violation code, and every transition must append to an immutable, idempotent audit table. Derive the idempotency key from sha256(vehicle_vin + inspection_timestamp_utc + driver_cdl_hash) and cache it with a 24-hour TTL so a duplicate submission from a flaky cellular link cannot double-book a record.
Verification and Testing
Anchor link to "Verification and Testing"Prove the mapping is correct with contract tests that assert each § 396.11 obligation, not just that the code runs. The empty-defect coercion and the dispatch gate are the two behaviors most worth pinning.
import pytest
def test_empty_defects_becomes_attestation():
defects, status = map_defects([])
assert defects == []
assert status == "no_defects" # § 396.11(a) affirmative certification
def test_critical_defect_blocks_dispatch_until_released():
# A certified-but-unrepaired critical defect must stay held.
assert can_dispatch(DVIRState.DEFECT_CERTIFIED, has_critical_defect=True) is False
assert can_dispatch(DVIRState.RELEASED, has_critical_defect=True) is True
def test_naive_timestamp_is_rejected():
with pytest.raises(ValueError):
normalize_timestamp("2026-07-01T08:00:00") # no offset -> reject
def test_idempotency_key_is_stable():
def key(vin, ts, cdl):
return hashlib.sha256(f"{vin}{ts}{cdl}".encode()).hexdigest()
a = key("1FUJGLDR1CLBP8834", "2026-07-01T12:00:00Z", "abc")
b = key("1FUJGLDR1CLBP8834", "2026-07-01T12:00:00Z", "abc")
assert a == b # duplicate submission resolves to one record
Run these under pytest in CI and fail the build on any regression — the dispatch-gate assertion in particular is the guard that keeps a non-compliant vehicle off the road.
Common Failure Modes and Gotchas
Anchor link to "Common Failure Modes and Gotchas"- Timezone drift between device and server. Mobile clocks skew, and a device reporting a future or wildly past offset will pass naive parsing. Reject offsets outside ±12h and reconcile offline-queued reports against the server’s receipt time, not the device clock — a report submitted hours after the inspection must still normalize to the inspection instant.
- Signature is a CDN URL, not base64. When a mobile app uploads the signature image separately and sends only a link, the hashing step silently hashes the URL string. Detect the
http(s)://prefix, fetch and re-encode in middleware, and only then hash — otherwise every audit hash is meaningless. - Empty array read as missing data. An
if defects:guard that skips persistence when the list is empty destroys the § 396.11(a) clean-report attestation. Always branch on presence-of-key, and coerce the empty list tono_defectsexplicitly. - Duplicate submissions on poor connectivity. Without the composite idempotency key, a retried POST creates two records for one inspection, and an auditor sees a conflicting pair. Cache the key before writing, and treat a cache hit as a success no-op returning the original record id.
Related
Anchor link to "Related"- FMCSA DVIR Rule 396.11 Breakdown — the clause-by-clause parent reference this field contract implements.
- Standardized DVIR JSON Schema Design — the canonical record shape every payload is coerced into.
- Defect Taxonomy Mapping for Heavy Trucks — the controlled vocabulary each
defect_coderesolves against. - Automated Field Mapping & Data Normalization — upstream cleanup of driver-entered strings before mapping.
- Critical vs Non-Critical Routing Logic — the engine that acts on the severity bands this mapping emits.
Part of the FMCSA DVIR Rule 396.11 Breakdown guide. Back to Core DVIR Architecture & FMCSA Compliance Mapping.