Pydantic¶
Pydantic is confined to one job: validating and documenting the shape of
requests and responses at the FastAPI boundary. It does not appear in
core or adaptation, which use plain dataclasses instead (see
Python). This chapter is about that boundary, not about Pydantic in
general.
Pydantic v2, explicitly¶
src/test_data_workbench/api/models.py imports BaseModel,
ConfigDict, and Field from pydantic, the v2 API. Every request and
response type used by the FastAPI app (see FastAPI) is a
BaseModel subclass defined here: DatabaseConnection,
SchemaAnalysisRequest/Response, GeneratorCreationRequest/
Response, DataGenerationRequest/Response,
RapidDeploymentRequest/Response, and several more. pyproject.toml
pins pydantic>=2.5.0.
ConfigDict over the old inner class Config¶
Every model that needs configuration uses the v2 model_config = ConfigDict(...)
class attribute rather than the v1-style nested class Config::
class DatabaseConnection(BaseModel):
"""Database connection information."""
model_config = ConfigDict(json_schema_extra={
"examples": [{
"connection_string": "postgresql://user:pass@localhost:5432/dbname",
"timeout": 30,
}]
})
connection_string: str = Field(..., description="Database connection string")
timeout: Optional[int] = Field(30, description="Connection timeout in seconds")
json_schema_extra is used consistently across the request/response models
to attach a worked example to each schema. Because FastAPI derives its
OpenAPI document from these models automatically, that example shows up
directly in the /docs Swagger UI (see FastAPI) without any separate
documentation-writing step, the example lives next to the field definitions
it describes.
Field for defaults, requiredness, and description¶
Field(...) marks a value required (connection_string: str = Field(...,
description=...)); a concrete default marks it optional
(timeout: Optional[int] = Field(30, ...)). The description on every
field is not decoration, it is what populates the parameter descriptions in
the generated OpenAPI schema and therefore in /docs. A model with
undocumented fields would still validate correctly; it would just produce a
less useful API reference, which is the whole reason to bother with the
description= kwarg at all here rather than relying on type hints alone.
Aliasing around a name collision¶
DataGenerationRequest has one field worth calling out specifically:
class DataGenerationRequest(BaseModel):
model_config = ConfigDict(populate_by_name=True, json_schema_extra={...})
...
validate_output: bool = Field(True, description="Validate generated data", alias="validate")
The Python attribute is validate_output, but the wire field (what a
client actually sends and what shows up in the OpenAPI schema) is
validate, via alias="validate". validate is a natural field name
from an API consumer’s point of view, but it collides with meaning inside
this codebase, core/validators.py and ValidationResult already use
“validate” as a term of art for AST-level generator/contribution checking
(see Architecture), and a field literally named
validate on a Pydantic model risks shadowing conventions elsewhere in the
code that reason about “validation” as a concept. The alias keeps the public
API ergonomic while keeping the internal attribute name unambiguous.
populate_by_name=True is what allows the model to also be constructed
from Python code using the attribute name validate_output directly,
rather than only accepting the alias.
Boundary, not backbone¶
Nothing constructed inside adaptation or core is a Pydantic model;
SchemaInfo, Table, Column, and the rest are the dataclasses from
Python. An endpoint function is where the translation happens: it
receives a validated Pydantic request model, calls into the dataclass-based
core, and builds a Pydantic response model from the result to return. Keeping
that boundary sharp means the validation and JSON-schema machinery Pydantic
brings is paid for exactly once, at the edge, rather than threaded through
every internal function call.