Pydantic in adapters only
Context and Problem Statement
Pydantic provides powerful validation, serialization, and settings management. Should we use it in the domain layer for our data models, or restrict it to specific layers?
Decision Drivers
- Domain models must remain pure and dependency-free (see ADR-0002)
- External data (JWT claims, HTTP requests, environment variables) requires robust validation
- Adapters need OpenAPI schema generation and structured error messages
- Mixing validation frameworks in the domain creates coupling to external libraries
Considered Options
- Pydantic everywhere (domain + adapters)
- Pydantic in adapters only, pure dataclasses in domain
- No Pydantic at all — manual validation everywhere
Decision Outcome
Chosen option: "Pydantic in adapters only", because it keeps the domain pure while leveraging Pydantic's strengths at system boundaries where external data enters the system.
Where Pydantic belongs
| Layer | Use case | Why |
|---|---|---|
adapters/ |
Parsing JWT claims from dict to domain TokenClaims |
Rich validation of incoming external data |
adapters/ |
Request/Response DTOs for HTTP APIs | Automatic OpenAPI schemas, user input validation |
adapters/ |
IdP configuration (Cognito endpoints, JWKS URIs) | pydantic-settings for env vars, .env, secrets |
ports/ |
Configuration schema for PolicyRule from YAML/JSON |
Structural validation with clear error messages |
The mapping pattern
Adapters receive raw data (dict, JSON, env vars), validate it with a Pydantic BaseModel, and then map it to a domain dataclass. Services only ever receive domain objects.
external data → Pydantic BaseModel (adapter) → domain dataclass (service)
What NOT to do
- DO NOT use Pydantic in
domain/— domain models remain pure dataclasses - DO NOT expose Pydantic models to services — services receive domain objects
- DO NOT use
BaseModelwhere a simple dataclass without complex validation suffices
Consequences
- Good, because domain layer has zero external dependencies
- Good, because Pydantic validation catches malformed external data at the boundary
- Good, because OpenAPI schemas are generated automatically from adapter DTOs
- Neutral, because it requires explicit mapping between Pydantic models and domain dataclasses
- Bad, because the mapping layer adds boilerplate code
Confirmation
domain/must never importpydantic— enforced by import linting- Code review ensures new Pydantic models are placed in
adapters/orports/, never indomain/orservices/
More Information
Suggested dependencies: pydantic for adapter models, pydantic-settings for proxy configuration.