Skip to content

refactor: split the Deck response contract from its detail view#30

Merged
lesnik512 merged 1 commit into
mainfrom
refactor/split-deck-contract
Jun 26, 2026
Merged

refactor: split the Deck response contract from its detail view#30
lesnik512 merged 1 commit into
mainfrom
refactor/split-deck-contract

Conversation

@lesnik512

@lesnik512 lesnik512 commented Jun 26, 2026

Copy link
Copy Markdown
Member

Problem

One Deck schema served two endpoints with different card-loading strategies, leaking the difference into the type:

class Deck(DeckBase):
    id: PositiveInt
    cards: list[Card] | None
  • get_deckfetch_with_cards (selectinload) → cards populated.
  • list_decks / create_deck / update_deck → relationship is lazy="noload", so .cards returns [] without loading.

So list_decks reported cards: [] for every deck even when the deck has cards — the list contract was lying. The | None was also dead: noload yields [], never None.

Change

Split the one schema into two:

class Deck(DeckBase):
    """Light deck view for lists and writes; cards are not loaded."""
    id: PositiveInt

class DeckWithCards(Deck):
    """Deck detail view; cards are eager-loaded (selectinload) and always present."""
    cards: list[Card]
  • get_deckDeckWithCards (cards always present, non-optional)
  • list_decksDecks (Collection[Deck]), create_deck / update_deckDeck

Each endpoint's return type now states exactly what it loads. The dead | None is gone. The two-shape convention is documented in CLAUDE.md (Conventions) and in docstrings on the schemas.

Contract change ⚠️

Intentional: the cards key disappears from list / create / update responses (they never loaded cards anyway). get_deck is unchanged. Any client reading cards off a list response is affected.

Tests

test_get_one_deck locks DeckWithCards (asserts cards); list/create/update tests never asserted cards, so they stay green. 19 passed, 100% coverage.

🤖 Generated with Claude Code

A single Deck schema served two endpoints with different card-loading
strategies, leaking that difference into a cards: list[Card] | None
field. Worse, list_decks reported cards: [] for every deck regardless
of its real cards, because the relationship is lazy="noload" — the
list contract was lying, and the | None was dead (noload yields []).

Split into Deck (no cards) and DeckWithCards(Deck) (cards: list[Card],
always populated). get_deck returns DeckWithCards; list/create/update
return the light Deck. Each endpoint's type now states exactly what it
loads, and the dead | None is gone.

This is an intentional wire-contract change: the cards key disappears
from list/create/update responses. The two-shape convention is recorded
in CLAUDE.md and in docstrings on the Deck/DeckWithCards schemas.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@lesnik512 lesnik512 force-pushed the refactor/split-deck-contract branch from a6726b5 to 4e59e1c Compare June 26, 2026 10:16
@lesnik512 lesnik512 merged commit 88b9ee9 into main Jun 26, 2026
2 checks passed
@lesnik512 lesnik512 deleted the refactor/split-deck-contract branch June 26, 2026 10:17
lesnik512 added a commit to modern-python/fastapi-sqlalchemy-template that referenced this pull request Jun 26, 2026
One Deck schema served two endpoints with different card-loading
strategies, leaking the difference into the type:

    class Deck(DeckBase):
        id: PositiveInt
        cards: list[Card] | None

- get_deck -> fetch_with_cards (selectinload) -> cards populated.
- list_decks / create_deck / update_deck -> relationship is
  lazy="noload", so .cards returns [] without loading.

So list_decks reported cards: [] for every deck even when the deck has
cards. The | None was also dead: noload yields [], never None.

Split the one schema into two:

    class Deck(DeckBase):
        """Light deck view for lists and writes; cards are not loaded."""
        id: PositiveInt

    class DeckWithCards(Deck):
        """Deck detail view; cards are eager-loaded and always present."""
        cards: list[Card]

- get_deck -> DeckWithCards (cards always present, non-optional)
- list_decks -> Decks (Collection[Deck]), create_deck / update_deck -> Deck

Each endpoint's return type now states exactly what it loads.

Contract change (intentional): the cards key disappears from list /
create / update responses (they never loaded cards anyway). get_deck is
unchanged.

Ports modern-python/litestar-sqlalchemy-template#30.

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
lesnik512 added a commit that referenced this pull request Jun 26, 2026
PR #30 split the Deck response contract (light Deck for lists,
DeckWithCards for detail) but no test asserted that cards is absent
from list responses — the existing tests passed either way.

Add a test that creates a deck WITH a card and asserts the list item
has no cards key. Under the old single-schema design this would have
failed (it returned the misleading cards: []), so it pins the fix.

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