cube-js/cube · error

--data must be a JSON object

Error message

--data must be a JSON object

What it means

parse_data normalizes the --data flag value (string or already-parsed) into a serde_json Map. After deserializing the raw text as JSON (which would first fail with "--data is not valid JSON" if malformed), it requires the top-level value to be a JSON object. Scalars, arrays, or other non-object values are rejected because every request body the CLI builds is an object.

Source

Thrown at rust/cube-cli/src/util.rs:28

/// Accepts inline JSON (`'{"name": "x"}'`), `@path/to/file.json`, or `-`
/// to read from stdin — the same convention as `gh api` / `curl -d`.
pub fn parse_data(data: Option<&str>) -> Result<Map<String, Value>> {
    let Some(data) = data else {
        return Ok(Map::new());
    };
    let raw = if data == "-" {
        let mut buf = String::new();
        std::io::stdin().read_to_string(&mut buf)?;
        buf
    } else if let Some(path) = data.strip_prefix('@') {
        std::fs::read_to_string(path).with_context(|| format!("failed to read {path}"))?
    } else {
        data.to_string()
    };
    let value: Value = serde_json::from_str(&raw).context("--data is not valid JSON")?;
    match value {
        Value::Object(map) => Ok(map),
        _ => bail!("--data must be a JSON object"),
    }
}

/// Insert a flag value into a JSON body if it was provided on the CLI.
pub fn set<T: serde::Serialize>(body: &mut Map<String, Value>, key: &str, value: &Option<T>) {
    if let Some(v) = value {
        body.insert(key.to_string(), serde_json::to_value(v).unwrap());
    }
}

/// Push a query parameter if the flag was provided.
pub fn push<T: ToString>(query: &mut Query, key: &str, value: &Option<T>) {
    if let Some(v) = value {
        query.push((key.to_string(), v.to_string()));
    }
}

/// How one endpoint implements the deprecated offset paging it still accepts,

View on GitHub (pinned to 7d981676b3)

Solutions

  1. Wrap the value in a JSON object, e.g. --data '{"key": "value"}'
  2. If you meant to send a list, find the field it belongs under: --data '{"records": [...]}'
  3. Check shell quoting — the value must arrive as one argument containing valid JSON

Example fix

// before
cube load --data '[{"id":1},{"id":2}]'

// after
cube load --data '{"records": [{"id":1},{"id":2}]}'
Defensive patterns

Strategy: validation

Validate before calling

// validate --data before invoking
class IsObject {}
function isJsonObject(s) {
  try { const v = JSON.parse(s); return v !== null && typeof v === 'object' && !Array.isArray(v); }
  catch { return false; }
}
if (!isJsonObject(dataArg)) throw new Error('--data must be a JSON object');

Type guard

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

Try / catch

match parse_data(&data_arg) {
    Err(e) if e.to_string().contains("--data must be a JSON object") => {
        eprintln!("Pass a top-level JSON object, e.g. --data '{{\"key\": \"value\"}}'");
    }
    Err(e) => return Err(e),
    Ok(map) => { /* use map */ }
}

Prevention

When it happens

Trigger: Passing --data a top-level JSON array (`'[1,2]'`), a scalar (`'42'`, `'"str"'`), or any non-object value. Valid inline JSON that is not an object triggers exactly this bail.

Common situations: Users passing a JSON array of records intending bulk upload; quoting issues causing the shell to hand the flag a scalar; copy-pasted payloads whose root is an array.

Related errors


AI-assisted analysis of cube-js/cube@7d981676b3 (2026-09-02). Data as JSON: /api/errors/c70e1a6626655669. Report an issue: GitHub.