Skip to content

fix: [ANDROSDK-2376] store the JSON value of a dataStore entry - #2697

Open
andresmr wants to merge 2 commits into
developfrom
ANDROSDK-2376
Open

fix: [ANDROSDK-2376] store the JSON value of a dataStore entry#2697
andresmr wants to merge 2 commits into
developfrom
ANDROSDK-2376

Conversation

@andresmr

@andresmr andresmr commented Aug 13, 2026

Copy link
Copy Markdown
Contributor

DataStoreEntryDTO.toDomain called toString() on value, a JsonWrapper value class, so every entry read from the server dataStore was persisted as JsonWrapper(json={...}) and could not be deserialized by any consumer. The same call turned a null wrapper into the literal string "null", making an absent value indistinguishable from a JSON null. Serializing the wrapped JsonElement and preserving null fixes both.

Both read paths — getNamespaceValues38 and getNamespaceKeyValue — route through toDomain, so both were affected, and every namespace is: ANDROID_SETTINGS_APP is stored corrupted too, but the bug is masked there because settingModule() parses that namespace into its own typed tables rather than reading the generic dataStore. That is why it went unnoticed since ANDROSDK-2016 — no consumer read the server dataStore raw until now. The new mapping tests fail on 6 of 7 cases against the previous implementation, and the fix was verified end to end against a local DHIS2 instance with the Android Capture App plugin system reading dhis2AndroidPlugins.

Migration 181: repairing installations that already downloaded corrupted values

Downloaded entries were stored as JsonWrapper(json={...}) instead of the JSON payload, and an absent value was stored as the literal string "null". Only downloaded rows are affected, so the repair is restricted to SYNCED entries and leaves anything written locally untouched.

The two statements are order-dependent:

  1. UPDATE DataStore SET value = NULL WHERE syncState = 'SYNCED' AND value = 'null';

    Restores an absent value to NULL. This must run before the unwrapping below: a genuine JSON null was stored as JsonWrapper(json=null), and unwrapping it first would leave the string "null", which this statement would then wrongly turn into a NULL.

  2. UPDATE DataStore SET value = substr(value, 18, length(value) - 18) WHERE syncState = 'SYNCED' AND value LIKE 'JsonWrapper(json=%)';

    Strips the 17-character "JsonWrapper(json=" prefix and the trailing ")" to recover the payload.

DataBaseMigrationShould.repair_data_store_values_wrapped_by_json_wrapper_to_string_in_migration_181 pins both statements and their ordering against real SQLite: it migrates to 180, seeds a wrapped object, a wrapped array, a genuine JSON null, an absent value and a locally edited row, applies migration 181 and asserts each outcome.

Related task: ANDROSDK-2376

andresmr and others added 2 commits August 13, 2026 14:05
DataStoreEntryDTO.toDomain called toString() on `value`, a JsonWrapper value
class, so every entry read from the server dataStore was persisted as
"JsonWrapper(json={...})" and could not be deserialized by any consumer.

The same call also turned a null wrapper into the literal string "null", making
an absent value indistinguishable from a JSON null. Serializing the wrapped
JsonElement fixes both.

Both read paths, getNamespaceValues38 and getNamespaceKeyValue, route through
toDomain, so both were affected. Every namespace is, including
ANDROID_SETTINGS_APP, where the bug is masked because settingModule() parses
that namespace into its own typed tables rather than reading the generic
dataStore.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@taridepaco
taridepaco marked this pull request as ready for review August 14, 2026 07:27
@sonarqubecloud

Copy link
Copy Markdown

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.

2 participants