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.
Prerequisites
Anchor link to "Prerequisites"- Python 3.10+ (the code below uses
match/caseand PEP 604X | Yunion syntax). jsonschema>=4.18— providesDraft202012Validator,iter_errors(), and format-assertion support.- The registered
DVIRRecordJSON 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.
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.
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.
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.
{
"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.
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
Verification and testing
Anchor link to "Verification and testing"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.
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.jsonschematreats1.0and1as equal under"type": "integer"unless you also constrain with"multipleOf": 1or 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_pathon nested arrays. When a defect list contains mixed object structures,err.absolute_pathis adequeof 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. Customformatvalidators 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.
Frequently Asked Questions
Anchor link to "Frequently Asked Questions"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.
Related
Anchor link to "Related"- Standardized DVIR JSON Schema Design — the canonical
DVIRRecordcontract this gate enforces. - Defect Taxonomy Mapping for Heavy Trucks — the controlled defect vocabulary the
enumconstraints validate against. - FMCSA DVIR Rule 396.11 Breakdown — the clause-by-clause obligations these validation gates protect.
- Compliance Boundary Enforcement in Cloud Workflows — how accepted payloads are chained and role-gated after validation.
- Severity Scoring Algorithms for DVIR Defects — where validated critical defects are graded and routed.
Back to the parent topic Standardized DVIR JSON Schema Design, part of Core DVIR Architecture & FMCSA Compliance Mapping.