Elegant projection of Pydantic BaseModels through Python Protocols.
Project description
pydantic-projections
Elegant projection of Pydantic BaseModels through Python Protocols — serialise and deserialise only the fields a Protocol declares, nothing more.
Install
uv add pydantic-projections
# or
pip install pydantic-projections
Why
You have a fat BaseModel for internal use, and you want to expose only a subset of its fields over an API, to a logging system, or to a downstream consumer. Pydantic already lets you do this with model_dump(include=...), but that's stringly-typed and type-unsafe. A Protocol describes the shape you want; pydantic-projections turns that Protocol into a real BaseModel at runtime, cached once per Protocol.
Usage
from typing import Protocol
from pydantic import BaseModel
from pydantic_projections import project, projection
class User(BaseModel):
id: int
name: str
email: str
password_hash: str
class UserSummary(Protocol):
id: int
name: str
user = User(id=1, name="Alice", email="a@b.c", password_hash="secret")
# One-shot: project an instance, get a BaseModel typed as UserSummary
summary = project(user, UserSummary)
summary.model_dump_json()
# -> '{"id":1,"name":"Alice"}'
# Get the reusable class (cached): useful for response_model, schema export, etc.
SummaryModel = projection(UserSummary)
SummaryModel.model_validate_json('{"id":1,"name":"Alice","extra":"ignored"}')
# -> extra fields are ignored
SummaryModel.model_json_schema()
# -> standard pydantic JSON schema
Nested protocols and containers
Protocols can reference other Protocols. The projection is built recursively, so list[P], dict[str, P], P | None, and plain P all work:
class AddressSummary(Protocol):
street: str
zip_code: str
class UserWithAddresses(Protocol):
id: int
name: str
address: AddressSummary
past_addresses: list[AddressSummary]
shipping: AddressSummary | None
@property-style Protocols
Protocols that declare fields as properties are also supported — the property's return type is used:
class UserDisplay(Protocol):
@property
def display_name(self) -> str: ...
project(user, UserDisplay).display_name
Computed / derived fields on the source
@computed_field / @property declarations on the source model are readable through the projection, because validation runs with from_attributes=True:
class User(BaseModel):
id: int
name: str
@computed_field
@property
def display_name(self) -> str:
return f"User: {self.name}"
class UserDisplay(Protocol):
display_name: str
project(user, UserDisplay).display_name # -> "User: Alice"
Typing at the call site
project(instance, Proto) is typed to return Proto, so summary.name resolves to str in mypy/pyright without a cast. At runtime the object is a BaseModel subclass that structurally satisfies the Protocol.
Config pass-through and frozen
Projections are immutable by default (frozen=True): a projection is a derived view of its source, so attempting instance.x = ... raises ValidationError. Opt back into mutation with frozen=False if you need it. Merge additional ConfigDict options (e.g. alias generator for camelCase output) via config=:
from pydantic import ConfigDict
from pydantic.alias_generators import to_camel
CamelSummary = projection(
UserSummary,
config=ConfigDict(alias_generator=to_camel, populate_by_name=True),
)
MutableSummary = projection(UserSummary, frozen=False)
Classes are cached per (protocol, config, frozen) triple; config values must be hashable.
Error handling
project() wraps pydantic's ValidationError in a ProjectionError that carries the protocol, source type, and original validation error:
from pydantic_projections import ProjectionError
try:
project(partial_user, UserSummary)
except ProjectionError as e:
e.protocol # the Protocol class
e.source_type # type(instance)
e.validation_error # the underlying pydantic ValidationError
JSON shortcut
from pydantic_projections import project_json
project_json(user, UserSummary) # str
project_json(user, UserSummary, indent=2) # forwards **kwargs to model_dump_json
Cache management
from pydantic_projections import cache_clear
cache_clear() # useful in test fixtures or hot-reload workflows
Semantics
- Extras are ignored on deserialisation (
extra="ignore"). This is a hard invariant — passingextra="forbid"viaconfig=does not override it. from_attributes=True— accepts dicts, JSON, or arbitrary objects that expose the Protocol's members. Also a hard invariant.- Projections are immutable by default (
frozen=True). Passfrozen=Falsefor a mutable variant. frozenandconfig=propagate to nested projections — an alias generator orfrozenflag applied at the top level also applies to every Protocol reachable through containers and unions.- Optional widening is allowed: source
name: stris accepted by a Protocol declaringname: str | None. - Narrowing is not: if the source value is
Nonefor a Protocol field typedstr, validation raises. - Classes are cached per
(protocol, config, frozen)viafunctools.cache.
Limitations (v0.1)
- Cyclic Protocols (a Protocol that references itself transitively) are not supported and will recurse.
- Generic Protocols (
Protocol[T]) with unresolvedTypeVars are not supported. - Config values passed via
config=must be hashable for caching.
Development
uv sync
uv run pytest
uv run python scripts/validate_tests.py
uv run ruff check src/ tests/
uv run mypy src/
uv run coverage run -m pytest && uv run coverage report
Tests use pytest-describe (describe_/when_/with_/it_). See CLAUDE.md for conventions.
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file pydantic_projections-0.3.0.tar.gz.
File metadata
- Download URL: pydantic_projections-0.3.0.tar.gz
- Upload date:
- Size: 59.3 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: uv/0.11.7 {"installer":{"name":"uv","version":"0.11.7","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
5ce9ebe257b489cde7b44fb817492f9adfe12d3c476c91e40fb7f666cc77169c
|
|
| MD5 |
ce174951ded216d7f197203d6d0c908f
|
|
| BLAKE2b-256 |
490d7234c47b23b7eaf86e7fb02689b890a875e94e325cf1ede0f19e4fd36d0a
|
File details
Details for the file pydantic_projections-0.3.0-py3-none-any.whl.
File metadata
- Download URL: pydantic_projections-0.3.0-py3-none-any.whl
- Upload date:
- Size: 7.3 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: uv/0.11.7 {"installer":{"name":"uv","version":"0.11.7","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
a89a201922b1687aa739f4a4114b2308b222f960ccacc8504b6ed802b417ddc2
|
|
| MD5 |
3d09fb580c7417be05b05f4a46da22f7
|
|
| BLAKE2b-256 |
f09596a55a1fd822ffbff9a5fef0e8473a1fcf557581e42398a45f59b52616be
|