Skip to content

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

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

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

Conversation

@lesnik512

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. ruff format --check, ruff check --no-fix, ty check all pass.

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

🤖 Generated with Claude Code

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 lesnik512 merged commit 976b2fc into main Jun 26, 2026
2 checks passed
@lesnik512 lesnik512 deleted the refactor/split-deck-response-contract branch June 26, 2026 10:23
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