Writing model output directly to relational databases without separate validation creates persistent data debt. Valid JSON may still have invalid data: nulls for required strings, negative quantities, or non-existent foreign keys. Once saved, errors propagate to queries, reports and business workflows.
This tutorial implements the full pipeline: free-text input → database insertion. Mistral Large 4 outputs structured extraction constrained by JSON Schema. The result undergoes multi-stage checks: JSON Schema validation, Pydantic validation, and isolated business rule checks invisible to the model. Only when all validations pass will the service start a database transaction and insert data. Any failed check skips transaction initiation and returns a specific error.
Stack: Python 3.12, FastAPI, Pydantic v2, SQLAlchemy 2.0, official Mistral SDK with pinned dependency versions. This lightweight pattern can be reused in bigger services with no extra framework overhead.

Architecture of the Write Path
Layered acceptance
Four distinct layers sit between the model response and the database. The first is syntactic JSON decoding. The second is structural compliance with the declared JSON Schema. The third is type and constraint checking performed by Pydantic. The fourth is a set of pure functions that encode business invariants. Each layer can reject the payload; none of them mutates the original response.
Transaction boundary
The database session is opened only after the fourth layer has returned success. This guarantees that a validation failure never leaves a partial or locked row. The transaction itself is kept as short as possible: a single insert or an insert-plus-update of related audit tables.
Idempotency key
Every request carries a client-supplied idempotency key. Before the model is called, the service checks whether that key has already produced a successful write. If it has, the previous result is returned without calling the model again. This protects against client retries that would otherwise generate duplicate rows.
Separation of model and persistence credentials
The process that calls Mistral Large 4 never holds database credentials that allow writes. A second, narrowly privileged role is used for the insert. This reduces the blast radius if the model-calling process is compromised.
Repository Layout and Runtime Assumptions
Source organization
The repository contains api/ for the FastAPI routers, domain/ for the Pydantic models and business functions, db/ for the SQLAlchemy models and session factory, and clients/ for the Mistral wrapper. Tests live in a parallel tree that mirrors the source layout.
Dependency pinning
Exact versions of FastAPI, Pydantic, SQLAlchemy, and the Mistral SDK are declared. The database driver is also pinned. A lockfile is committed so that every environment installs the same packages.
Configuration surface
Database URL, Mistral API key, model name, and maximum request duration are supplied through environment variables. The application refuses to start if any required variable is absent. Optional variables control logging level and whether the idempotency check is enforced.
Schema evolution policy
Changes to the JSON Schema or the Pydantic model require a corresponding database migration and a new set of fixture tests. The migration and the code change ship in the same pull request so that the two cannot drift.
Constructing the Structured Extraction Request
Schema as single source of truth
The JSON Schema is defined in a Python module that is imported by both the Mistral client and the Pydantic model generator. This eliminates the possibility that the prompt schema and the validation model diverge.
Minimal system prompt
The system prompt states only that the model is an extraction engine and must return a JSON object matching the supplied schema. No few-shot examples are included. The schema itself supplies all necessary constraints.
Response format enforcement
The Mistral client is called with the response format set to the JSON Schema object. Temperature is set to zero. Max tokens is set high enough to accommodate the largest expected object but low enough to bound cost.
Handling of partial model output
If the model returns a truncated response (for example because max tokens was reached), the JSON decoder fails and the request is rejected with a 422. No attempt is made to repair or complete the object.
Multi-Stage Validation Before the Write
Stage one and two: decode and schema
The raw string is decoded. The resulting dictionary is checked against the JSON Schema using a lightweight validator. Failures at this stage are cheap and are returned immediately.
Stage three: Pydantic
The dictionary is instantiated as a Pydantic model. Pydantic enforces types, required fields, and simple range constraints. Custom validators attached to the model can perform cross-field checks that the JSON Schema cannot express.
Stage four: business invariants
Pure functions receive the validated Pydantic instance and return either success or a structured error. Typical checks include referential integrity against a read-only cache, value ranges that depend on external configuration, and mutual exclusion rules between fields.
Decision table for rejection reasons
Validation layer | Example failure | Retryable | HTTP status | Logged detail level |
|---|---|---|---|---|
JSON decode | Trailing comma, unquoted key | No | 422 | Full payload hash |
JSON Schema | Missing required field | No | 422 | Field path only |
Pydantic | String where integer expected | No | 422 | Field path + type |
Business invariant | Foreign key not found in cache | No | 422 | Rule name only |
Database constraint | Unique violation after all checks | No | 409 | Constraint name |
Database Write and Observability
Short transaction
Once validation succeeds, a new SQLAlchemy session is created, the row is inserted, and the session is committed. The session is closed in a finally block so that connection leaks cannot occur.
Audit trail
An immutable audit record is written in the same transaction. It stores the request ID, the idempotency key, a hash of the accepted payload, and the model version that produced it. This record is the only place where model output is retained after the write.
Metrics and tracing
Every request emits spans for model latency, validation latency, and database latency. Counters track validation failures by layer and database constraint violations. These metrics are the primary signal for detecting schema drift or model regression.
Failure injection for the write path
Integration tests can force the database to raise a unique-constraint error even after validation has passed. The service must map that error to a 409 and must not leave the transaction open.

Testing the Full Path
Fixture-driven unit tests
A directory of JSON fixtures exercises every validation layer. Each fixture is labeled with the expected rejection layer and the expected HTTP status. The tests run without a database or a live model.
Recorded-response integration tests
A small set of real model responses (captured once and committed) is replayed through the full stack, including a test database. The tests assert both the final database state and the HTTP response.
Property-based checks
For numeric fields, a property-based test generates values inside and outside the allowed range and confirms that the business layer rejects the out-of-range cases.
Continuous enforcement
The fixture suite and the recorded-response suite run on every pull request. A separate nightly job exercises the live model with a fixed set of documents and stores the results for manual review; those results are never treated as pass/fail gates.
Latency, Cost, and Operational Limits
Latency composition
Under typical document sizes the model generation dominates. Validation and the database write together stay under 50 ms in the measured hypothetical environment. The service therefore budgets the majority of its latency allowance to the model call.
Token and cost control
Because the schema is fixed, the prompt token count is stable for a given document length. Completion tokens vary with the size of the extracted object. The service logs both numbers so that cost per successful write can be calculated after the fact.
Comparison of write-path costs
Component | Fixed or variable | Primary control lever | Observability metric |
|---|---|---|---|
Schema tokens | Fixed | Keep schema concise | Prompt tokens per request |
Document tokens | Variable | Truncate or pre-summarize | Prompt tokens per request |
Completion tokens | Variable | Limit max tokens, simplify schema | Completion tokens per request |
Validation CPU | Fixed | Keep Pydantic model simple | Validation duration histogram |
Database round-trip | Fixed | Connection pooling, indexes | DB duration histogram |
Production Verdict
The write path is ready for production use once authentication, rate limiting, and durable logging are added around it. The core guarantee—no unvalidated model output ever reaches the database—is enforced by construction. The service should be monitored for rising validation-failure rates, which usually indicate either a model regression or a schema change that has not been communicated to callers.
Implementation checklist before go-live:
Environment variables validated at startup.
JSON Schema and Pydantic model generated from the same source.
Business invariant functions covered by fixtures.
Idempotency key enforced and tested.
Database role limited to the required insert privileges.
Metrics and traces exported for every layer.
Recorded-response tests passing in CI.
Documented procedure for schema changes that includes a migration and new fixtures.
When the checklist is complete, the service can safely accept model-structured data and commit it only after every field has been examined by code the model cannot influence.