Skip to main content
Justice AI Unit logo
Government

The Justice AI Unit, the Ministry of Justice team building AI tools for judges and probation staff, uses Pydantic Validation as the executable contract wherever information changes form: audio to transcript, transcript to draft, bundle to citations, Python object to TypeScript client. Pydantic AI puts the same contracts around the actions a model proposes.

“Pydantic gives us an executable contract at those boundaries.”
— Elliott Phillips, Forward Deployed Engineer, Justice AI Unit

Products Used:

Pydantic ValidationPydantic AI
5 mins
As Markdown

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 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.

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.

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, 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.

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.

Diagram of contracts across Justice AI: Pydantic and FastAPI models generate an OpenAPI specification, which generates TypeScript types and Zod schemas; on the Python backend, external API responses pass through Pydantic validation before Python code; on the TypeScript client, API responses pass through Zod validation before application code

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

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

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.

  • 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.