refactor: deepen ORM→HTTP serialization behind a Collection seam#29
Merged
Conversation
The persistence→HTTP serialization seam was shallow and leaked: every list handler built its response envelope from raw ORM rows (schemas.Decks(items=objects)), each carrying a ty: ignore for the Sequence[Model] → list[Schema] mismatch, and get_card carried a stray return_dto=PydanticDTO on top of a manual model_validate. Introduce a generic Collection[T] base with a from_models classmethod that routes through model_validate, so the ORM→schema coercion happens once at the seam. Decks/Cards become named subclasses (clean OpenAPI names); list handlers call schemas.Xs.from_models(objects); get_card drops its redundant DTO. Removes all four ty: ignore[invalid-argument-type] suppressions. Wire contract unchanged; existing HTTP tests cover the seam. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
lesnik512
added a commit
to modern-python/fastapi-sqlalchemy-template
that referenced
this pull request
Jun 26, 2026
Port of modern-python/litestar-sqlalchemy-template#29, adapted to this repo's idiom and extended to converge on the litestar template's end state: no casts anywhere, every handler routed through its schema. Introduce a deep generic `Collection[T]` seam in app/schemas.py so the ORM→schema coercion happens once behind a small interface. `Cards`/`Decks` become `Collection[Card]`/`Collection[Deck]` subclasses (schema names unchanged). List handlers call `schemas.Xs.from_models(objects)`. Kill every `typing.cast` in app/api/decks.py — the local equivalent of the leak the litestar PR removed. Single-object handlers now return via `schemas.X.model_validate(instance)`; collections via `from_models`. Update the CLAUDE.md convention to mandate explicit ORM→schema conversion and forbid casts. Wire contract unchanged; 19 tests pass, 100% coverage held. Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Problem
The persistence→HTTP serialization seam was shallow and leaked. Every list handler built its response envelope from raw ORM rows:
— each of the four carrying a
ty: ignorefor theSequence[Model]→list[Schema]static mismatch.get_cardseparately carried a strayreturn_dto=PydanticDTO[schemas.Card]on top of a manualmodel_validate.Change
Introduce a deep
Collection[T]seam inapp/schemas.py:The ORM→schema coercion now happens once, behind a small interface. Routing through
model_validate(param typedAny) absorbs the coercion cleanly, so all fourty: ignore[invalid-argument-type]suppressions are removed. List handlers callschemas.Xs.from_models(objects);get_carddrops its redundant DTO and now returns through its schema like every other handler.Decks/Cardsstay as named subclasses, so OpenAPI schema names are unchanged.Contract & tests
{items: [...]}, no audit timestamps, same schema names.schemas.X.model_validate(instance)was already typed).🤖 Generated with Claude Code