ADR-0055: Import rejects invalid backups with a recoverable error
Context
SettingsViewModel.importJson launched an IO coroutine that let
ExportCodec.parseJson's SerializationException escape. Picking a malformed
file ({invalid) crashed the process with a fatal
kotlinx.serialization.json.JsonDecodingException; the user saw no message.
The file-provider read also ran on the main thread, so a read failure could
crash before the coroutine even started. A crash is not a recoverable error,
and reporting a rejected file as "Imported 0 transactions" would be a lie.
Decision
Import validates the payload before any write and reports a typed outcome; the UI turns it into a localized, accessible message.
ExportCodec.parseJsonraisesExportFormatException(carrying anImportError) for empty/blank payloads, malformed or wrong-shape JSON, and versions outside the supported1..CURRENT_VERSIONrange. Older supported versions still parse (ADR-0054).ExportRepository.importJsonreturnsImportResult:Imported(inserted)orRejected(error). It catches onlyExportFormatException; unexpected persistence failures propagate rather than being converted into a false success.- The
SettingsViewModel.importJsonoverload reads the pickedUrionDispatchers.IO, closes the stream, maps read failures toImportError.Unreadable, rethrowsCancellationException, and delivers the result back on the main thread. - The Data screen renders the outcome as a live-region message: an inserted
count on success, a specific error string otherwise. Cancelling the picker
(null
Uri) is a silent no-op.
Consequences
- The exact
{invalidfile shows "Import failed: this isn't a valid AutoBudget backup"; the app stays alive and the ledger is untouched. - Empty, truncated, wrong-shape and unreadable files are handled the same way.
- A backup from a newer app version is rejected explicitly instead of being imported as an empty success.
- Valid retries report the real inserted count; a cancelled picker has no side effects.
Rejected alternatives
- Catch
Exceptionin the repository and returnImported(0)— hides real failures as false success. - Report only a generic "Import failed" — loses the actionable empty/malformed/unsupported/unreadable distinction.
- Keep reading the document on the main thread — leaves a read-failure crash path open.
Amendment (2026-10-19): CSV text cells are neutralized
CSV quoting delimits fields but does not stop a spreadsheet from evaluating a
cell that starts with a formula marker. Every untrusted TEXT column is prefixed
with a single ASCII apostrophe when its first significant character (after
whitespace, Unicode format characters and control characters) is =, +, -,
@ or a full-width variant. Numeric columns (amount, coordinates, timestamps)
are exempt, so a negative amount stays numeric; JSON exports are unchanged. The
apostrophe is part of the stored cell text, and the defence is documented with
its limits (importer version/locale, save-and-reopen) rather than as a
guarantee.