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.
Typical service signatures become easy to express:
ExampleModelCreate = BuilderCreate().build_view(ExampleModel)
ExampleModelCreateResult = BuilderCreateResult().build_view(ExampleModel)
ExampleModelLoad = BuilderLoad().build_view(ExampleModel)
ExampleModelUpdate = BuilderUpdate().build_view(ExampleModel)
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, custom).
Works on nested models; referenced models get views too.
Builders for common patterns, or define views manually.
Fully typed with shipped
py.typedand tests.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.
from typing import Annotated
from pydantic import BaseModel, computed_field, gt
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
Build a load view:
from pydantic_views import BuilderLoad
ExampleModelLoad = BuilderLoad().build_view(ExampleModel)
Which is equivalent to:
from pydantic import gt
from pydantic_views import View
class ExampleModelLoad(View[ExampleModel]):
field_str: str
field_int: Annotated[int, gt(5)]
field_computed_field: int
To build an update view:
from pydantic_views import BuilderUpdate
ExampleModelUpdate = BuilderUpdate().build_view(ExampleModel)
Which is equivalent to:
from pydantic import Field, PydanticUndefined
from pydantic_views import View
class ExampleModelUpdate(View[ExampleModel]):
field_str: str = Field(default_factory=lambda: PydanticUndefined)
On Update views every field uses a default factory that returns PydanticUndefined,
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"