Safe JSON imports for offline web applications
Importing a JSON file looks simple: parse the text and assign the result to application state. That shortcut turns one malformed file into lost work, broken references or stored content that becomes dangerous when rendered. Offline applications need stronger boundaries because there is no server-side validator to catch the mistake later.
Use a transaction, even without a database
Keep the current state untouched until the entire candidate import passes. The pipeline should have explicit stages:
- Read the file with a size limit.
- Parse JSON inside an error boundary.
- Validate the top-level schema and version.
- Normalize fields into the current model.
- Validate relationships between records.
- Sanitize any content that may reach HTML.
- Show an import summary.
- Replace the existing state once, then persist it.
This is a transaction in practical terms: the old state survives every failure before the final commit.
Validate shape before meaning
Start with cheap structural checks. Confirm that the root is an object, required collections are arrays, IDs use the expected type, and bounded strings or numbers stay inside allowed limits. Reject unknown schema versions instead of guessing how to interpret them.
function assertImportShape(value) {
if (!value || typeof value !== "object") throw new Error("Invalid root");
if (value.schemaVersion !== 2) throw new Error("Unsupported version");
if (!Array.isArray(value.projects)) throw new Error("Projects must be an array");
if (value.projects.length > 5000) throw new Error("Import is too large");
}Schema validation should also define whether extra fields are ignored or rejected. Ignoring them helps forward compatibility; rejecting them catches spelling mistakes. Make that choice intentionally.
Check references after normalization
A file can satisfy its field schema and still be unusable. A task may point to a project ID that does not exist, two objects may share an ID, or a parent relationship may form a cycle. Build sets and maps from normalized IDs, then check every reference.
Duplicate IDs deserve a hard failure. Silently keeping the first or last record makes the result dependent on array order. For missing optional references, an explicit repair policy can be acceptable, but the summary must report every repair before the user commits the import.
Treat imported text as untrusted
JSON parsing does not make text safe. If imported names, notes or descriptions are later inserted with innerHTML, they can become executable markup. Prefer text rendering APIs. If rich text is a real requirement, sanitize it with a maintained allowlist-based library and store the original format separately when round-trip fidelity matters.
Test the failures users will actually encounter
A compact fixture set should include valid data, truncated JSON, an unsupported version, excessive record counts, duplicate IDs, missing references, cycles and markup in text fields. Also test persistence failure after validation. If browser storage is full, the application should retain the in-memory state or roll it back according to a documented rule.
A good importer earns trust by being predictable. It explains what is wrong, preserves the existing data and never commits a half-valid result.