How the Justice AI Unit validates AI output for judges and probation staff
The Justice AI Unit is the UK Ministry of Justice team that leads responsible AI adoption across the justice system and delivers the AI Action Plan for Justice. Elliott Phillips, one of the unit's forward deployed engineers, builds tools that help judges and probation staff work with documents and recordings. The questions he keeps returning to are the ones Pydantic Validation exists to answer: what does this field mean, where did it come from, and what can the next component safely assume?
"Much of the interesting work happens where information changes form: audio becomes a transcript, a transcript becomes a draft, a bundle becomes a set of documents and citations, or a Python object becomes an API response consumed by a TypeScript application. Pydantic gives us an executable contract at those boundaries."
Elliott Phillips, Forward Deployed Engineer, Justice AI Unit
The challenge: AI output that frontline users have to trust
The unit's products sit between language models and people whose decisions carry legal weight. A transcript feeds a draft. A document bundle feeds a set of claims and the citations that are supposed to support them. A Python service feeds a TypeScript front end. At each handoff, a model can return something plausible that is not quite what the next component expects, and a field that silently means the wrong thing is worse than one that fails.
A whole team builds and extends those products. A developer adding a task to a transcription pipeline should get the same guarantees as the developer who wrote the first one, without rediscovering them.
The solution: Pydantic Validation as the contract at every boundary
Elliott's design habit is to work through a component as a small Python class with a declarative, purpose-driven API. The model describes what a component accepts, and its validators let the team test awkward cases before connecting it to a service. When data is invalid, the validation error points at the field, including fields inside nested objects.
Justice Transcribe: validated AI responses, bounded retries
In Justice Transcribe, structured AI responses are validated before they are returned to calling code. A shared wrapper handles parsing and bounded retries, so a developer adding another task gets the same behavior as every existing one: a validated model or an error. There is no third outcome.
Docket: citations that stay attached to evidence
Docket, the unit's tool for helping judges work with document bundles, keeps citations alongside generated claims. Its models record two things separately: whether a quotation was found in the source, and whether it was judged to support the claim. Conflating those is how a confident, well-formatted citation ends up pointing at nothing.
An illustrative extension of that work lets the model select a passage while Pydantic derives the page and quotation from the source:
from typing_extensions import Self
from pydantic import (
BaseModel,
Field,
PrivateAttr,
ValidationInfo,
computed_field,
model_validator,
)
class Excerpt(BaseModel, frozen=True):
page: int = Field(ge=1)
quote: str = Field(min_length=1)
class Citation(BaseModel, extra="forbid", frozen=True):
source_id: str
_source: Excerpt = PrivateAttr()
@model_validator(mode="after")
def resolve_source(self, info: ValidationInfo) -> Self:
source = (info.context or {}).get(self.source_id)
if not isinstance(source, Excerpt):
raise ValueError("Citation is outside the supplied evidence")
self._source = source
return self
@computed_field
@property
def excerpt(self) -> Excerpt:
return self._source
The application supplies the authorized excerpts through validation context. An unknown reference fails validation, and so does any attempt to submit a page or quotation directly, because extra="forbid" rejects fields the model did not declare. With computed fields, input and output schemas differ on purpose: the model supplies a reference, and consumers receive the resolved citation. Whether the passage actually supports the claim remains a separate assessment.
One contract from FastAPI to TypeScript
In Docket, schema generation carries the contract beyond Python. FastAPI exposes the Pydantic models through OpenAPI, and from that specification the team generates TypeScript types and Zod runtime validators for the client.

Generated types catch mismatches while the client is being written. The JSON client then validates responses against the supplied schema before handing data to consuming components. Custom Python validators stay on the server, since OpenAPI cannot express every rule.
The products are split into discrete packages, such as transcript formatting and report generation. Most have no database or LLM SDK dependency, so another team can bring its own integrations and work to the same Pydantic data contract.
"I like handing over a component with a model the next developer can read, test, and reuse. That's what lets us at Justice AI move at pace, building quality products for frontline users in a scalable, secure way."
Elliott Phillips, Forward Deployed Engineer, Justice AI Unit
Pydantic AI: contracts around what a model proposes
The unit already uses Pydantic AI to put contracts around the actions a model proposes. The ambition is agents that carry complex work across its products, preserving evidence through each handoff while application code limits what each agent can do. Those contracts have to make uncertainty, failure, and the need for human review explicit.
"We want to take substantial administrative work off justice professionals' desks and give them results they can scrutinise, freeing their time for the decisions that need their judgement."
Elliott Phillips, Forward Deployed Engineer, Justice AI Unit
The results: one contract from model output to frontline user
A validated result or an error on every task. Justice Transcribe's shared wrapper means every structured AI task returns a validated Pydantic model or an error that names the failing field. A developer adding a task inherits the parsing, retries, and checks without rebuilding them.
Citations a reviewer can check. Docket records whether a quotation was found in the source separately from whether it supports the claim, so a well-formatted citation can't stand in for evidence.
One definition of the data from Python to TypeScript. The FastAPI models generate the OpenAPI specification, the TypeScript types, and the Zod validators, so the client and server can't drift apart without a type error or a rejected response.
Components other teams can reuse. Most packages carry no database or LLM SDK dependency, so another team can plug in its own integrations and keep the same Pydantic data contract.
Key takeaways
- Put the contract where information changes form. Audio to transcript, bundle to citation, Python to TypeScript. Each boundary gets a model that says what the next component can assume.
- Record "found" and "supports" separately. A citation model that tracks whether a quotation exists apart from whether it backs the claim stops a well-formatted reference from standing in for evidence.
- Generate the client contract from the server models. OpenAPI from FastAPI, then TypeScript types and Zod validators, means one definition of the shape and no hand-maintained copy to drift.
- Keep the rules the schema cannot express on the server. Validation context and model validators catch what OpenAPI cannot describe.
Building products where a model's output has to be trusted by someone else? Start with Pydantic Validation for the contracts and Pydantic AI for the agents.