Skip to content
Core Architecture

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.

DVIR JSON schema validation data flow Heterogeneous producers — a first-party mobile app, a telematics tablet, a third-party ELD export and an OCR scan of a paper form — emit tolerant JSON that is coerced by the normalizer into the canonical form. The normalized payload passes through the schema gate, which runs JSON Schema Draft 2020-12 validation followed by Pydantic model coercion and cross-field rules. Accepted records are serialized deterministically, hashed with SHA-256 and appended to the write-once WORM ledger while emitting a telematics event keyed on the inspection identifier. Rejected records return an RFC 9457 Problem Details response carrying the exact failing JSON path back to the producer. Producers · tolerant JSON Mobile app Telematics tablet ELD vendor export OCR paper scan Normalizer VIN upper · ISO 8601 → UTC Z · int odometer canonical Schema gate Draft 2020-12 validate Pydantic coercion cross-field rules RFC 9457 reject back to producer · exact JSON path accepted Canonical serialize sorted keys · SHA-256 chained digest Telematics event keyed on inspection_id WORM audit ledger append-only · SHA-256 chain accepted / canonical path rejected · RFC 9457 return

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-constrained system_domain forces 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_certification object 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.

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.

python
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.

python
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:

python
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:

python
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:

DVIR unit disposition state machine A unit begins cleared for dispatch. A minor defect moves it to scheduled maintenance, a major defect to a repair order, and a critical defect with oos_trigger set to an out-of-service hold. Scheduled maintenance returns to cleared for dispatch once preventive maintenance completes, and a repair order returns once the repair is certified. The out-of-service hold stays held while no repair certification is present — dispatch is blocked — and returns to cleared for dispatch only when a repair_certification object is present, satisfying 49 CFR § 396.11(c)(2). cleared_for_dispatch unit may run scheduled_maintenance minor · 0–34 repair_order major · 35–69 oos_hold critical · 70–100 · dispatch blocked minor defect major defect critical (oos_trigger) PM completed repair certified no certification repair_certification present · § 396.11(c)(2)

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.

python
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.

Before promoting the schema to production, confirm each of the following:

  1. 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 $id version is bumped on any breaking change.
  2. additionalProperties: false everywhere. Every object rejects unknown fields; a producer sending unexpected keys fails loudly at the gateway.
  3. Derived fields are never trusted from the wire. oos_trigger and no_defects are recomputed on ingest; a client asserting oos_trigger=false on a critical defect is overruled.
  4. Conditional requirements are enforced. A critical defect with no repair_certification is rejected, satisfying 49 CFR § 396.11©(2) structurally rather than by convention.
  5. Timestamps are UTC and attributable. inspected_at is normalized to Z and offset-naive values are rejected, preserving chain-of-custody across state lines.
  6. 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.
  7. 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.

Back to Core DVIR Architecture & FMCSA Compliance Mapping.