Skip to content
Core Architecture

JSON Schema Validation for Electronic DVIRs

Every electronic Driver Vehicle Inspection Report (DVIR) that reaches your ingestion layer must clear a deterministic validation gate before any compliance routing begins. If a malformed payload slips past that gate — a missing coupling-verification timestamp, a floating-point odometer reading, an unrecognized defect code — it corrupts the audit trail that 49 CFR § 396.11(a) requires the carrier to preserve, and a DOT auditor will treat the gap as a recordkeeping violation. This page answers one focused question: how do you build a JSON Schema validation layer that rejects non-conforming electronic DVIRs at the API boundary, returns a field-precise error the mobile client can act on, and never silently drops a recoverable submission?

The validation layer is the entry point of the Standardized DVIR JSON Schema Design — it enforces the canonical DVIRRecord contract that the rest of the Core DVIR Architecture & FMCSA Compliance Mapping pipeline depends on. Get it wrong and the downstream defect taxonomy, severity scoring, and retention stages all inherit corrupted data.

  • Python 3.10+ (the code below uses match/case and PEP 604 X | Y union syntax).
  • jsonschema>=4.18 — provides Draft202012Validator, iter_errors(), and format-assertion support.
  • The registered DVIRRecord JSON Schema from the parent Standardized DVIR JSON Schema Design, including the controlled defect vocabulary defined in Defect Taxonomy Mapping for Heavy Trucks.
  • A quarantine queue (Redis list, SQS, or a database table) for recoverable anomalies, distinct from the hard-reject path.
The DVIR validation gate: one payload in, three deterministic outcomes out A mobile DVIR payload enters an API gateway and reaches the cached DVIRRecord validator, which was compiled once at import from a draft-pinned schema. The validator runs the JSON Schema draft 2020-12 check and the triage function classifies the result. A clean payload with zero errors is accepted with HTTP 200 and routed into the compliance pipeline. A payload whose first error is a fatal keyword — type, required, or additionalProperties — is hard rejected with HTTP 400. A payload with only recoverable anomalies such as enum drift or a hex-case mismatch is sent to the quarantine queue for reconciliation so the driver's attestation is never dropped. ingress validation gate triage() outcome mobile DVIR payload JSON body get_validator() compiled once · cached $schema pinned 2020-12 iter_errors(payload) errors? classify none fatal keyword recoverable accept · 200 → compliance pipeline zero violations reject · 400 field-precise error type · required · additionalProperties quarantine held for reconciliation enum drift · hex-case mismatch

Step 1: Pin the draft and compile the validator once

Anchor link to "Step 1: Pin the draft and compile the validator once"

Draft 07 and Draft 2020-12 are not interoperable. Draft 2020-12 replaces definitions with $defs, uses prefixItems instead of array-form items for tuple validation, and adds unevaluatedProperties. Choose the draft that matches the vocabulary already in use across your services and declare it explicitly with the $schema keyword so the library never falls back silently. Compile the validator a single time at import and reuse it — recompiling per request is the most common latency regression in high-throughput ingestion.

python
import json
from functools import cache
from pathlib import Path

from jsonschema import Draft202012Validator
from jsonschema.protocols import Validator

SCHEMA_PATH = Path(__file__).parent / "dvir_record.schema.json"


@cache
def get_validator() -> Validator:
    """Compile the DVIRRecord validator once; reuse across all requests."""
    schema = json.loads(SCHEMA_PATH.read_text(encoding="utf-8"))
    # Fail fast on a schema authoring mistake before serving traffic.
    Draft202012Validator.check_schema(schema)
    # format_checker enables assertion of "date-time", "uri", etc.
    return Draft202012Validator(
        schema, format_checker=Draft202012Validator.FORMAT_CHECKER
    )

Step 2: Choose the execution mode — collect vs. fail-fast

Anchor link to "Step 2: Choose the execution mode — collect vs. fail-fast"

Run two modes deliberately. In staging and regression testing, iterate iter_errors() to surface every structural violation in one pass, so a schema change never hides a second defect behind the first. In production ingestion, use is_valid() / validate() for fail-fast behavior to keep per-request latency low. The imperative rule at the boundary: on any error, reject the payload with a 400 and a structured body; do not accept-and-repair.

python
from dataclasses import dataclass


@dataclass(frozen=True, slots=True)
class FieldError:
    field: str        # dot-notation path, e.g. "defects.0.severity_code"
    message: str
    validator: str    # which keyword failed: "required", "enum", "pattern"...


def collect_errors(payload: dict) -> list[FieldError]:
    """Staging/regression mode: return ALL violations, sorted by path."""
    validator = get_validator()
    errors = [
        FieldError(
            field=".".join(str(p) for p in err.absolute_path) or "(root)",
            message=err.message,
            validator=str(err.validator),
        )
        for err in validator.iter_errors(payload)
    ]
    return sorted(errors, key=lambda e: e.field)

Step 3: Enforce cross-field constraints the top-level schema misses

Anchor link to "Step 3: Enforce cross-field constraints the top-level schema misses"

Electronic inspection forms deviate from the expected contract in predictable ways, and naive top-level validation passes them. Encode the conditional rules directly in the schema with if/then and dependentRequired so the validator — not scattered Python — owns the invariant. Two constraints that § 396.11 makes non-negotiable: a tractor-trailer configuration must carry a trailer identification block and a coupling-verification timestamp, and any defect marked repaired must carry a mechanic certification signature.

json
{
  "allOf": [
    {
      "if": { "properties": { "config": { "const": "tractor_trailer" } } },
      "then": {
        "required": ["trailer_id", "coupling_verified_at"],
        "properties": {
          "coupling_verified_at": { "type": "string", "format": "date-time" }
        }
      }
    }
  ],
  "$defs": {
    "defect": {
      "type": "object",
      "properties": {
        "status": { "enum": ["open", "repaired", "deferred"] },
        "mechanic_signature": {
          "type": "string",
          "pattern": "^[0-9a-f]{64}$"
        }
      },
      "dependentRequired": { "status": [] },
      "if": { "properties": { "status": { "const": "repaired" } } },
      "then": { "required": ["mechanic_signature"] }
    }
  }
}

Step 4: Route recoverable anomalies to quarantine, reject fatal ones

Anchor link to "Step 4: Route recoverable anomalies to quarantine, reject fatal ones"

A structurally fatal payload (wrong type on a required identifier, empty defects[] where a safety-critical anomaly was reported) must be rejected outright. A recoverable anomaly — an enum value that drifted from a third-party telematics provider, or an uppercase hex signature against a lowercase-only pattern — should be quarantined for reconciliation rather than dropped, so no attestation is lost. Translate the JSON Pointer path into a human-readable field label before it ever reaches a dispatcher.

python
FATAL_KEYWORDS = {"type", "required", "additionalProperties"}


def triage(payload: dict) -> tuple[str, list[FieldError]]:
    """Return ('accept' | 'quarantine' | 'reject', errors)."""
    errors = collect_errors(payload)
    if not errors:
        return "accept", []
    # Any fatal structural error => hard reject at the gateway (400).
    if any(e.validator in FATAL_KEYWORDS for e in errors):
        return "reject", errors
    # Only recoverable violations (enum drift, pattern case) => quarantine.
    return "quarantine", errors

Prove correctness with schema contract tests rather than by inspecting live traffic. Assert three things: a known-good fixture validates clean, each crafted-bad fixture fails on the exact expected keyword and path, and the validator compiles (check_schema) so an authoring mistake fails CI, not production.

python
import pytest


def test_golden_payload_is_clean(golden_dvir):
    assert triage(golden_dvir) == ("accept", [])


@pytest.mark.parametrize(
    "mutation, exp_field, exp_validator",
    [
        ({"odometer": 105432.0}, "odometer", "type"),        # float where int required
        ({"config": "tractor_trailer", "trailer_id": None},
         "coupling_verified_at", "required"),                # missing § 396.11 coupling proof
        ({"defects": [{"status": "repaired"}]},
         "defects.0.mechanic_signature", "required"),        # unsigned repair
    ],
)
def test_expected_rejection(golden_dvir, mutation, exp_field, exp_validator):
    payload = golden_dvir | mutation
    verdict, errors = triage(payload)
    assert verdict in {"reject", "quarantine"}
    assert any(
        e.field == exp_field and e.validator == exp_validator for e in errors
    )

Run the collect-mode path in CI so a schema edit that would let a second violation slip through is caught the moment it lands.

Common failure modes and gotchas

Anchor link to "Common failure modes and gotchas"
  • Odometer type coercion. Devices transmit floating-point precision (105432.0) where § 396 recordkeeping expects an integer. jsonschema treats 1.0 and 1 as equal under "type": "integer" unless you also constrain with "multipleOf": 1 or reject at the gateway. Return a field-level error so the mobile client re-encodes rather than accepting a lossy value.
  • Enum drift from third-party telematics. A legacy defect severity code outside your controlled vocabulary must alert and quarantine — never silently drop the record. Version the enum list alongside the schema so you can distinguish a genuine bad value from a value your schema simply hasn’t adopted yet.
  • Hex case mismatch on signatures. A mobile SDK that emits an uppercase SHA-256 hash fails a lowercase-only ^[0-9a-f]{64}$ pattern. Decide the canonical case once, normalize on ingest, and document it — do not loosen the pattern to [0-9a-fA-F], which would let two encodings of the same signature into the audit chain.
  • absolute_path on nested arrays. When a defect list contains mixed object structures, err.absolute_path is a deque of keys and indices; join it to dot notation (defects.2.severity_code) before reporting, or a dispatcher cannot tell the driver which row to fix. Custom format validators for ISO 8601 timestamps must run before the record is committed to the compliance ledger, since a string that merely looks date-shaped passes "type": "string" unchecked.

Payloads that clear this gate carry the structural integrity the downstream stages assume; validated critical defects then flow into severity handling described in Severity Scoring Algorithms for DVIR Defects, and every accepted transition is chained per Compliance Boundary Enforcement in Cloud Workflows. For the authoritative recordkeeping obligation these checks protect, see 49 CFR § 396.11 and the Python jsonschema documentation.

Should I use Draft 07 or Draft 2020-12 for DVIR schemas?

Use whichever draft matches the vocabulary already used across your services, and declare it explicitly with the $schema keyword so the library never falls back silently. Draft 2020-12 is preferable for new pipelines because $defs, prefixItems, and unevaluatedProperties express DVIR cross-field constraints more precisely, but do not mix keywords from both drafts in one schema — they are not interoperable.

Why does my integer odometer field accept a floating-point value?

Under JSON Schema, 105432.0 satisfies "type": "integer" because it has no fractional part. Add "multipleOf": 1 if you want the schema itself to reject non-whole numbers, or reject the float at the API gateway and return a field-level error so the mobile client re-encodes the value as an integer before resubmitting.

What is the difference between rejecting and quarantining a payload?

Reject fatal structural violations — a wrong type on a required identifier, a missing required field, an empty defect array where a safety-critical anomaly was reported — outright with a 400 at the gateway. Quarantine only recoverable anomalies, such as an enum value that drifted from a telematics provider or a hex-case mismatch, so the driver’s attestation is preserved for reconciliation instead of being silently dropped.

How do I turn a jsonschema error into a readable field name?

Join the absolute_path deque of the ValidationError into a dot-notation string, for example defects.2.severity_code, falling back to (root) when the path is empty. That label lets a dispatcher tell the driver exactly which row and field to correct, rather than surfacing a raw JSON Pointer.

Back to the parent topic Standardized DVIR JSON Schema Design, part of Core DVIR Architecture & FMCSA Compliance Mapping.