windmill-labs/windmill · error

Cannot convert decimal to json

Error message

Cannot convert decimal to json

What it means

When Windmill reads a PostgreSQL NUMERIC column it decodes it into a rust_decimal::Decimal and then serializes it to JSON with serde_json. serde_json's arbitrary-precision support is not enabled, so Decimal::serialize goes through f64 and fails outright when the decimal is out of the f64 range (e.g. extremely large exponents) or otherwise unserializable, and the code maps any such failure to "Cannot convert decimal to json". Note that values that merely lose precision (past ~15-17 significant digits) do NOT fail here — they are flagged via state.numeric_precision_loss and returned as a possibly-rounded JSON number.

Source

Thrown at backend/windmill-worker/src/pg_executor.rs:1899

        // truncates past ~15-17 significant digits. Switching to JSON String
        // would preserve precision but break any user script doing arithmetic
        // / comparison on numeric column results (`row.amount + 1` becomes
        // string concat, `row.amount > 100` is lexicographic). Left as Number
        // for back-compat. Instead, on the FIRST cell whose decimal
        // representation can't round-trip through f64, we flip
        // `state.numeric_precision_loss` so the caller can emit a single
        // job-log warning recommending a `::text` cast. The check is bounded
        // by `NUMERIC_PRECISION_CHECK_BUDGET` cells (see comment there) and
        // short-circuits on the first lossy value, so the hot path on a
        // numeric-heavy result set is two atomic loads + an early return.
        Type::NUMERIC => get_basic(row, column, column_i, |a: Decimal| {
            if state.should_check_precision() && !decimal_fits_f64_losslessly(&a) {
                state
                    .numeric_precision_loss
                    .store(true, std::sync::atomic::Ordering::Relaxed);
            }
            Ok(serde_json::to_value(a)
                .map_err(|_| anyhow::anyhow!("Cannot convert decimal to json"))?)
        })?,
        Type::FLOAT8 => get_basic(row, column, column_i, |a: f64| f64_to_json_number(a))?,
        Type::BYTEA => get_basic(row, column, column_i, |a: Vec<u8>| {
            Ok(JSONValue::String(format!("\\x{}", hex::encode(a))))
        })?,
        // these types require a custom StringCollector struct as an intermediary (see struct at bottom)
        Type::TS_VECTOR => get_basic(row, column, column_i, |a: StringCollector| {
            Ok(JSONValue::String(a.0))
        })?,
        Type::OID => get_basic(row, column, column_i, |a: u32| {
            Ok(JSONValue::Number(serde_json::Number::from(a)))
        })?,
        // array types
        Type::BOOL_ARRAY => get_array(row, column, column_i, |a: bool| Ok(JSONValue::Bool(a)))?,
        Type::BIT_ARRAY => get_array(row, column, column_i, |a: bit_vec::BitVec| match a.len() {
            1 => Ok(JSONValue::Bool(a.get(0).unwrap())),
            _ => Ok(JSONValue::String(
                a.iter()

View on GitHub (pinned to e474e8803c)

Solutions

  1. Find the offending row/column and fix or clamp the value in Postgres (e.g. SELECT ... WHERE abs(col) > 1e308, then UPDATE with a bounded value).
  2. Cast the column to text in the query (SELECT col::text) so it is returned as a string and bypasses the Decimal-to-JSON path.
  3. Cast to float8 if approximate precision is acceptable (SELECT col::float8), which uses the f64_to_json_number path.
  4. Round the value in SQL (e.g. round(col, 20)) so it fits comfortably in an f64.

Example fix

// before
SELECT amount FROM ledger;
// after (return numeric as text to bypass Decimal->f64 JSON serialization)
SELECT amount::text AS amount FROM ledger;
Defensive patterns

Strategy: validation

Validate before calling

-- run before the job/query to detect values that cannot survive f64 JSON serialization
SELECT id FROM t WHERE col::text ~ 'e[0-9]{7,}' OR abs(col) > 1.7976931348623157e308 LIMIT 1;

Try / catch

// In the calling script, catch the job error and fall back to a text-cast query
try {
  const rows = await wmill.query('SELECT col FROM t');
} catch (e) {
  if (String(e.message).includes('Cannot convert decimal to json')) {
    return await wmill.query('SELECT col::text AS col FROM t');
  }
  throw e;
}

Prevention

When it happens

Trigger: Querying a Postgres NUMERIC column whose value cannot be represented as an f64-backed JSON number — typically values with huge exponents like 1e+1000000000 or NaN-scale numerics produced by numeric overflow in SQL arithmetic — when the row is converted to JSON by pg_cell_to_json_value / postgres_row_to_row_data_with_state.

Common situations: A numeric column accumulating values via repeated multiplication or exponentiation in SQL until the exponent explodes; importing scientific data into NUMERIC; a trigger or computed column producing degenerate numeric values; queries run from a Windmill PostgreSQL script/resource whose result set includes such a cell.

Understand the failure class

Background: "JSON serialization failed", "not JSON serializable", "Failed to serialize": why JSON marshaling errors happen and how to fix them — this error's family across 46 libraries.

Related errors


AI-assisted analysis of windmill-labs/windmill@e474e8803c (2026-09-03). Data as JSON: /api/errors/ffa4922cb9f211ae. Report an issue: GitHub.