actualbudget/actual · error

Failed to parse mapping: ${String(message)}

Error message

Failed to parse mapping: ${String(message)}

What it means

mappingsFromString parses a serialized custom-sync mapping string back into nested Maps. Any exception thrown during parsing (malformed JSON, wrong shape, non-object values) is caught and rethrown wrapped in this error with the original message appended, so the root cause is preserved in the message text.

Source

Thrown at packages/loot-core/src/server/util/custom-sync-mapping.ts:27

      ]),
    ),
  );

export const mappingsFromString = (str: string): Mappings => {
  try {
    const parsed = JSON.parse(str);
    if (typeof parsed !== 'object' || parsed === null) {
      throw new Error('Invalid mapping format');
    }
    return new Map(
      Object.entries(parsed).map(([key, value]) => [
        key,
        new Map(Object.entries(value as object)),
      ]),
    );
  } catch (e) {
    const message = e instanceof Error ? e.message : e;
    throw new Error(`Failed to parse mapping: ${String(message)}`);
  }
};

export const defaultMappings: Mappings = new Map([
  [
    'payment',
    new Map([
      ['date', 'date'],
      ['payee', 'payeeName'],
      ['notes', 'notes'],
    ]),
  ],
  [
    'deposit',
    new Map([
      ['date', 'date'],
      ['payee', 'payeeName'],
      ['notes', 'notes'],

View on GitHub (pinned to d4334cb6e6)

Solutions

  1. Read the wrapped message — the original parse error after 'Failed to parse mapping:' pinpoints the syntax or shape problem.
  2. Validate the mapping string is valid JSON with shape { [category]: { [from]: to } } before calling.
  3. Regenerate the mapping string from the source app instead of hand-editing it.
  4. Fall back to defaultMappings if the custom string cannot be parsed.

Example fix

// before
const mappings = mappingsFromString(process.env.CUSTOM_SYNC_MAPPING);
// after
let mappings;
try {
  mappings = mappingsFromString(process.env.CUSTOM_SYNC_MAPPING);
} catch (e) {
  console.warn('Invalid mapping, using defaults:', e.message);
  mappings = defaultMappings;
}
Defensive patterns

Strategy: try-catch

Validate before calling

let parsed: unknown;
try { parsed = JSON.parse(mappingString); } catch { throw new Error('Mapping is not valid JSON'); }
const shapeOk = parsed && typeof parsed === 'object' &&
  Object.values(parsed).every(v => v && typeof v === 'object' && !Array.isArray(v));
if (!shapeOk) throw new Error('Mapping must be { category: { from: to } }');

Type guard

function isMappingString(s: unknown): s is string {
  if (typeof s !== 'string') return false;
  try {
    const v = JSON.parse(s);
    return !!v && typeof v === 'object' &&
      Object.values(v).every(x => x && typeof x === 'object' && !Array.isArray(x));
  } catch { return false; }
}

Try / catch

try {
  mappings = mappingsFromString(raw);
} catch (e) {
  if (e.message.startsWith('Failed to parse mapping:')) {
    console.warn(`${e.message} — falling back to defaultMappings`);
    mappings = defaultMappings;
  } else { throw e; }
}

Prevention

When it happens

Trigger: Passing a hand-edited or corrupted mapping string (invalid JSON, top-level value that is not an object of objects), an environment variable or config file containing truncated mappings, or a mappings string produced by a different/older schema version.

Common situations: Users hand-editing custom sync mapping config; upgrades changing the mapping schema so old serialized mappings no longer match; shell escaping mangling quotes in an env-var-provided mapping string.

Understand the failure class

Related errors


AI-assisted analysis of actualbudget/actual@d4334cb6e6 (2026-08-29). Data as JSON: /api/errors/e2ef1d0854187876. Report an issue: GitHub.