Skip to content

fix: preserve model JSON in backups#1224

Merged
GT-610 merged 2 commits into
mainfrom
fix/backup-v2-json-serialization
Jul 5, 2026
Merged

fix: preserve model JSON in backups#1224
GT-610 merged 2 commits into
mainfrom
fix/backup-v2-json-serialization

Conversation

@GT-610

@GT-610 GT-610 commented Jul 5, 2026

Copy link
Copy Markdown
Collaborator

#1223

Summary by CodeRabbit

  • Bug Fixes
    • Improved backup and restore reliability by performing stricter typed-store validation when loading and before merging backups.
    • Backup JSON encoding is now more robust: unsupported values fail fast with a clear UnsupportedError instead of being silently stringified.
    • Restore now rejects corrupted typed-store entry shapes with a FormatException, while preserving required internal metadata fields.
  • Tests
    • Added unit coverage for JSON encoding behavior and restore-time validation, including failure scenarios.

@coderabbitai

coderabbitai Bot commented Jul 5, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: a3fb5b89-5a57-49cb-8715-92efd6b1b807

📥 Commits

Reviewing files that changed from the base of the PR and between 18bc7d2 and a56c402.

📒 Files selected for processing (2)
  • lib/data/model/app/bak/backup2.dart
  • test/backup_v2_test.dart
🚧 Files skipped from review as they are similar to previous changes (1)
  • test/backup_v2_test.dart

📝 Walkthrough

Walkthrough

BackupV2 in lib/data/model/app/bak/backup2.dart now validates restorable typed stores (spis, snippets, keys) after JSON deserialization and before merging, throwing FormatException for corrupted non-internal entries while skipping internal metadata keys. Backup serialization now goes through a new toJsonString() method that wraps json.encode, and _toEncodable now throws UnsupportedError for objects lacking toJson() instead of logging and falling back to toString(). test/backup_v2_test.dart adds unit tests covering serialization and restore validation behavior.

Possibly related PRs

  • lollipopkit/flutter_server_box#1196: Modifies the same BackupV2.backup JSON encoding path and _toEncodable helper in lib/data/model/app/bak/backup2.dart, overlapping directly with this PR's serialization changes.

Suggested labels: bug, test

Suggested reviewers: lollipopkit

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the main change: preserving model JSON in backups.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/backup-v2-json-serialization

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (2)
test/backup_v2_test.dart (1)

95-112: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

Test doesn't actually verify internal-key skipping.

_validateRestorableStore skips an entry if _isInternalStoreKey(entry.key) or entry.value is Map. Here '__lkpt_lastUpdateTs' is given a Map value ({'server': 1}), so the entry would pass validation regardless of whether it's recognized as an internal key — the test doesn't exercise the internal-key branch at all. To actually prove internal keys are skipped, use a non-Map value (which would otherwise trigger the FormatException) for the internal key.

✅ Proposed fix to actually exercise the internal-key skip branch
     test('allows internal metadata in typed stores', () {
       final raw = json.encode({
         'version': BackupV2.formatVer,
         'date': 1,
         'spis': {
-          '__lkpt_lastUpdateTs': {'server': 1},
+          '__lkpt_lastUpdateTs': 1234567890,
         },
         'snippets': {},
         'keys': {},
         'container': {},
         'history': {},
         'settings': {},
       });

       final backup = BackupV2.fromJsonString(raw);

-      expect(backup.spis['__lkpt_lastUpdateTs'], {'server': 1});
+      expect(backup.spis['__lkpt_lastUpdateTs'], 1234567890);
     });
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@test/backup_v2_test.dart` around lines 95 - 112, The test in
BackupV2.fromJsonString is not actually exercising the internal-key skip branch
because the value for '__lkpt_lastUpdateTs' is a Map, which already bypasses
validation. Update the test data so the internal store key uses a non-Map value
that would normally fail _validateRestorableStore, then assert BackupV2 still
parses it successfully; this should specifically verify the _isInternalStoreKey
path rather than the generic Map exception.
lib/data/model/app/bak/backup2.dart (1)

157-167: 🩺 Stability & Availability | 🔵 Trivial | ⚖️ Poor tradeoff

Broad NoSuchMethodError catch can mask unrelated bugs inside toJson().

(value as dynamic).toJson() catches any NoSuchMethodError, not just the case where toJson() itself is missing. If a type does implement toJson() but that method internally triggers a NoSuchMethodError (e.g. calling a dynamic method on a nested field, or a null dynamic receiver), this handler will misreport it as "missing toJson()" and swallow the real stack trace/cause, making the actual bug harder to diagnose.

Consider checking for a shared toJson capability more precisely (e.g., a marker interface Jsonable { Map<String, dynamic> toJson(); } on the model types actually passed here, or checking value is Map/value is Function gates before falling back to dynamic dispatch) rather than relying on catching NoSuchMethodError as a proxy for "no method".

♻️ Possible improvement using an interface instead of dynamic dispatch
-Object? _toEncodable(Object? value) {
-  if (value is Enum) return value.name;
-
-  try {
-    return (value as dynamic).toJson();
-  } on NoSuchMethodError {
-    throw UnsupportedError(
-      'Cannot JSON-encode ${value.runtimeType}: missing toJson()',
-    );
-  }
-}
+Object? _toEncodable(Object? value) {
+  if (value is Enum) return value.name;
+  if (value is JsonEncodable) return value.toJson();
+
+  throw UnsupportedError(
+    'Cannot JSON-encode ${value.runtimeType}: missing toJson()',
+  );
+}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@lib/data/model/app/bak/backup2.dart` around lines 157 - 167, The _toEncodable
helper is catching any NoSuchMethodError from dynamic toJson() calls, which can
hide real bugs inside valid toJson implementations. Update _toEncodable in
backup2.dart to avoid using NoSuchMethodError as a proxy for “missing toJson()”;
instead, use a more explicit capability check such as a shared Jsonable
interface for model types, or otherwise narrow the dynamic fallback so only
unsupported values reach the UnsupportedError path. Keep the Enum.name handling
intact and preserve the original stack trace for genuine toJson() failures.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@lib/data/model/app/bak/backup2.dart`:
- Around line 157-167: The _toEncodable helper is catching any NoSuchMethodError
from dynamic toJson() calls, which can hide real bugs inside valid toJson
implementations. Update _toEncodable in backup2.dart to avoid using
NoSuchMethodError as a proxy for “missing toJson()”; instead, use a more
explicit capability check such as a shared Jsonable interface for model types,
or otherwise narrow the dynamic fallback so only unsupported values reach the
UnsupportedError path. Keep the Enum.name handling intact and preserve the
original stack trace for genuine toJson() failures.

In `@test/backup_v2_test.dart`:
- Around line 95-112: The test in BackupV2.fromJsonString is not actually
exercising the internal-key skip branch because the value for
'__lkpt_lastUpdateTs' is a Map, which already bypasses validation. Update the
test data so the internal store key uses a non-Map value that would normally
fail _validateRestorableStore, then assert BackupV2 still parses it
successfully; this should specifically verify the _isInternalStoreKey path
rather than the generic Map exception.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: c01d2ba3-453d-47b6-8833-435dbdafbaef

📥 Commits

Reviewing files that changed from the base of the PR and between 1e13eee and 18bc7d2.

📒 Files selected for processing (2)
  • lib/data/model/app/bak/backup2.dart
  • test/backup_v2_test.dart

coderabbitai[bot]
coderabbitai Bot previously approved these changes Jul 5, 2026
@GT-610 GT-610 merged commit 17b429c into main Jul 5, 2026
4 checks passed
@GT-610 GT-610 deleted the fix/backup-v2-json-serialization branch July 5, 2026 15:50
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.

1 participant