clockworklabs/SpacetimeDB · error

Invalid JSON5 configuration

Error message

Invalid JSON5 configuration

What it means

After masking numeric literals and strings, the parser re-parses the reconstructed JSON5 text into a JSON Value. If that final parse fails, the config is rejected with this opaque message — deliberately vague because JSON5 parser diagnostics may echo source lines containing secrets.

Solutions

  1. Validate the config's JSON5 syntax with a linter/JSON5 parser.
  2. Fix missing commas, braces, or brackets around the edited section.
  3. Temporarily test with `json5.parse()` locally to locate the line, since the CLI hides diagnostics to protect secrets.

Example fix

// before (config)
{ env: { A: 1 B: 2 } }
// after (config)
{ env: { A: 1, B: 2 } }
Defensive patterns

Strategy: validation

Validate before calling

// validate overall JSON5 structure before invoking the CLI
const json5 = require('json5');
json5.parse(fs.readFileSync(configPath, 'utf8')); // throws with line info locally

Try / catch

match result { Err(e) if e.to_string() == "Invalid JSON5 configuration" => { eprintln!("Structural syntax error in config; validate with a local JSON5 parser"); }, Err(e) => return Err(e), Ok(v) => v }

Prevention

When it happens

Trigger: Structurally invalid JSON5 config: missing commas, unbalanced braces/brackets, trailing garbage, or numeric placeholder corruption after masking.

Common situations: Hand-edited configs with syntax mistakes; comment or comma typos; editors leaving stray characters in spacetimedb config files.

Understand the failure class

Related errors


AI-assisted analysis of clockworklabs/SpacetimeDB@eddf9f5014 (2026-09-20). Data as JSON: /api/errors/b613f096c9cdd79a. Report an issue: GitHub.

Appendix: source

Thrown at crates/cli/src/spacetime_config/environment.rs:89

            strings.push(json5::from_str(&content[start..i]).map_err(|_| anyhow::anyhow!("Invalid JSON5 string"))?);
        }
    }
    // Check decoded strings as well: Unicode escapes must not manufacture an
    // internal marker and cause a string to be interpreted as a number.
    let mut prefix = "__spacetime_numeric_".to_owned();
    while content.contains(&prefix) || strings.iter().any(|s| s.contains(&prefix)) {
        prefix.push('_');
    }
    let mut text = String::with_capacity(content.len());
    let mut previous = 0;
    for (index, (start, end)) in replacements.into_iter().enumerate() {
        text.push_str(&content[previous..start]);
        text.push_str(&format!("\"{prefix}{index}\""));
        previous = end;
    }
    text.push_str(&content[previous..]);
    // Parser diagnostics may quote the source line, which can contain secrets.
    let mut value: Value = json5::from_str(&text).map_err(|_| anyhow::anyhow!("Invalid JSON5 configuration"))?;
    restore(&mut value, &prefix, &numbers, None)?;
    Ok(value)
}

/// Deserialize through JSON text so Serde's flattened-field buffer does not
/// receive visit_u128 from Value's deserializer. That buffer cannot represent
/// u128, while the arbitrary-precision JSON parser preserves its decimal token.
/// Diagnostics discard values while retaining a bounded, ordinary unknown field
/// name, which is useful for correcting misspelled configuration options.
pub(super) fn decode_config(value: Value) -> anyhow::Result<super::SpacetimeConfig> {
    let encoded = serde_json::to_vec(&value).map_err(|_| anyhow::anyhow!("Invalid configuration structure"))?;
    serde_json::from_slice(&encoded).map_err(|error| {
        let diagnostic = error.to_string();
        if let Some((field, suffix)) = diagnostic
            .strip_prefix("unknown field `")
            .and_then(|message| message.split_once('`'))
            && suffix.starts_with(", expected ")
            && !field.is_empty()

View on GitHub (pinned to eddf9f5014)