…absent
Any list holding at least one row failed to load. GET /api/lists/{id}/data rows
do not include `listId`, but ListDataRow.ListId was declared `required`, so
System.Text.Json threw before the rows ever reached the view:
JsonException: JSON deserialization for type 'InterlinedList.Models.ListDataRow'
was missing required properties including: 'listId'.
Live row keys (captured 2026-09-16) are exactly:
createdAt, createdByUser, id, lastEditedByUser, rowData, updatedAt, version
Found while verifying a side-note from #17's service work, not from the backlog.
Why it survived this long: the test account's only list has ZERO rows, so every
manual pass over the Lists view exercised the empty path. There is also an
asymmetry that hides it — POST /api/lists/{id}/data DOES return `listId` in its
{message, data:{...}} envelope, so a write-path verification looks fine. Only
the read path omits it.
Fix: ListId is now nullable, with the reasoning recorded on the property so it
does not get "tidied" back to required. The caller already knows which list it
asked for.
Also models the fields the payload carries that the app was dropping:
- version — monotonic per-row version, the hook for optimistic concurrency on
row edits so two clients editing one row can be detected instead of silently
last-writer-wins.
- rowNumber — server ordinal, null on a schema-less list.
- createdByUser / lastEditedByUser — who added and last changed the row, which
is what makes a shared list legible (see #65).
Verified by deserializing both real shapes in a net10.0 harness — the read
payload now parses (ListId null, version 1, createdByUser @Messenger) and the
create-shape payload still populates ListId. No other code reads .ListId, so
the nullability change has no call-site fallout.
Test-account hygiene: two throwaway lists were created to obtain a row payload
at all (the account had none), then both deleted and the account confirmed back
to its single pre-existing list.
Closes #144
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Closes #144
Severity: any list with at least one row failed to load
GET /api/lists/{id}/datarows do not includelistId, butListDataRow.ListIdwas declaredrequired, soSystem.Text.Jsonthrew before the rows reached the view:Live row keys (2026-09-16) are exactly
createdAt, createdByUser, id, lastEditedByUser, rowData, updatedAt, version. NolistId.This wasn't in the backlog — found while verifying a side-note from #17's service work.
Why it survived
Two reasons, and the second is the interesting one:
POST /api/lists/{id}/datadoes returnlistIdin its{message, data:{…}}envelope:{"message":"Row created successfully", "data":{"id":"679fbb8b-…","listId":"0f061f4d-…","rowData":{"probe":"value"},"version":1,…}}So verifying row-add against the create response — which is what
the-gaps.mdsession 2 describes — confirms a field the read path omits. That asymmetry is recorded on the property so it doesn't get "tidied" back torequiredlater.Fix
ListIdis nullable. The caller already knows which list it asked for.Also models the fields the payload carries that the app was silently dropping:
version— monotonic per-row version. This is the hook for optimistic concurrency on row edits, so two clients editing one row can be detected rather than silently last-writer-wins. Relevant to Schema: replace the freeform JSON row editor with a typed, schema-driven one #21.rowNumber— server ordinal; null on a schema-less list.createdByUser/lastEditedByUser— who added and last changed the row, which is what makes a shared list legible (Lists: contributors panel #65).Verification
Deserialized both real shapes in a
net10.0harness:grep -rn '\.ListId'shows no other consumer, so the nullability change has no call-site fallout.dotnet buildgreen in Debug and Release.Test-account hygiene
Two throwaway lists had to be created to obtain a row payload at all (the account had none). Both were deleted and the account confirmed back to its single pre-existing
New list. The first attempt also taught me the create envelope is{message, data:{…}}rather than{list:{…}}, and that row creates want{data:…}not{rowData:…}— which is what the client already sends.🤖 Generated with Claude Code