Skip to content

Schema Validation Rules for FSMA 204 Supplier Data Ingestion

Regulatory compliance under FSMA 204 is not achieved at the point of recall; it is engineered at the boundary of data ingestion. When supplier payloads enter a traceability system, they must be validated against precise Key Data Element (KDE) schemas before they are committed to the lot graph. A single malformed timestamp, missing Traceability Lot Code, or non-standardized unit of measure fractures downstream Critical Tracking Event (CTE) lineage and invalidates automated recall workflows. Within the broader Supplier Data Ingestion & Sync Automation pipeline, this page owns one narrow but load-bearing responsibility: the strict, non-negotiable gate that every normalized record must pass before it reaches the traceability ledger.

The triggering requirement is concrete. FSMA 204 Subpart S obligates a regulated facility to produce sortable, electronic traceability records within 24 hours of an FDA request during an outbreak investigation. That guarantee is only credible if every persisted CTE carries a complete, well-typed KDE set. Schema validation is the control that makes the guarantee true: it converts fragmented, vendor-specific input into a uniform contract, rejects records that would silently corrupt the lot chain, and preserves the evidence an auditor needs to understand why a record was rejected. This page defines the validation architecture, the KDE data contract, a runnable pydantic v2 implementation with tenacity-guarded persistence, the quarantine strategy that keeps rejected records audit-visible, and the operational configuration to run the gate in production.

KDE Boundary Validation Architecture

Validation must execute immediately after payload normalization and strictly before database persistence. Regardless of whether the pipeline processes flat files, EDI 856/810 transactions, or RESTful JSON payloads, the transport layer should first emit a canonical dictionary. That separation of concerns keeps parsing logic isolated from compliance enforcement — a pattern reinforced by the CSV/EDI Parser Setup, which normalizes heterogeneous supplier files before this gate ever runs. Whether a record arrives as a flat-file row or a REST-polled object, it converges on the same schema check, so a CSV record and an API record become indistinguishable once they reach the ledger.

In high-throughput environments, the validation window must align with the ingestion cadence. If the system relies on scheduled fetches or webhook-driven streams, the API Polling Strategies should define explicit batch boundaries that enable synchronous schema evaluation without introducing upstream latency. Crucially, validation failures must never halt the entire pipeline. Invalid records are quarantined, logged, and routed for remediation while compliant payloads proceed to the ledger. This fail-forward design preserves availability while maintaining strict regulatory boundaries — one malformed record never aborts a supplier batch.

The gate is a three-stage funnel. Stage one checks structural completeness: are all required KDEs present for this CTE type? Stage two checks type and format: is the timestamp ISO 8601 with an explicit offset, is the quantity a positive number, is the unit of measure inside the controlled vocabulary? Stage three checks business rules: does the record preserve CTE continuity — for example, does a Shipping event carry a destination location, and does an event timestamp precede system-ingestion time? A record that clears all three stages is committed; a record that fails any stage is quarantined with its precise failure path intact.

Figure — KDE boundary validation gates:

Three-stage KDE validation funnel A normalized payload enters a three-stage funnel. Stage one checks that all required Key Data Elements are present; stage two checks types and formats, including an ISO 8601 timestamp; stage three checks business rules such as the unit-of-measure enum and CTE continuity. A record that passes every stage is committed to the traceability ledger. A record that fails any stage drops into a shared quarantine store, is logged with its payload, error path, and hash, and is routed for remediation. yes yes yes no no no Normalized payload All required KDEs present? Types & formats valid? ISO 8601 Business rules? UOM · continuity Commit to trace ledger Quarantine + log payload · error path · hash Route for remediation

Precise KDE Mapping Requirements

FSMA 204 mandates specific KDEs for each CTE type. The validation schema must enforce strict typing, format constraints, and business rules across the core elements below. This is the validation-scoped view of the complete field catalog documented in the KDE Field Mapping Guide, which covers every transport and CTE across the compliance model.

Canonical KDE Type Validation rule Regulatory Source (21 CFR Part 1, Subpart S)
traceability_lot_code str Alphanumeric (hyphen/underscore allowed), non-empty, ≤ 50 chars § 1.1320 (Traceability Lot Code assignment)
product_description str Non-empty, ≥ 2 chars; mapped to internal SKU/GTIN crosswalk § 1.1340(a) (shipping KDEs)
quantity Decimal Strictly > 0 § 1.1340(a) (quantity and unit of measure)
unit_of_measure enum One of kg, lb, case, pallet, ea, liter, gallon § 1.1340(a) (unit of measure)
location_id str GLN, FDA Facility Registration Number, or internal registry key § 1.1340 / § 1.1345 (ship-from / receive-to location)
event_timestamp datetime ISO 8601, timezone-aware, coerced to UTC, never in the future § 1.1340 / § 1.1345 (date/time of the CTE)
cte_type enum Closed set of recognized CTEs § 1.1315 (Critical Tracking Event definitions)

Optional fields such as reference_document_id or carrier_scac should pass through without blocking ingestion, provided they conform to expected data types. The engine must, however, reject partial KDE sets that break CTE continuity — for example, a Shipping event missing a destination location_id, or a Receiving event lacking a traceability_lot_code. Optional KDEs should default to an explicit null rather than an empty string, so downstream recall queries can distinguish “not applicable” from “missing and required.” Malformed records are never silently coerced; they are quarantined with full context for reconciliation.

Production Validation Engine

Implementing this in Python requires declarative schema enforcement, resilient error handling, and audit-ready logging. The model below uses pydantic v2 for validation and tenacity to guard the two I/O-bound handoffs — the durable quarantine write and the ledger publish — that can fail transiently. The validation itself is deterministic and is never retried; only the transport-class writes around it are. Structured logging captures the payload fingerprint, the CTE type, and the precise failure path for every decision.

from __future__ import annotations

import hashlib
import json
import logging
from datetime import datetime, timezone
from decimal import Decimal
from enum import Enum

from pydantic import BaseModel, ConfigDict, Field, ValidationError, field_validator
from tenacity import retry, retry_if_exception_type, stop_after_attempt, wait_exponential

# Structured audit logger; in production attach a JSON formatter and ship to SIEM.
audit_logger = logging.getLogger("fsma204.validation")
audit_logger.setLevel(logging.INFO)

UOM_ENUM = {"kg", "lb", "case", "pallet", "ea", "liter", "gallon"}


class CTEType(str, Enum):
    HARVESTING = "Harvesting"
    COOLING = "Cooling"
    INITIAL_PACKING = "Initial Packing"
    SHIPPING = "Shipping"
    RECEIVING = "Receiving"
    TRANSFORMATION = "Transformation"


class TransientWriteError(Exception):
    """Raised when a durable store rejects a write for a retryable reason."""


class KDEPayload(BaseModel):
    # strict=True blocks silent coercion; extra="allow" preserves optional KDEs.
    model_config = ConfigDict(strict=True, extra="allow")

    traceability_lot_code: str = Field(..., min_length=3, max_length=50)
    product_description: str = Field(..., min_length=2)
    quantity: Decimal = Field(..., gt=0)
    unit_of_measure: str
    location_id: str = Field(..., min_length=5)
    event_timestamp: str
    cte_type: CTEType

    @field_validator("traceability_lot_code")
    @classmethod
    def validate_lot_code(cls, v: str) -> str:
        if not v.replace("-", "").replace("_", "").isalnum():
            raise ValueError("Lot code must be alphanumeric (hyphen/underscore allowed)")
        return v

    @field_validator("unit_of_measure")
    @classmethod
    def validate_uom(cls, v: str) -> str:
        if v not in UOM_ENUM:
            raise ValueError(f"Unit of measure must be one of {sorted(UOM_ENUM)}")
        return v

    @field_validator("event_timestamp")
    @classmethod
    def validate_timestamp(cls, v: str) -> str:
        # Enforce ISO 8601 with an explicit offset, coerce to UTC, reject the future.
        try:
            dt = datetime.fromisoformat(v.replace("Z", "+00:00"))
        except ValueError as exc:
            raise ValueError("Timestamp must be valid ISO 8601") from exc
        if dt.tzinfo is None:
            raise ValueError("Timestamp must include a timezone offset")
        if dt.astimezone(timezone.utc) > datetime.now(timezone.utc):
            raise ValueError("Event timestamp cannot be in the future")
        return v


def _fingerprint(record: dict[str, object]) -> str:
    raw = json.dumps(record, sort_keys=True, separators=(",", ":"), default=str)
    return hashlib.sha256(raw.encode("utf-8")).hexdigest()[:12]


@retry(
    retry=retry_if_exception_type(TransientWriteError),
    stop=stop_after_attempt(3),
    wait=wait_exponential(multiplier=0.5, max=8),
    reraise=True,
)
def persist_quarantine(document: dict[str, object]) -> None:
    """Write one quarantine artifact to a durable, versioned store.

    Deterministic validation never enters this function; only the transient
    write path is retried, so a flaky object-store call does not lose evidence.
    """
    # store.put(document["payload_hash"], document)  # replace with real client
    audit_logger.warning(
        "KDE_REJECTED | hash=%s | errors=%s",
        document["payload_hash"],
        document["validation_errors"],
    )


def validate_supplier_batch(
    raw_records: list[dict[str, object]],
) -> tuple[list[dict[str, object]], list[dict[str, object]]]:
    valid_records: list[dict[str, object]] = []
    invalid_records: list[dict[str, object]] = []

    for idx, record in enumerate(raw_records):
        payload_hash = _fingerprint(record)
        try:
            validated = KDEPayload(**record)
        except ValidationError as exc:
            error_summary = [
                {"loc": err["loc"], "msg": err["msg"]} for err in exc.errors()
            ]
            document = {
                "original_payload": record,
                "validation_errors": error_summary,
                "payload_hash": payload_hash,
                "quarantined_at": datetime.now(timezone.utc).isoformat(),
            }
            persist_quarantine(document)  # tenacity-guarded durable write
            invalid_records.append(document)
            continue

        valid_records.append(validated.model_dump(mode="json"))
        audit_logger.info(
            "KDE_VALID | record_idx=%d | hash=%s | cte_type=%s",
            idx,
            payload_hash,
            validated.cte_type.value,
        )

    return valid_records, invalid_records

The model enforces the full contract declaratively: strict=True blocks pydantic’s implicit coercion so a string "12" is never silently accepted where a Decimal quantity is required, the controlled unit_of_measure enum rejects any unit outside the approved vocabulary, and the timestamp validator guarantees timezone awareness before coercing to UTC. Because validate_supplier_batch catches ValidationError per record and continues, one bad row is isolated without aborting the batch — the partial-commit contract that keeps ingestion available under real supplier data.

Error Handling and Quarantine Strategy

The engine draws a hard line between two failure classes and handles each differently. Validation faults — a lot code with illegal characters, a negative quantity, a timezone-naive timestamp, a unit outside the enum — are deterministic. Retrying them changes nothing, so they are routed straight to quarantine with zero validation retries. Transport faults — a durable-store write that times out, an object-store 503, a transient network partition on the quarantine or ledger handoff — are non-deterministic and often succeed on a second attempt, so persist_quarantine wraps only those in a tenacity retry with exponential backoff. Conflating the two either wastes cycles retrying a permanently malformed record or, worse, silently loses evidence when a transient write fails.

Quarantine is not merely error handling; it is a regulatory artifact. Each quarantined record is written as a self-contained JSON document carrying the original raw payload, the precise pydantic error path, the SHA-256 fingerprint, and a UTC timestamp. This preserves the full provenance an auditor needs to answer the only question that matters during a traceback: what did the supplier actually send, and why was it rejected? The engine processes records independently, so the valid records in a batch continue to the ledger while the bad record is isolated for reconciliation. This mirrors the dead-letter behavior of the shared Error Handling Workflows, so operators reconcile schema rejections through the same tooling as every other ingestion stage.

Quarantine depth per supplier is itself a signal. A sudden spike in rejections from one vendor usually means an upstream schema change — a renamed field, a switched date format, a new location-code convention — and that anomaly should surface to the Data Quality Monitoring layer against per-supplier SLAs rather than accumulate silently. When an entire supplier feed starts failing structural validation, the Fallback Routing Logic determines whether records divert to a manual review queue or halt for that vendor while healthy feeds continue.

Figure — two fault classes, two handling paths:

Deterministic validation faults versus transient transport faults A fault raised at the gate is classified before the engine reacts. Deterministic validation faults — an illegal lot code, a non-positive quantity, a naive timestamp, or a bad unit of measure — fail identically on every attempt, so they are routed straight to the quarantine store with zero retries. Transient transport faults — a store timeout, a 503, or a network partition on the durable write — often succeed on retry, so they enter a tenacity retry with exponential backoff and at most three attempts. A successful write is committed to the ledger; once attempts are exhausted, the record falls back to the same quarantine store. 0 retries on failure backoff write ok attempts exhausted Fault raised at the gate classify before reacting Validation fault deterministic illegal lot code · quantity ≤ 0 naive timestamp · bad UOM Transport fault transient store timeout · 503 network partition tenacity retry exp. backoff · ≤ 3 attempts Quarantine store raw payload · error path · hash Ledger commit durable write ok

Integration with the Ingestion Pipeline

This gate is one enforcement point inside the parent Supplier Data Ingestion & Sync Automation pipeline, and it is deliberately narrow: its only job is to accept a normalized payload, decide, and hand off. Validated records flow into the message queue that fronts Async Batch Processing, where I/O-bound persistence and enrichment run on a worker pool decoupled from validation. For high-volume feeds, synchronous validate-and-write becomes a bottleneck; offloading heavy database writes to workers maintains backpressure and prevents memory exhaustion when a single batch carries hundreds of thousands of records.

The contract this gate enforces is identical to the one applied inside the CSV/EDI Parser Setup, so a flat-file record and a REST-polled record are indistinguishable once they reach the ledger. Because the engine reads normalized input and emits fingerprinted records into distinct stores, its output honors the access-control expectations described in the Security Boundaries for Trace Data guidance — raw supplier payloads, quarantine artifacts, and validated KDEs each live in their own controlled store. New vendors are wired to this gate through Supplier Onboarding Automation, which registers the per-supplier field mapping before the first live batch runs.

Operational Notes

Deploy the validation engine as a triggered job — an object-storage event handler, a Kubernetes Job, or a Celery task fired on batch arrival — rather than a long-lived loop, so a crashed run is restarted cleanly by the scheduler and the normalized input remains the single source of truth. Recommended runtime and dependency versions:

  • Python 3.10+ (the code uses from __future__ import annotations, list[...] / dict[...] generics, and the X | Y union style).
  • pydantic ≥ 2.5 — the v2 field_validator / model_dump API. Do not mix in the v1 validator decorator.
  • tenacity ≥ 8.2 for wait_exponential and retry_if_exception_type.

Configuration should come from the environment, never from code. At minimum, provide a QUARANTINE_STORE target (use versioned, access-controlled object storage in production), a LEDGER_QUEUE endpoint for validated records, and the per-supplier column_mapping that the parser applies before this gate sees a record. Every validation decision must be logged with enough context to satisfy FDA inspection: the payload fingerprint, decision timestamp, specific KDE failures, and remediation status. Retention policies must align with FSMA 204 recordkeeping mandates — traceability data must remain accessible and searchable for at least two years, as detailed in the Data Retention Policies and the FDA FSMA 204 Traceability Rule. Keep each supplier’s mapping and the UOM enum under version control so a schema change is a reviewable diff, not a silent production surprise. For the flat-file-specific walkthrough of this gate, see Validating supplier CSV against KDE schemas.

Frequently Asked Questions

Why enforce the timestamp as timezone-aware instead of accepting a naive datetime?

A naive timestamp is ambiguous: the same wall-clock string can represent different absolute instants depending on the supplier’s locale, which breaks the ordering of a Shipping event against its matching Receiving event during recall reconstruction. The validator requires an explicit ISO 8601 offset, coerces to UTC, and rejects any future-dated event. Under 21 CFR 1.1340/1.1345 the event date and time is a load-bearing KDE, so an unanchored timestamp is treated as invalid rather than persisted.

What happens to a record that fails KDE validation?

It is quarantined, never dropped. The raw payload, the specific pydantic error path, the SHA-256 fingerprint, and a UTC timestamp are written to a durable quarantine store, while the valid records in the same batch continue to the ledger uninterrupted. This partial-commit contract means one malformed record never aborts an entire supplier batch.

Why retry transport errors but not validation errors?

Validation faults — a negative quantity, a bad unit of measure, a naive timestamp — are deterministic and fail identically on every retry, so the engine routes them straight to quarantine with zero retries. Transport faults on the durable write or ledger handoff are transient and often succeed on a second attempt, so persist_quarantine wraps only those in a tenacity exponential-backoff retry. Conflating the two either wastes cycles or risks losing quarantine evidence.

Why use pydantic strict mode instead of default coercion?

Default pydantic coercion will silently turn a string "12" into an integer, or a truthy value into a bool, which hides upstream data-quality defects that matter for compliance. strict=True blocks implicit coercion so a supplier sending the wrong type is surfaced as a validation error and quarantined, rather than persisted as a plausible-looking but unverified KDE.

Which 21 CFR Part 1 subpart governs the KDEs this gate validates?

Subpart S. Critical Tracking Event definitions are in § 1.1315, Traceability Lot Code assignment in § 1.1320, shipping KDEs including ship-from location in § 1.1340, and receiving KDEs including receive-to location in § 1.1345. The KDEPayload model enforces exactly the fields those sections require, and the mapping table cites the source for each element.

How does the engine keep validation decisions audit-ready?

Every decision is logged with the payload fingerprint, the CTE type, a UTC timestamp, and — on rejection — the precise error path. Quarantine artifacts are self-contained JSON documents retained for a minimum of two years to satisfy FSMA 204 recordkeeping. Because each record carries a stable SHA-256 fingerprint, an auditor can correlate a quarantined artifact with the exact bytes the supplier sent during a traceback.

Up: Supplier Data Ingestion & Sync Automation — this schema gate is the compliance enforcement point of the parent ingestion pipeline.