{"record":{"id":"ffa4922cb9f211ae","repo":"windmill-labs/windmill","slug":"cannot-convert-decimal-to-json","errorCode":null,"errorMessage":"Cannot convert decimal to json","messagePattern":"Cannot convert decimal to json","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"backend/windmill-worker/src/pg_executor.rs","lineNumber":1899,"sourceCode":"        // truncates past ~15-17 significant digits. Switching to JSON String\n        // would preserve precision but break any user script doing arithmetic\n        // / comparison on numeric column results (`row.amount + 1` becomes\n        // string concat, `row.amount > 100` is lexicographic). Left as Number\n        // for back-compat. Instead, on the FIRST cell whose decimal\n        // representation can't round-trip through f64, we flip\n        // `state.numeric_precision_loss` so the caller can emit a single\n        // job-log warning recommending a `::text` cast. The check is bounded\n        // by `NUMERIC_PRECISION_CHECK_BUDGET` cells (see comment there) and\n        // short-circuits on the first lossy value, so the hot path on a\n        // numeric-heavy result set is two atomic loads + an early return.\n        Type::NUMERIC => get_basic(row, column, column_i, |a: Decimal| {\n            if state.should_check_precision() && !decimal_fits_f64_losslessly(&a) {\n                state\n                    .numeric_precision_loss\n                    .store(true, std::sync::atomic::Ordering::Relaxed);\n            }\n            Ok(serde_json::to_value(a)\n                .map_err(|_| anyhow::anyhow!(\"Cannot convert decimal to json\"))?)\n        })?,\n        Type::FLOAT8 => get_basic(row, column, column_i, |a: f64| f64_to_json_number(a))?,\n        Type::BYTEA => get_basic(row, column, column_i, |a: Vec<u8>| {\n            Ok(JSONValue::String(format!(\"\\\\x{}\", hex::encode(a))))\n        })?,\n        // these types require a custom StringCollector struct as an intermediary (see struct at bottom)\n        Type::TS_VECTOR => get_basic(row, column, column_i, |a: StringCollector| {\n            Ok(JSONValue::String(a.0))\n        })?,\n        Type::OID => get_basic(row, column, column_i, |a: u32| {\n            Ok(JSONValue::Number(serde_json::Number::from(a)))\n        })?,\n        // array types\n        Type::BOOL_ARRAY => get_array(row, column, column_i, |a: bool| Ok(JSONValue::Bool(a)))?,\n        Type::BIT_ARRAY => get_array(row, column, column_i, |a: bit_vec::BitVec| match a.len() {\n            1 => Ok(JSONValue::Bool(a.get(0).unwrap())),\n            _ => Ok(JSONValue::String(\n                a.iter()","sourceCodeStart":1881,"sourceCodeEnd":1917,"githubUrl":"https://github.com/windmill-labs/windmill/blob/e474e8803ce2ff5c2df09a58dab51d45f5c922ca/backend/windmill-worker/src/pg_executor.rs#L1881-L1917","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","solutions":["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).","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.","Cast to float8 if approximate precision is acceptable (SELECT col::float8), which uses the f64_to_json_number path.","Round the value in SQL (e.g. round(col, 20)) so it fits comfortably in an f64."],"exampleFix":"// before\nSELECT amount FROM ledger;\n// after (return numeric as text to bypass Decimal->f64 JSON serialization)\nSELECT amount::text AS amount FROM ledger;","handlingStrategy":"validation","validationCode":"-- run before the job/query to detect values that cannot survive f64 JSON serialization\nSELECT id FROM t WHERE col::text ~ 'e[0-9]{7,}' OR abs(col) > 1.7976931348623157e308 LIMIT 1;","typeGuard":null,"tryCatchPattern":"// In the calling script, catch the job error and fall back to a text-cast query\ntry {\n  const rows = await wmill.query('SELECT col FROM t');\n} catch (e) {\n  if (String(e.message).includes('Cannot convert decimal to json')) {\n    return await wmill.query('SELECT col::text AS col FROM t');\n  }\n  throw e;\n}","preventionTips":["Cast extreme NUMERIC columns to ::text or ::float8 in queries meant for job consumption","Add CHECK constraints (abs(col) < 1e308) to bound numeric accumulation","Round aggregates in SQL before returning them to scripts"],"tags":["postgres","json-serialization","numeric-overflow"],"backgroundTag":"json-serialization-failed","analyzedSha":"e474e8803ce2ff5c2df09a58dab51d45f5c922ca","analyzedAt":"2026-09-03T12:38:19.024Z","contentChangedAt":"2026-09-03T12:38:19.024Z","schemaVersion":2},"datasetVersion":"2026-09-08T05:18:18.240Z"}