refactor: split the Deck response contract from its detail view#30
Merged
Conversation
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>
a6726b5 to
4e59e1c
Compare
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>
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
One
Deckschema served two endpoints with different card-loading strategies, leaking the difference into the type:get_deck→fetch_with_cards(selectinload) → cards populated.list_decks/create_deck/update_deck→ relationship islazy="noload", so.cardsreturns[]without loading.So
list_decksreportedcards: []for every deck even when the deck has cards — the list contract was lying. The| Nonewas also dead:noloadyields[], neverNone.Change
Split the one schema into two:
get_deck→DeckWithCards(cards always present, non-optional)list_decks→Decks(Collection[Deck]),create_deck/update_deck→DeckEach endpoint's return type now states exactly what it loads. The dead
| Noneis gone. The two-shape convention is documented in CLAUDE.md (Conventions) and in docstrings on the schemas.Contract change⚠️
Intentional: the
cardskey disappears fromlist/create/updateresponses (they never loaded cards anyway).get_deckis unchanged. Any client readingcardsoff a list response is affected.Tests
test_get_one_decklocksDeckWithCards(assertscards); list/create/update tests never assertedcards, so they stay green. 19 passed, 100% coverage.🤖 Generated with Claude Code