Standardized DVIR JSON Schema Design
The migration from paper Driver Vehicle Inspection Reports to electronic submissions collapses the moment two mobile clients disagree on what a valid payload looks like. A standardized JSON schema is the data contract that removes that ambiguity: it defines every field name, type, enumeration, and cross-field rule that an inspection record must satisfy before it enters the compliance system. The regulatory hook is direct — 49 CFR § 396.11(a) requires that every driver report list any defect that would affect safe operation, and § 396.11©(2) requires certification that those defects were repaired before the vehicle returns to service. A schema that cannot represent “defect present, not yet repaired” as a machine-enforceable state cannot prove compliance to a DOT auditor. This page defines the canonical DVIR schema, its normalization rules, the Python validation workflow, and the mapping from parsed values to compliance actions. It is the structural foundation for the broader Core DVIR Architecture & FMCSA Compliance Mapping framework, and every payload flowing through JSON Schema Validation for Electronic DVIRs is checked against the contract defined here.
Problem Framing: One Schema, Many Producers
Anchor link to "Problem Framing: One Schema, Many Producers"A production DVIR pipeline ingests from heterogeneous producers: a first-party mobile app, a driver’s telematics tablet, a third-party ELD vendor export, and occasionally an OCR pass over a scanned paper form. Each producer has its own idea of how to spell a timestamp, how to encode a severity, and whether the odometer is an integer or a float. Without a single authoritative contract, the compliance system inherits every one of those inconsistencies and turns them into audit exposure.
The schema solves three concrete failure modes:
- Silent data loss. A free-text defect note (“brakes kinda soft”) cannot be routed automatically, so it is dropped or mis-triaged. An
enum-constrainedsystem_domainforces the producer to classify the defect at capture time. - Unprovable state. § 396.11©(2) demands proof of repair certification. If the schema has no
repair_certificationobject with a signer and timestamp, the fleet cannot demonstrate the vehicle was legally returned to service. - Non-deterministic routing. Downstream systems branch on defect severity. If severity is a free string, the same physical defect routes differently depending on which client submitted it.
The schema’s job is to make every one of these conditions representable, required where the regulation requires it, and rejectable at the gateway when it is missing.
Prerequisites and Environment Setup
Anchor link to "Prerequisites and Environment Setup"Target Python 3.10+ so the schema models can use the union (X | Y) type syntax and structural match statements used in the routing logic below. The reference stack for authoring and enforcing the contract:
jsonschema>=4.20— validates raw payloads against a JSON Schema document (Draft 2020-12).pydantic>=2.5— runtime type coercion, computed fields, and the source of truth for generating the JSON Schema.python-dateutil— tolerant ISO 8601 parsing during normalization, before strict re-serialization.orjson— fast, RFC 8259-correct JSON serialization for the immutability hash.
The schema does not stand alone. It consumes the controlled vocabulary defined in Defect Taxonomy Mapping for Heavy Trucks for the system_domain and severity_band enumerations, and it must satisfy the field-to-regulation crosswalk documented in the FMCSA DVIR Rule 396.11 Breakdown. Pin these dependencies in a lockfile and treat the schema document itself as a versioned artifact — bumping a $id version is a breaking change for every producer.
Data Schema and Normalization
Anchor link to "Data Schema and Normalization"The canonical DVIR record is composed of an inspection header, a driver-supplied defects array, and a repair_certification block that is conditionally required. Field types and enumerations are shared verbatim with the parent architecture so a defect object serialized here validates unchanged downstream.
| Field | Type | Enumeration / Constraint | Compliance tag |
|---|---|---|---|
inspection_id |
str |
UUIDv4 | idempotency key |
vehicle_vin |
str |
17-char, [A-HJ-NPR-Z0-9]{17} (no I/O/Q) |
asset identity |
unit_number |
str |
fleet-local, 1–32 chars | maintenance routing key |
driver_id |
str |
CDL or internal employee id | § 396.11(a) attribution |
inspection_type |
enum |
pre_trip, post_trip |
§ 396.11(a) vs § 396.13 |
inspected_at |
str |
ISO 8601 date-time, UTC (Z) |
chain-of-custody |
odometer |
int |
0–4,000,000 | audit-defensible reading |
defects |
array |
0…n defect objects | § 396.11(a) defect list |
no_defects |
bool |
true only when defects is empty |
§ 396.11(b) satisfactory report |
driver_signature |
str |
64-char lowercase hex (SHA-256) | § 396.11(a) driver certification |
repair_certification |
object |
required when any defect band is critical |
§ 396.11©(2) return-to-service |
Each element of the defects array uses the same object shape as the Defect Taxonomy Mapping for Heavy Trucks contract so records interoperate without translation:
| Defect field | Type | Enumeration / Constraint | Compliance tag |
|---|---|---|---|
raw_code |
str |
free-form, 1–128 chars | evidence — retained verbatim |
system_domain |
enum |
brakes, steering, lighting, tires, coupling, frame, emergency_equipment |
maps to § 396.11(a) inspection items |
component |
str |
canonical token, snake_case | maintenance routing key |
severity_band |
enum |
minor (0–34), major (35–69), critical (70–100) |
drives OOS decision |
severity_score |
int |
0–100 | audit-defensible numeric |
oos_trigger |
bool |
true when band == critical |
§ 396.11©(2) hold |
Normalization happens before validation, not after. Producers send tolerant input; the normalizer coerces it into the strict canonical form, and only then does the strict schema run. Reject anything the normalizer cannot make canonical rather than guessing.
from datetime import datetime, timezone
from dateutil import parser as dtparser
def normalize_header(raw: dict) -> dict:
"""Coerce tolerant producer input into the canonical schema form.
Runs BEFORE strict JSON Schema validation. Anything that cannot be
made canonical is left invalid so the gateway rejects it (49 CFR
§ 396.11(a) requires an attributable, time-stamped record).
"""
out = dict(raw)
# Force VIN uppercase; the pattern forbids I/O/Q but clients send them.
if vin := out.get("vehicle_vin"):
out["vehicle_vin"] = vin.strip().upper()
# Any offset-aware timestamp -> UTC 'Z'. Timezone drift across a
# multi-state route corrupts the inspection window audit.
if ts := out.get("inspected_at"):
parsed = dtparser.isoparse(ts)
if parsed.tzinfo is None:
raise ValueError("inspected_at missing timezone; cannot attribute")
out["inspected_at"] = parsed.astimezone(timezone.utc).isoformat(
timespec="seconds"
).replace("+00:00", "Z")
# Odometer floats are non-conforming; truncate is data loss, so reject.
odo = out.get("odometer")
if isinstance(odo, float) and not odo.is_integer():
raise ValueError("odometer must be an integer reading")
if odo is not None:
out["odometer"] = int(odo)
# A report with no defects must assert it explicitly (§ 396.11(b)).
out["no_defects"] = not out.get("defects")
return out
Core Workflow: Pydantic as the Single Source of Truth
Anchor link to "Core Workflow: Pydantic as the Single Source of Truth"Hand-maintaining a JSON Schema document and a parallel set of runtime models guarantees they drift. Define the contract once as Pydantic models and generate the JSON Schema from them, so the gateway validator and the application code can never disagree.
from enum import StrEnum
from pydantic import BaseModel, Field, field_validator, model_validator
class SystemDomain(StrEnum):
BRAKES = "brakes"
STEERING = "steering"
LIGHTING = "lighting"
TIRES = "tires"
COUPLING = "coupling"
FRAME = "frame"
EMERGENCY_EQUIPMENT = "emergency_equipment"
class SeverityBand(StrEnum):
MINOR = "minor" # 0–34
MAJOR = "major" # 35–69
CRITICAL = "critical" # 70–100
class Defect(BaseModel):
model_config = {"extra": "forbid"} # additionalProperties: false
raw_code: str = Field(min_length=1, max_length=128)
system_domain: SystemDomain
component: str = Field(pattern=r"^[a-z0-9_]+$")
severity_band: SeverityBand
severity_score: int = Field(ge=0, le=100)
oos_trigger: bool = False
@model_validator(mode="after")
def band_matches_score(self) -> "Defect":
# The band and score are two encodings of the same fact; a mismatch
# is a corrupt record, not a warning. Reject it.
bounds = {
SeverityBand.MINOR: range(0, 35),
SeverityBand.MAJOR: range(35, 70),
SeverityBand.CRITICAL: range(70, 101),
}
if self.severity_score not in bounds[self.severity_band]:
raise ValueError("severity_score outside declared band")
# oos_trigger is derived, never trusted from the wire.
object.__setattr__(
self, "oos_trigger", self.severity_band is SeverityBand.CRITICAL
)
return self
class RepairCertification(BaseModel):
model_config = {"extra": "forbid"}
mechanic_id: str = Field(min_length=1)
certified_at: str # ISO 8601, validated in the header validator
signature: str = Field(pattern=r"^[a-f0-9]{64}$") # SHA-256, lowercase
class DVIRReport(BaseModel):
model_config = {"extra": "forbid"}
inspection_id: str
vehicle_vin: str = Field(pattern=r"^[A-HJ-NPR-Z0-9]{17}$")
unit_number: str = Field(min_length=1, max_length=32)
driver_id: str = Field(min_length=1)
inspection_type: str = Field(pattern=r"^(pre_trip|post_trip)$")
inspected_at: str
odometer: int = Field(ge=0, le=4_000_000)
defects: list[Defect] = Field(default_factory=list)
no_defects: bool
driver_signature: str = Field(pattern=r"^[a-f0-9]{64}$")
repair_certification: RepairCertification | None = None
@field_validator("inspected_at")
@classmethod
def utc_iso(cls, v: str) -> str:
if not v.endswith("Z"):
raise ValueError("inspected_at must be UTC with 'Z' suffix")
return v
@model_validator(mode="after")
def enforce_return_to_service(self) -> "DVIRReport":
# § 396.11(c)(2): a vehicle with a critical (OOS) defect may not
# return to service without repair certification. The contract
# makes that legally required transition structurally mandatory.
has_critical = any(d.oos_trigger for d in self.defects)
if has_critical and self.repair_certification is None:
raise ValueError(
"critical defect present; repair_certification required "
"before return-to-service (49 CFR § 396.11(c)(2))"
)
if self.no_defects and self.defects:
raise ValueError("no_defects=true contradicts non-empty defects[]")
return self
Generating the wire-level JSON Schema document from these models keeps the gateway validator honest. Emit it as a build artifact and version it via $id:
import orjson
schema_doc = DVIRReport.model_json_schema()
schema_doc["$schema"] = "https://json-schema.org/draft/2020-12/schema"
schema_doc["$id"] = "https://fleet-compliance.example/schemas/dvir/v2.json"
with open("dvir.schema.json", "wb") as fh:
fh.write(orjson.dumps(schema_doc, option=orjson.OPT_INDENT_2))
The jsonschema runtime enforcement of this generated document — draft selection, iter_errors vs fail-fast, and structured error reporting — is covered in depth in JSON Schema Validation for Electronic DVIRs. The additionalProperties: false (extra="forbid") setting is deliberate: an unknown field is almost always a producer sending data the compliance system will silently ignore, and silent ignore is how audit gaps form.
Compliance Thresholding and Routing
Anchor link to "Compliance Thresholding and Routing"Once a payload validates, the severity_band on each defect maps deterministically to a compliance action. These thresholds are shared verbatim with the Severity Scoring Algorithms for DVIR Defects engine so a score computed there routes identically here.
| Severity band | Score | Compliance action | Regulatory basis |
|---|---|---|---|
critical |
70–100 | Trigger an OOS hold; block dispatch; require mechanic certification before return-to-service | 49 CFR § 396.11©(2), § 396.11(a)(3) |
major |
35–69 | Open a repair order inside the regulated repair window; certify repair before next dispatch | 49 CFR § 396.11©(2) |
minor |
0–34 | Route to scheduled maintenance queue; document, no dispatch hold | 49 CFR § 396.11(a) |
Routing acts on the validated model, never on the raw payload. Take the worst band present and branch:
from collections import Counter
def route(report: DVIRReport) -> str:
"""Map a validated DVIR to its compliance action. Deterministic:
the worst band present dictates the vehicle's disposition."""
bands = [d.severity_band for d in report.defects]
if SeverityBand.CRITICAL in bands:
# Imperative: trigger the OOS hold now, block dispatch.
return "oos_hold"
if SeverityBand.MAJOR in bands:
return "repair_order"
if bands: # only minor defects
return "scheduled_maintenance"
return "cleared_for_dispatch"
The severity-to-action state machine — how a unit moves from oos_hold through certified repair back to cleared_for_dispatch — is enforced by the Critical vs Non-Critical Routing Logic engine:
The invariant the schema guarantees: no path reaches cleared_for_dispatch from oos_hold without a repair_certification object, because DVIRReport.enforce_return_to_service rejects the return-to-service payload otherwise.
Production Integration and Platform Synchronization
Anchor link to "Production Integration and Platform Synchronization"A validated DVIR is only useful once it reaches the systems that act on it: the CMMS that opens the work order, the telematics platform that flags the unit, and the immutable ledger that satisfies the audit. Two properties make this safe under retries and at-least-once delivery.
Idempotency. The inspection_id UUID is the idempotency key end to end. Downstream upserts key on it so a redelivered payload updates rather than duplicates — a duplicated OOS hold is an operational incident, and a duplicated ledger entry corrupts the audit count.
Cryptographic chaining. Serialize the validated, canonical model deterministically and hash it. Store that digest alongside the record so any later mutation is detectable; chain each digest to the previous one to make the ledger tamper-evident.
import hashlib
import orjson
def content_hash(report: DVIRReport, prev_hash: str) -> str:
"""SHA-256 over the canonical serialization, chained to the prior
record. Deterministic key order is mandatory or the hash is unstable."""
canonical = orjson.dumps(
report.model_dump(mode="json"),
option=orjson.OPT_SORT_KEYS,
)
return hashlib.sha256(prev_hash.encode() + canonical).hexdigest()
Emit the compliance action as an event keyed on inspection_id; consumers dedupe on it. Records that arrive faster than the ingestion tier can process are handed to Async Batching for High-Volume Ingestion rather than dropped. When the producer is a mobile app, the wire contract and export semantics are aligned in Mobile App DVIR Export Integration, and inconsistent driver-entered values are reconciled by Automated Field Mapping & Data Normalization before they ever reach this schema.
Engineering Standards Checklist
Anchor link to "Engineering Standards Checklist"Before promoting the schema to production, confirm each of the following:
- Schema is generated, not hand-written. The JSON Schema document is emitted from the Pydantic models in CI so the two cannot drift; the artifact is committed and its
$idversion is bumped on any breaking change. additionalProperties: falseeverywhere. Every object rejects unknown fields; a producer sending unexpected keys fails loudly at the gateway.- Derived fields are never trusted from the wire.
oos_triggerandno_defectsare recomputed on ingest; a client assertingoos_trigger=falseon a critical defect is overruled. - Conditional requirements are enforced. A critical defect with no
repair_certificationis rejected, satisfying 49 CFR § 396.11©(2) structurally rather than by convention. - Timestamps are UTC and attributable.
inspected_atis normalized toZand offset-naive values are rejected, preserving chain-of-custody across state lines. - Every ingested record is hashed and chained. A SHA-256 digest over the canonical serialization is persisted with each record for tamper-evident, WORM-compatible retention.
- Rejections are structured. Validation failures return RFC 9457 Problem Details (which obsoletes RFC 7807) with the exact JSON path, so a mobile developer can fix client form logic without a support ticket.
Related
Anchor link to "Related"- JSON Schema Validation for Electronic DVIRs — runtime enforcement, draft selection, and error-path reporting for the contract defined here.
- Defect Taxonomy Mapping for Heavy Trucks — the controlled vocabulary behind the
system_domainandseverity_bandenumerations. - FMCSA DVIR Rule 396.11 Breakdown — the clause-by-clause crosswalk each schema field must satisfy.
- Severity Scoring Algorithms for DVIR Defects — how the
severity_scoreand band consumed here are computed. - Critical vs Non-Critical Routing Logic — the state machine that acts on the routing decision.