fix(notifications): always serialize streams so clearing the last subscription saves
#25
Reference in New Issue
Block a user
No description provided.
Delete Branch "fix/notifications-empty-subscriptions"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Problem
Turning off the last notification subscription (going from one opted-in stream to zero) failed with "could not save" and the toggle stuck on. It happened regardless of which stream was last — a clear sign the empty-set case was the trigger, not any particular stream.
Root cause
The backend's
PUT /auth/me/notifications/subscriptionsvalidator requires thestreamsfield:The app serializes request bodies with kotlinx.serialization, whose default
encodeDefaults = falseomits any property equal to its default.NotificationSubscriptionsDto.streamsdefaulted toemptyList(), so an empty set serialized to{}(field dropped) → backend rejected it 400 "Validation failed". Any non-empty set includesstreams, so only the final toggle-off broke.Confirmed against the live API:
{"streams":["news.post"]}200{"streams":[]}200{}(what the app sent)400 Validation failedFix
Remove the default from
NotificationSubscriptionsDto.streams. kotlinx always serializes a property that has no default, so an empty set now goes out as{"streams":[]}. Chosen because the backend contract is correct (the web client always sends the field) and the app was the party violating it — a one-line DTO change with zero blast radius on other request bodies. The sole call site already passesstreamsexplicitly, and the server always returns the field, so response decoding is unaffected. A comment on the field explains why the default must not be re-added.Added a regression test asserting the empty DTO serializes to
{"streams":[]}using the productionJsonconfig (so it fails if the default ever returns).Verification
NotificationsDtoTest.emptySubscriptionsStillSerializeStreamsFieldpasses;:app:testDebugUnitTestand:app:assembleDebuggreen (JDK 21,-Pksp.incremental=false).[].Docs note added in a companion
docs/PR (PLAN.md §11 gotcha).AI-assisted: this change was authored with Claude Code (disclosed per org policy).
🤖 Generated with Claude Code
Turning off the final notification subscription (going from one opted-in stream to zero) failed with "could not save" and the toggle stuck on. The backend's PUT /auth/me/notifications/subscriptions validator requires the `streams` field (body('streams').isArray()), but kotlinx.serialization omits a property equal to its default (encodeDefaults=false). NotificationSubscriptionsDto defaulted `streams` to emptyList(), so an empty set serialized to `{}` and the backend rejected it 400 "Validation failed". Any non-empty set included the field, so only the last toggle-off broke — regardless of which stream it was. Remove the default from NotificationSubscriptionsDto.streams so kotlinx always emits the field; an empty set now sends `{"streams":[]}` (200). The one call site already passes streams explicitly and the server always returns the field, so response decoding is unaffected. Add a regression test asserting the empty DTO serializes to `{"streams":[]}` under the production Json config. Verified on-device (AVD) against the live site and via the live API (`{}` -> 400, `{"streams":[]}` -> 200). Co-Authored-By: Claude <noreply@anthropic.com>