Skip to main content

Pydantic — validation from a class definition

Summary: declare the shape of the data once as a typed class, and the library generates the parsing, coercion, error reporting and IDE types from it. The type annotations stop being documentation and become the specification.

How it works

class ClarifyResult(BaseModel):
extract_sql: str
questions: list[str]
confidence: float = 1.0 # optional, with a default

result = ClarifyResult.model_validate_json(raw) # raises if raw doesn't fit
result.extract_sql # str — guaranteed
  • Subclassing BaseModel is the whole declaration. No constructor, no validation code. Fields without a default are required.
  • The methods come freemodel_validate_json() (JSON text in), model_validate() (dict in), model_dump() / model_dump_json() (back out).
  • It reports every problem at once, not the first one — a ValidationError lists each bad field with its location and what was wrong. Let it bubble up; it's better than anything you'd write.
  • It coerces where that's unambiguous"5" into an int field. Not everywhere; a non-numeric string is an error, not a silent zero.
  • After a successful parse the object is typed. Editor completion works, and downstream code doesn't re-check anything.

What to do

  • Validate at the boundary — wherever data enters your process from outside it: an HTTP body, a config file, a third-party API response, a model's output. Inside the boundary, trust the object.
  • Don't hand-roll isinstance chains. They only check the fields you remembered, they drift from the real shape, and they fail one field at a time.
  • Define the model next to the thing it describes, not in a shared schemas.py dumping ground — it's part of that component's contract.

Where this came up

Every phase of the data-patch-agent parses into a model rather than returning prose. The reason it matters there — LLM output is the input to the next function, so it has to be checked before anything downstream touches it — is in validating every phase into a schema.