pcottle/learnGitBranching · error · Error

level progress must be a JSON object

Error message

level progress must be a JSON object

What it means

LevelStore._normalizeImportedSolvedMap() validates the top-level shape of level-progress JSON passed to importLevelProgress(). It rejects null, non-objects, and arrays, throwing a plain Error('level progress must be a JSON object'). The expected shape is a map of levelID -> true or {solved, best}.

Source

Thrown at src/js/stores/LevelStore.js:65

} catch (e) {
  console.warn('local storage failed', e);
}

function _syncToStorage() {
  try {
    localStorage.setItem(SOLVED_MAP_STORAGE_KEY, JSON.stringify(_solvedMap));
  } catch (e) {
    console.warn('local storage failed on set', e);
  }
}

function _cloneSolvedMap() {
  return JSON.parse(JSON.stringify(_solvedMap));
}

function _normalizeImportedSolvedMap(progress) {
  if (!progress || typeof progress !== 'object' || Array.isArray(progress)) {
    throw new Error('level progress must be a JSON object');
  }

  var normalized = {};
  Object.keys(progress).forEach(function(levelID) {
    var levelData = progress[levelID];

    // Backwards compatibility with the old storage format.
    if (levelData === true) {
      normalized[levelID] = true;
      return;
    }

    if (!levelData || typeof levelData !== 'object' || Array.isArray(levelData)) {
      throw new Error('invalid progress data for level: ' + levelID);
    }

    normalized[levelID] = {
      solved: levelData.solved === true,

View on GitHub (pinned to 5b09d0ff96)

Solutions

  1. Re-shape the imported JSON so the root is an object keyed by level ID, e.g. {"intro-1": true}
  2. If exporting progress yourself, dump the same structure _cloneSolvedMap() produces
  3. Wrap importLevelProgress in try/catch and surface a friendly message for malformed files

Example fix

// before
importLevelProgress(["intro-1"]);
// after
importLevelProgress({"intro-1": true});
Defensive patterns

Strategy: type-guard

Validate before calling

function isProgressMap(v) {
  return Boolean(v) && typeof v === 'object' && !Array.isArray(v);
}
if (!isProgressMap(data)) { reject('expected a JSON object keyed by level ID'); }

Type guard

function isProgressMap(v) {
  return v !== null && typeof v === 'object' && !Array.isArray(v);
}

Try / catch

try { importLevelProgress(data); } catch (e) { if (/must be a JSON object/.test(e.message)) { showError('invalid save file'); } else throw e; }

Prevention

When it happens

Trigger: Calling importLevelProgress() with a JSON array of level IDs, a string/number, or null/undefined — e.g. importing a save file whose root is [{...}] instead of {id: ...}.

Common situations: Hand-edited or machine-exported save files with the wrong root structure; mixing up the export format (array of records vs keyed map); passing JSON.parse output of an empty file (null).

Understand the failure class

Background: Schema validation failed / invalid input schema: payload rejected because its shape doesn't match the expected schema — this error's family across 28 libraries.

Related errors


AI-assisted analysis of pcottle/learnGitBranching@5b09d0ff96 (2026-08-27). Data as JSON: /api/errors/a4d70311ae1c2f8d. Report an issue: GitHub.