Views for Pydantic models
Project description
Typed views for Pydantic models
pydantic-views lets you derive focused, type-safe Pydantic models (“views”) from a base model. Each view exposes only the fields appropriate for a given operation—create, update, load, or a custom flow—so you avoid hand-maintaining parallel schemas.
You annotate each field of your base model once with how it may be accessed (read-only, write-only, hidden, …), and pydantic-views generates the right model for every operation. Nested and recurrent models get their matching views automatically, validators and field metadata are preserved, and the result is fully typed.
Typical service signatures become easy to express:
class ExampleModelCreate(View[ExampleModel], preset=CreatePreset):
pass
class ExampleModelCreateResult(View[ExampleModel], preset=CreateResultPreset):
pass
class ExampleModelLoad(View[ExampleModel], preset=LoadPreset):
pass
class ExampleModelUpdate(View[ExampleModel], preset=UpdatePreset):
pass
def create(input: ExampleModelCreate) -> ExampleModelCreateResult: ...
def load(model_id: str) -> ExampleModelLoad: ...
def update(model_id: str, input: ExampleModelUpdate) -> ExampleModelLoad: ...
Features
Unlimited views per model (create, update, load, or any custom flow).
Declare field access once; every view is derived from a single source of truth.
Works on nested models; referenced (and recurrent) models get their views too.
Works on RootModel and complex container types (list, set, tuple, dict, Literal, unions).
Ready-made presets and builders for common patterns, or define views manually.
Preserves validators, aliases, defaults, titles, descriptions and other field metadata.
Custom access tags for fine-grained, per-view field selection.
Helpers to build a view from a model, build a model from a view, and merge a view into a model.
Fully typed with a shipped py.typed marker and an extensive test suite.
Open source and published on PyPI.
Installation
Using pip:
pip install pydantic-views
Using poetry:
poetry add pydantic-views
Using uv:
uv add pydantic-views
Quickstart
Mark each field with its access mode using the provided annotations. Unmarked fields default to read/write everywhere.
from typing import Annotated
from annotated_types import Gt
from pydantic import BaseModel, computed_field
from pydantic_views import AccessMode, Hidden, ReadOnly, ReadOnlyOnCreation
class ExampleModel(BaseModel):
# Unmarked fields are read/write everywhere.
field_str: str
# Read-only fields are removed from create and update views.
field_read_only_str: ReadOnly[str]
# Read-only-on-creation fields are hidden on create, update and load views,
# but appear on create-result views.
field_api_secret: ReadOnlyOnCreation[str]
# Combine access modes with Annotated and keep validators (Gt in this case).
field_int: Annotated[int, AccessMode.READ_ONLY, AccessMode.WRITE_ONLY_ON_CREATION, Gt(5)]
# Hidden fields never appear.
field_hidden_int: Hidden[int]
# Computed fields appear only on read views.
@computed_field
def field_computed_field(self) -> int:
return self.field_hidden_int * 5
Access modes
Every field carries one or more access modes. The mode decides which generated views expose the field. You can use the convenient annotation aliases (ReadOnly[T], WriteOnly[T], …) or attach AccessMode values directly with Annotated when you want to combine several modes or keep extra validators.
Annotation |
Access mode |
Create |
CreateResult |
Update |
Load |
|---|---|---|---|---|---|
(unmarked) / ReadAndWrite[T] |
READ_AND_WRITE |
✓ |
✓ |
✓ |
✓ |
ReadOnly[T] |
READ_ONLY |
✓ |
✓ |
||
WriteOnly[T] |
WRITE_ONLY |
✓ |
✓ |
||
ReadOnlyOnCreation[T] |
READ_ONLY_ON_CREATION |
✓ |
|||
WriteOnlyOnCreation[T] |
WRITE_ONLY_ON_CREATION |
✓ |
|||
Hidden[T] |
HIDDEN |
||||
@computed_field |
✓ |
✓ |
Create also hides default None values, Update makes every field optional, and the read views (CreateResult and Load) include computed fields.
The four standard views
pydantic-views ships four presets that cover the typical CRUD lifecycle. Each preset is a Preset (a NamedTuple) you pass to a View subclass via preset=:
CreatePreset — input accepted when creating a resource (writable and write-on-creation fields).
CreateResultPreset — what you return after creation (readable fields plus computed fields).
UpdatePreset — partial input for updates (writable fields, all optional).
LoadPreset — what you return when reading a resource (readable fields plus computed fields).
Build a load view
from pydantic_views import View, LoadPreset
class ExampleModelLoad(View[ExampleModel], preset=LoadPreset):
pass
Which is equivalent to:
from typing import Annotated
from annotated_types import Gt
from pydantic import BaseModel
from pydantic_views import View
class ExampleModelLoad(BaseModel):
field_str: str
field_int: Annotated[int, Gt(5)]
field_computed_field: int
Build an update view
from pydantic_views import View, UpdatePreset
class ExampleModelUpdate(View[ExampleModel], preset=UpdatePreset):
pass
Which is equivalent to:
from pydantic import Field, BaseModel
from pydantic_core import MISSING
from pydantic_views import View
class ExampleModelUpdate(BaseModel):
field_str: str = Field(default_factory=lambda: MISSING)
On Update views every field uses a default factory that returns MISSING, so fields become optional. Applying the view to a model only updates values that were set.
original_model = ExampleModel(
field_str="anything",
field_read_only_str="anything",
field_api_secret="anything",
field_int=10,
field_hidden_int=33,
)
update = ExampleModelUpdate(field_str="new_data")
updated_model = update.view_apply_to(original_model)
assert isinstance(updated_model, ExampleModel)
assert updated_model.field_str == "new_data"
If a field is not set on the update view, the original value is kept.
original_model = ExampleModel(
field_str="anything",
field_read_only_str="anything",
field_api_secret="anything",
field_int=10,
field_hidden_int=33,
)
update = ExampleModelUpdate()
updated_model = update.view_apply_to(original_model)
assert isinstance(updated_model, ExampleModel)
assert updated_model.field_str == "anything"
Working with view data
Every generated view inherits a small set of helpers from View to move data between the base model and its views:
View.view_build_from(model) — build a view instance from a model instance, omitting unset fields.
view.view_build_to() — build a base-model instance from the view, using only the fields set on the view.
view.view_apply_to(model) — return a copy of model updated with the fields set on the view (deep merge).
View.view_class_root() — return the base model class the view was generated from.
# Project a stored model into the read view returned by your API.
stored = ExampleModel(
field_str="value",
field_read_only_str="ro",
field_api_secret="secret",
field_int=10,
field_hidden_int=33,
)
payload = ExampleModelLoad.view_build_from(stored)
# Apply a partial update and get a new, validated model back.
patched = ExampleModelUpdate(field_str="updated").view_apply_to(stored)
Two ways to define your views
Subclass View (with a preset)
The declarative form integrates with type checkers and IDEs, and lets you add extra fields:
from pydantic_views import View, CreatePreset
class ExampleModelCreate(View[ExampleModel], preset=CreatePreset):
# You can add fields that are not part of the base model.
extra_flag: bool = False
The preset= keyword expands the preset’s configuration into the view; an explicit keyword passed alongside it (for example view_name=...) overrides the preset’s value. The bundled mypy plugin understands both the preset= form and explicit keywords. (The older **LoadPreset._asdict() splat still works at runtime, but mypy cannot analyse ** unpackings in class keywords, so prefer preset= for full type checking.)
You can also pass the configuration directly as keyword arguments instead of a preset:
from pydantic_views import AccessMode, View
class ExampleModelReadOnly(
View[ExampleModel],
view_name="ReadOnly",
access_modes=(AccessMode.READ_AND_WRITE, AccessMode.READ_ONLY),
include_computed_fields=True,
):
pass
Use a builder
The imperative form builds (and caches) a view class on demand—handy when generating views dynamically:
from pydantic_views import BuilderLoad, BuilderUpdate
ExampleModelLoad = BuilderLoad().build_view(ExampleModel)
ExampleModelUpdate = BuilderUpdate().build_view(ExampleModel)
Views are cached per base model, so building the same view twice returns the same class.
Custom views with builders
For anything beyond the standard presets, build your own Builder. The configuration is shared by builders, presets and the View subclass keyword arguments:
access_modes — which access modes to include in the view.
include_tags / exclude_tags — force-include or force-exclude fields by AccessTag.
all_optional — make every field optional (the basis of Update views).
all_nullable — make every field nullable.
hide_default_null — drop default None values so they don’t appear in the schema.
include_computed_fields — include @computed_field properties.
from pydantic_views import AccessMode, Builder
builder = Builder(
view_name="Summary",
access_modes=(AccessMode.READ_AND_WRITE, AccessMode.READ_ONLY),
include_computed_fields=True,
all_nullable=True,
)
ExampleModelSummary = builder.build_view(ExampleModel)
Nested models
When a field references another Pydantic model, pydantic-views generates a matching view for it automatically, recursively, and even for self-referential or circular models. Container types (list, set, tuple, dict, Literal and unions) are traversed too.
from pydantic import BaseModel
from pydantic_views import ReadOnly, View, LoadPreset
class Address(BaseModel):
street: str
zip_code: ReadOnly[str]
class User(BaseModel):
name: str
addresses: list[Address]
class UserLoad(View[User], preset=LoadPreset):
pass
# UserLoad.addresses is a list of the generated AddressLoad view.
Static type checking
Views are generated at runtime, so a type checker cannot see their fields on its own. pydantic-views ships a mypy plugin (pydantic_views.mypy) that reproduces the builder’s field selection statically. Enable it in your mypy configuration, before pydantic.mypy:
[mypy]
plugins = pydantic_views.mypy, pydantic.mypy
It understands both the preset= and explicit-keyword forms of a view, and types nested-model fields with their generated views (Address + Load -> AddressLoad):
from pydantic_views import LoadPreset, View
class UserLoad(View[User], preset=LoadPreset):
pass
reveal_type(UserLoad.model_validate({}).id) # int — the plugin knows the field exists
UserLoad(password="secret") # error: write-only field is not in a load view
For type checkers that don’t run mypy plugins (pyright, PyCharm, …) you can turn the plugin’s output into .pyi stubs. See Type checking and stub files for the plugin options, the stub-generation CLI, and the full list of what is and isn’t analysed statically.
Documentation
Full documentation, including the complete API reference, is available at pydantic-views.readthedocs.io.
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_views-0.5.0b0.tar.gz.
File metadata
- Download URL: pydantic_views-0.5.0b0.tar.gz
- Upload date:
- Size: 21.6 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
548d536b524195f4a4504c0d57f0c10968a91367d24fc861df058da30ac194fd
|
|
| MD5 |
9c98509e06f0166fb5e3fd7bd11fdf0f
|
|
| BLAKE2b-256 |
846f5b6a590392b368777981397369ffc9d628d4087bb7ed2bceb30459ebc846
|
Provenance
The following attestation bundles were made for pydantic_views-0.5.0b0.tar.gz:
Publisher:
push_code.yaml on alfred82santa/pydantic-views
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
pydantic_views-0.5.0b0.tar.gz -
Subject digest:
548d536b524195f4a4504c0d57f0c10968a91367d24fc861df058da30ac194fd - Sigstore transparency entry: 1839864074
- Sigstore integration time:
-
Permalink:
alfred82santa/pydantic-views@a6dac56577c6cb74fa5d59010239afec5a4c7946 -
Branch / Tag:
refs/heads/main - Owner: https://github.com/alfred82santa
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
push_code.yaml@a6dac56577c6cb74fa5d59010239afec5a4c7946 -
Trigger Event:
push
-
Statement type:
File details
Details for the file pydantic_views-0.5.0b0-py3-none-any.whl.
File metadata
- Download URL: pydantic_views-0.5.0b0-py3-none-any.whl
- Upload date:
- Size: 23.6 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
f025b03e6cbfa879cd92548dddafc442fb83f836c6876a33353c5516b75df468
|
|
| MD5 |
412135f3b2703085229a22e98a7541b0
|
|
| BLAKE2b-256 |
5ff38947f1f5ca7062e256831579c144b90f2863cd6b8260b700eaa9a9ed2cb3
|
Provenance
The following attestation bundles were made for pydantic_views-0.5.0b0-py3-none-any.whl:
Publisher:
push_code.yaml on alfred82santa/pydantic-views
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
pydantic_views-0.5.0b0-py3-none-any.whl -
Subject digest:
f025b03e6cbfa879cd92548dddafc442fb83f836c6876a33353c5516b75df468 - Sigstore transparency entry: 1839864244
- Sigstore integration time:
-
Permalink:
alfred82santa/pydantic-views@a6dac56577c6cb74fa5d59010239afec5a4c7946 -
Branch / Tag:
refs/heads/main - Owner: https://github.com/alfred82santa
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
push_code.yaml@a6dac56577c6cb74fa5d59010239afec5a4c7946 -
Trigger Event:
push
-
Statement type: