From 2d2a68d4a797d4f189dce217dbeb1c478b844af9 Mon Sep 17 00:00:00 2001 From: wtclaude Date: Wed, 22 Jul 2026 11:37:53 -0500 Subject: [PATCH] docs(android): note the empty-subscriptions PUT serialization gotcha MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Record the "can't turn off the last notification" class of bug in PLAN.md §11: the PUT /auth/me/notifications/subscriptions validator requires `streams`, so an empty set must serialize as {"streams":[]} not {}. kotlinx.serialization drops a property equal to its default (encodeDefaults=false), so a request DTO field defaulting to emptyList() gets omitted when empty and the server rejects it 400. Generalized to any "replace the full set" PUT/POST whose empty value equals a DTO default. Documents the fix in Android-app fix/notifications-empty-subscriptions. Co-Authored-By: Claude --- android/PLAN.md | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/android/PLAN.md b/android/PLAN.md index 9020d83..c7537d8 100644 --- a/android/PLAN.md +++ b/android/PLAN.md @@ -966,6 +966,17 @@ The ntfy relay is treated as **untrusted infrastructure**, and the design makes write to `/auth/me/notifications/subscriptions`. Tapping a notification deep-links to the relevant screen (§ open item below). +> **Gotcha — the PUT body must always carry `streams`, even when empty.** The backend validator +> requires the field (`body('streams').isArray()`), so an empty set has to be sent as +> `{"streams":[]}`, never `{}`. kotlinx.serialization omits a property equal to its default +> (`encodeDefaults=false`), so a DTO field like `streams: List = emptyList()` gets *dropped* +> from the body when the set is empty — the app then sends `{}` and the server rejects it `400`. +> Symptom: clearing your **last** subscription fails with "could not save" and the toggle sticks +> (any non-empty set still includes the field, so only the final toggle-off breaks). Fix: give the +> request DTO field **no default** so kotlinx always encodes it (Android-app +> `fix/notifications-empty-subscriptions`). The same trap applies to any "replace the full set" +> `PUT`/`POST` whose empty value equals a DTO default — prefer no default on required request fields. + ## 12. Build & CI (Gitea Actions) Builds run on the org's existing self-hosted runners (`runs-on: ubuntu-latest`, same label the other -- 2.49.1