Skip to main content

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 — passing extra="forbid" via config= 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). Pass frozen=False for a mutable variant.
  • frozen and config= propagate to nested projections — an alias generator or frozen flag applied at the top level also applies to every Protocol reachable through containers and unions.
  • Optional widening is allowed: source name: str is accepted by a Protocol declaring name: str | None.
  • Narrowing is not: if the source value is None for a Protocol field typed str, validation raises.
  • Classes are cached per (protocol, config, frozen) via functools.cache.

Limitations (v0.1)

  • Cyclic Protocols (a Protocol that references itself transitively) are not supported and will recurse.
  • Generic Protocols (Protocol[T]) with unresolved TypeVars 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


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

pydantic_projections-0.3.0.tar.gz (59.3 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

pydantic_projections-0.3.0-py3-none-any.whl (7.3 kB view details)

Uploaded Python 3

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

Hashes for pydantic_projections-0.3.0.tar.gz
Algorithm Hash digest
SHA256 5ce9ebe257b489cde7b44fb817492f9adfe12d3c476c91e40fb7f666cc77169c
MD5 ce174951ded216d7f197203d6d0c908f
BLAKE2b-256 490d7234c47b23b7eaf86e7fb02689b890a875e94e325cf1ede0f19e4fd36d0a

See more details on using hashes here.

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

Hashes for pydantic_projections-0.3.0-py3-none-any.whl
Algorithm Hash digest
SHA256 a89a201922b1687aa739f4a4114b2308b222f960ccacc8504b6ed802b417ddc2
MD5 3d09fb580c7417be05b05f4a46da22f7
BLAKE2b-256 f09596a55a1fd822ffbff9a5fef0e8473a1fcf557581e42398a45f59b52616be

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page