Skill 05 · Clickhouse Js Node Coding
Subchapter 5.4
reference/custom-json.mdMarkdown7 KBView on GitHub
parse / stringifyRequires: client
>= 1.14.0(configurablejson.parseandjson.stringify). Earlier versions cannot swap the JSON implementation.
When the user wants UInt64/Int64 values back as BigInt:
json.parse / json.stringify requires
@clickhouse/client >= 1.14.0.createClient({ json: { parse, stringify } }) option,
usually with json-bigint and useNativeBigInt: true.output_format_json_quote_64bit_integers: 0 so the server
emits unquoted 64-bit integers that the parser can turn into BigInt.output_format_json_quote_64bit_integers: 0 is the default
since ClickHouse 25.8, but setting it explicitly is useful for older
servers or portable examples.Number / parseInt / parseFloat loses
precision above Number.MAX_SAFE_INTEGER.The default JSON.stringify / JSON.parse:
BigInt.Date.prototype.toJSON() (ISO string) — fine for DateTime with
date_time_input_format: 'best_effort', surprising in some workflows.A custom { parse, stringify } lets you plug in JSONBig,
safe-stable-stringify, your own BigInt-aware serializer, etc.
import { createClient } from "@clickhouse/client";
const valueSerializer = (value: unknown): unknown => {
// Serialize Date as a UNIX millis number (instead of toJSON's ISO string)
if (value instanceof Date) {
return value.getTime();
}
// Serialize BigInt as a string so JSON.stringify won't throw
if (typeof value === "bigint") {
return value.toString();
}
if (Array.isArray(value)) {
return value.map(valueSerializer);
}
if (typeof value === "object" && value !== null) {
return Object.fromEntries(
Object.entries(value).map(([k, v]) => [k, valueSerializer(v)]),
);
}
return value;
};
const client = createClient({
json: {
parse: JSON.parse, // use default parsing
stringify: (obj: unknown) => JSON.stringify(valueSerializer(obj)),
},
});
await client.command({
query: `
CREATE OR REPLACE TABLE inserts_custom_json_handling
(id UInt64, dt DateTime64(3, 'UTC'))
ENGINE MergeTree
ORDER BY id
`,
});
await client.insert({
table: "inserts_custom_json_handling",
format: "JSONEachRow",
values: [
{
id: BigInt("250000000000000200"), // serialized as a string
dt: new Date(), // serialized as ms since epoch
},
],
});
await client.close();The custom
valueSerializerruns beforeJSON.stringify, so values are transformed before the standard hooks (Date.prototype.toJSON, objecttoJSON()methods, etc.) ever run.
If you want UInt64/Int64 to come back as BigInts (instead of strings
or precision-lossy numbers), plug in a BigInt-aware parser such as
json-bigint (opens in a new tab):
import { createClient } from "@clickhouse/client";
import JSONBig from "json-bigint";
const bigJson = JSONBig({ useNativeBigInt: true });
const client = createClient({
json: {
parse: bigJson.parse,
stringify: bigJson.stringify,
},
clickhouse_settings: {
output_format_json_quote_64bit_integers: 0,
},
});output_format_json_quote_64bit_integers: 0 is the default since
ClickHouse 25.8; setting it explicitly is useful for older servers and
makes the example self-contained. With it off, the server emits unquoted
64-bit integers that json-bigint parses straight to BigInt. The
json option applies to both outgoing JSON bodies and incoming
JSON-format responses.
If adding a dependency is awkward (locked lockfile, restricted environment,
or you just don’t want to pull in json-bigint), you can plug in a
hand-rolled reviver. This uses the context.source argument that
JSON.parse revivers gained in Node >= 20.16 / >= 21.7, so the raw
numeric literal is available before it’s coerced to a JS number:
import { createClient } from "@clickhouse/client";
const parseBigInt = (text: string) =>
JSON.parse(text, function (key, value, context) {
if (key.endsWith("__bigint")) {
return BigInt(context.source);
}
return value;
});
const client = createClient({
json: {
parse: parseBigInt,
stringify: JSON.stringify, // use default stringify
},
clickhouse_settings: { output_format_json_quote_64bit_integers: 0 },
});
const rs = await client.query({
query: "SELECT toUInt64(250000000000000200) AS id__bigint",
});
const { data } = await rs.json();
console.log(data[0].id__bigint); // 250000000000000200
await client.close();Trade-offs versus json-bigint:
BigInt.>= 20.16 / >= 21.7 for the reviver context.source.
On older Node, prefer json-bigint or upgrade Node.stringify still uses default JSON.stringify, which throws
on BigInt. Pair with the valueSerializer pattern above if your
inserts contain BigInt values.json.parse only. That only affects reading JSON responses;
outgoing JSON bodies use json.stringify. If you want consistent custom
handling in both directions, generally provide a matching stringify too
or a throwing serializer that prevents mismatches.bigint handling in stringify. Default JSON.stringify
throws on BigInt; if your data ever contains one, the insert will fail
with TypeError: Do not know how to serialize a BigInt.< 1.14.0. The json option doesn’t exist; you’ll
need to convert values manually before calling insert() / query() (or
upgrade).Number. JavaScript’s number type has
only 53 bits of mantissa — values above Number.MAX_SAFE_INTEGER (2^53 − 1)
are silently rounded. Do not try to fix precision loss by calling
Number(), parseInt(), or parseFloat() on the value. The correct fix
is a BigInt-aware parser (shown above), not a lossy cast.BigInt and
others are number, your app code needs to handle both types. Otherwise
JavaScript will throw a TypeError: Cannot mix BigInt and other types.