Skip to content

refactor: deepen ORM→HTTP serialization behind a Collection seam#29

Merged
lesnik512 merged 1 commit into
mainfrom
refactor/serialization-seam
Jun 26, 2026
Merged

refactor: deepen ORM→HTTP serialization behind a Collection seam#29
lesnik512 merged 1 commit into
mainfrom
refactor/serialization-seam

Conversation

@lesnik512

Copy link
Copy Markdown
Member

Problem

The persistence→HTTP serialization seam was shallow and leaked. Every list handler built its response envelope from raw ORM rows:

return schemas.Decks(items=objects)  # ty: ignore[invalid-argument-type]

— each of the four carrying a ty: ignore for the Sequence[Model]list[Schema] static mismatch. get_card separately carried a stray return_dto=PydanticDTO[schemas.Card] on top of a manual model_validate.

Change

Introduce a deep Collection[T] seam in app/schemas.py:

class Collection[T: Base](Base):
    items: list[T]

    @classmethod
    def from_models(cls, objects: Iterable[object]) -> Self:
        return cls.model_validate({"items": list(objects)})

class Cards(Collection[Card]): pass
class Decks(Collection[Deck]): pass

The ORM→schema coercion now happens once, behind a small interface. Routing through model_validate (param typed Any) absorbs the coercion cleanly, so all four ty: ignore[invalid-argument-type] suppressions are removed. List handlers call schemas.Xs.from_models(objects); get_card drops its redundant DTO and now returns through its schema like every other handler.

Decks/Cards stay as named subclasses, so OpenAPI schema names are unchanged.

Contract & tests

  • Wire contract unchanged{items: [...]}, no audit timestamps, same schema names.
  • Single-object handlers unchanged (schemas.X.model_validate(instance) was already typed).
  • The seam is exercised by every list-endpoint test; existing HTTP tests are the test surface. 19 passed, 100% coverage held.

🤖 Generated with Claude Code

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 lesnik512 merged commit b1899fb into main Jun 26, 2026
2 checks passed
@lesnik512 lesnik512 deleted the refactor/serialization-seam branch June 26, 2026 09:50
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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant