Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

JSON functions

JSON documents travel as text: a JSON column, a nested Parquet or Arrow column, a REST response’s nested object. These functions build and take them apart; CROSS JOIN UNNEST(JSON_EACH(col)) turns an array into rows (see Queries).

Building a document

JSON_OBJECT(k1, v1, …), JSON_ARRAY(v1, …) — build a JSON document, for a request body or a nested column: numbers and booleans as themselves, text as a JSON string, a null as null (the result itself is never null), and a NaN or infinity as null too, since JSON has neither. Text that is a JSON object or array — another json_object, or what json_get returned — goes in as that object or array, so json_object('id', id, 'tags', json_array(a, b)) nests. A null key fails the statement.

Reading a value

JSON_GET(doc, path) — one value out of a JSON document, as text: path is a.b, a[0].b or a.0.b (a leading $. is allowed). Strings come back unquoted, objects and arrays as JSON text; a missing key, an index past the end or a JSON null is null. A cell that is not JSON is an error, not a null. CAST(json_get(doc, 'n') AS INT) for a typed value.

Lambdas over an array

JSON_FILTER(arr, x -> cond), JSON_TRANSFORM(arr, x -> value), JSON_ANY(arr, x -> cond), JSON_ALL(arr, x -> cond) — work on a JSON array in place, without UNNEST and re-aggregating: the lambda’s body is evaluated once per element with its parameter bound to it. json_filter keeps the elements whose condition is true, as written; json_transform returns the array of the body’s values; json_any / json_all whether the condition is true for some / every element (an empty array: false / true). A nested Parquet list reads as exactly such an array.

SELECT id,
       json_filter(tags, t -> t LIKE 'vip%')                    AS vip_tags,
       json_transform(items, i -> json_get(i, 'sku'))           AS skus,
       json_any(items, i -> CAST(json_get(i, 'qty') AS INT) = 0) AS has_empty_line
FROM 'orders.parquet'
WHERE json_any(tags, t -> t = 'new');

An element is the parameter as itself — a number is a number (x > 5), a string a string, true/false a BOOL — and an object or array is its JSON text, which json_get and these functions take apart (lambdas nest). The body may name the row’s columns too (t -> t = region); the parameter shadows a column of its name. In json_transform’s result, text that is a JSON object or array (what json_get returns for one) goes in as that object or array, any other text as a string. An element the body cannot compare — a number against text, in an array that mixes kinds — counts as null (the condition is not true; json_transform puts null); a failing CAST still fails the run. A null cell is null; a cell that is JSON but not an array is an error. A lambda is an argument of these functions and json_reduce only, and never descends to a SQL source — the row is filtered here, while the rest of its WHERE still descends. Written (x, i) -> …, the lambda also gets the element’s position, from 0 as in a json_get path.

Folding an array

JSON_REDUCE(arr, initial, (acc, x) -> value) — a fold: acc starts at initial and becomes the body’s value at each element in turn; the result is the last one, initial for an empty array and null for a null cell. (acc, x, i) -> … adds the position. The accumulator keeps initial’s type, widened to what the body returns from it (a DECIMAL total stays a DECIMAL, a date a date); a FLOAT element arriving in an INT or DECIMAL total stops the statement rather than being cut short — start from 0.0 to sum floats. An element the body cannot compare stops it too, where json_transform would put null: a null mid-fold would quietly undo everything folded before it.

Arrays

CHARS(s) is a string’s characters as a JSON array, JSON_RANGE([start,] stop) the integers start (default 0) up to but not including stop, JSON_LENGTH(arr) an array’s element count, JSON_SLICE(arr, start[, stop]) its elements from start to before stop (from 0; a negative bound counts from the end, as in Python) and JSON_CONCAT(a, b, …) the arrays joined (null when any is). Together with json_reduce they make a per-character algorithm a function — a CNPJ check digit, letters included (the July 2026 alphanumeric CNPJ counts a character as its code point minus 48):

CREATE FUNCTION cnpj_dv(base, weights) AS
  LET s = json_reduce(chars(base), 0, (acc, c, i) ->
            acc + (ascii(c) - 48) * CAST(json_get(weights, CAST(i AS STRING)) AS INT))
  IN CASE WHEN s % 11 < 2 THEN 0 ELSE 11 - s % 11 END;

CREATE FUNCTION cnpj_valid(raw) AS
  LET d = translate(upper(raw), './-', '')
  IN length(d) = 14
     AND cnpj_dv(left(d, 12), '[5,4,3,2,9,8,7,6,5,4,3,2]') = ascii(substr(d, 13, 1)) - 48
     AND cnpj_dv(left(d, 13), '[6,5,4,3,2,9,8,7,6,5,4,3,2]') = ascii(substr(d, 14, 1)) - 48;
-- cnpj_valid('11.222.333/0001-81') and cnpj_valid('12.ABC.345/01DE-35') are true