BigQuery JSON functions: JSON_VALUE, JSON_QUERY and PARSE_JSON explained
JSON_VALUE returns a scalar as a string, JSON_QUERY returns an object or array as JSON, and their _ARRAY versions return arrays you can UNNEST. This guide runs one JSON document through each, compares them with the older JSON_EXTRACT functions, and explains the 'must be a constant expression' error.
To read JSON in BigQuery you need four functions, and the choice depends on what sits at the path:
| At the path there is | Use | Returns |
|---|---|---|
| a string, number or boolean | JSON_VALUE |
STRING |
| an object or an array, kept as JSON | JSON_QUERY |
JSON text |
| an array of scalars | JSON_VALUE_ARRAY |
ARRAY<STRING> |
| an array of objects | JSON_QUERY_ARRAY |
array of JSON |
JSON_VALUE on an object returns NULL, and so does a path that does not exist. Most “my JSON function returns NULL” questions end there.
Every result below is from a run in BigQuery on 6 October 2026, on this one document:
{
"id": 7,
"user": {"name": "ana", "tags": ["a", "b"]},
"items": [
{"sku": "x1", "qty": 2},
{"sku": "y2", "qty": 1}
],
"note": null,
"a.b": "dot"
}
In the queries, j is a STRING column holding it.
Note: Originally published in April 2025. Fully updated in October 2026 with one document used throughout, every result re-run, and a section on the constant expression error.
JSON_VALUE: get a scalar
SELECT
JSON_VALUE(j, '$.user.name'), -- ana
JSON_VALUE(j, '$.id'), -- 7
JSON_VALUE(j, '$.items[0].sku'), -- x1
JSON_VALUE(j, '$.user'), -- NULL
JSON_VALUE(j, '$.missing'), -- NULL
JSON_VALUE(j, '$.note'); -- NULL
Three things to take from that:
- The result is always a
STRING.$.idcomes back as the text7, not the number. Cast it:SAFE_CAST(JSON_VALUE(j, '$.id') AS INT64). - An object returns
NULL.$.useris an object, soJSON_VALUEhas nothing scalar to return. - You cannot tell a missing key from a JSON
null. Both return SQLNULL.
Array elements are addressed with [0], counting from zero.
JSON_QUERY: get an object or array
JSON_QUERY: get an object or array
SELECT
JSON_QUERY(j, '$.user'),
-- {"name":"ana","tags":["a","b"]}
JSON_QUERY(j, '$.user.name');
-- "ana"
JSON_QUERY returns whatever is at the path as JSON. Note the second result: on a string it returns the value with its quotes. If you compare that to 'ana' the filter matches nothing. For scalars use JSON_VALUE.
OWOX Data Marts
See your first report built in real time. 15 minutes.
- Connect your data warehouse
- Pick your metrics
- Get a live Google Sheets report
In the time it takes to write a ticket. Then imagine never writing that ticket again.
Book a DemoWe'll use your actual use case
JSON_VALUE_ARRAY and JSON_QUERY_ARRAY
JSON_VALUE_ARRAY and JSON_QUERY_ARRAY
Arrays need the _ARRAY versions, which return a BigQuery array you can UNNEST.
SELECT
JSON_VALUE_ARRAY(j, '$.user.tags'),
-- [a, b]
ARRAY_LENGTH(JSON_QUERY_ARRAY(j, '$.items'));
-- 2
Use JSON_VALUE_ARRAY when the elements are scalars, JSON_QUERY_ARRAY when they are objects. To get one row per item:
SELECT
JSON_VALUE(item, '$.sku') AS sku,
SAFE_CAST(JSON_VALUE(item, '$.qty') AS INT64)
AS qty
FROM t,
UNNEST(JSON_QUERY_ARRAY(j, '$.items')) AS item;
-- x1, 2
-- y2, 1
That pattern, UNNEST(JSON_QUERY_ARRAY(...)) and then JSON_VALUE on each element, covers most nested JSON. More on the array side in array functions.
JSON_EXTRACT_SCALAR vs JSON_VALUE
JSON_EXTRACT_SCALAR vs JSON_VALUE
The older functions still work and you will see them in existing queries. They map one to one:
| Older | Current | Same result |
|---|---|---|
JSON_EXTRACT_SCALAR |
JSON_VALUE |
yes: ana |
JSON_EXTRACT |
JSON_QUERY |
yes: "ana" |
JSON_EXTRACT_STRING_ARRAY |
JSON_VALUE_ARRAY |
yes |
JSON_EXTRACT_ARRAY |
JSON_QUERY_ARRAY |
yes |
The practical difference is how a key with special characters is written. The document above has a key called a.b:
SELECT
JSON_VALUE(j, '$."a.b"'), -- dot
JSON_EXTRACT_SCALAR(j, "$['a.b']"); -- dot
The current functions use double quotes inside the path; the older ones use brackets and single quotes. Google’s JSON functions reference recommends the current names for new queries.
“Argument 2 must be a constant expression”
“Argument 2 must be a constant expression”
The path has to be a literal. If you pass a column, BigQuery answers:
Argument 2 to JSON_EXTRACT must be a
constant expression
JSON_VALUE gives the same message with its own name. You hit this when the key you want is itself stored in a column. The way around it is to convert the string to the JSON type, which accepts a dynamic key in brackets:
SELECT
key,
STRING(PARSE_JSON(j)[key]) AS value
FROM (SELECT '{"a":"1","b":"2"}' AS j),
UNNEST(['a', 'b']) AS key;
-- a, 1
-- b, 2
PARSE_JSON and the JSON type
BigQuery has a native JSON data type. PARSE_JSON converts a string into it, and then you can reach fields with dots:
SELECT
PARSE_JSON(j).user.name, -- "ana"
STRING(PARSE_JSON(j).user.name), -- ana
INT64(PARSE_JSON(j).id), -- 7
JSON_TYPE(PARSE_JSON(j).id); -- number
Dot access returns JSON, so a string still carries its quotes. Convert with STRING(), INT64(), FLOAT64() or BOOL(). These are strict: INT64() on a JSON string fails. The LAX_ versions convert where they reasonably can:
SELECT LAX_INT64(PARSE_JSON('{"n":"12"}').n);
-- 12
PARSE_JSON stops the query on malformed input. On data you do not control, use SAFE.PARSE_JSON, which returns NULL:
SELECT SAFE.PARSE_JSON('{bad json');
-- NULL
If a column holds JSON you query often, storing it as JSON instead of STRING means it is parsed once, at load.
JSON to string, and back
SELECT
TO_JSON_STRING(PARSE_JSON(j).user),
-- {"name":"ana","tags":["a","b"]}
TO_JSON_STRING(STRUCT('ana' AS name, 3 AS n));
-- {"name":"ana","n":3}
TO_JSON_STRING turns a JSON value, a struct or an array into JSON text. TO_JSON does the same but returns the JSON type. To go from JSON to a plain string value, use JSON_VALUE or STRING() as above. Functions that build and edit JSON are in JSON data manipulation functions and creating JSON objects and arrays.
Why a JSON function returns NULL
Why a JSON function returns NULL
In order of how often each is the cause:
- The value is an object or array and you used
JSON_VALUE. UseJSON_QUERY. - The path is wrong. Keys are case-sensitive, arrays start at
[0], and a key with a dot or space needs quotes. - The key is missing in that row. Normal for event data.
- The string is not valid JSON.
JSON_VALUE('not json', '$.a')returnsNULL, not an error. - The value is a JSON
null.
Count them before you rely on the column: COUNTIF(JSON_VALUE(j, '$.id') IS NULL) tells you how many rows the extraction lost.
Parse it once
JSON columns are where one dataset quietly becomes several. One query reads $.user.name, another $.user.full_name after a schema change, a third forgets the cast and compares "7" to 7. Each returns rows, and the totals differ.
The steady fix is to extract the fields in one query, with the paths and casts written down, and have everything else read plain typed columns. In OWOX Data Marts that query is a Data Mart: your SQL, with a description and an owner, run in your warehouse. OWOX does not parse the JSON for you or guess the paths. It keeps the extraction you wrote as the one every report uses.
Turn your data into decisions.
Governed data marts give you the clean foundation ML needs to actually work.
- No AI hallucinations
- Analyst-governed definitions
- Every number traces to SQL
Frequently Asked Questions
What are the LAX functions in BigQuery?
LAX_INT64, LAX_FLOAT64, LAX_STRING and LAX_BOOL convert a JSON value to a SQL type and tolerate a mismatch: LAX_INT64 on the JSON string "12" returns 12. The strict versions, INT64() and the others, fail when the JSON type does not match.
How do I handle nested JSON arrays in BigQuery?
Turn the array into rows with UNNEST(JSON_QUERY_ARRAY(j, '$.items')) and read each element with JSON_VALUE. For an array of plain values, JSON_VALUE_ARRAY returns an ARRAY<STRING> directly.
What is the difference between JSON_QUERY and JSON_VALUE in BigQuery?
JSON_VALUE returns a scalar as a plain STRING. JSON_QUERY returns whatever is at the path as JSON text, so an object stays an object and a string keeps its quotes. Use JSON_VALUE for values and JSON_QUERY for objects and arrays.
How do I extract a value from JSON in BigQuery?
JSON_VALUE(json, '$.path') returns a string, number or boolean as a STRING. Cast the result when you need a number: SAFE_CAST(JSON_VALUE(j, '$.id') AS INT64). It returns NULL when the path holds an object or array, or does not exist.
What is the difference between JSON_EXTRACT_SCALAR and JSON_VALUE?
They return the same result. JSON_VALUE is the current name and JSON_EXTRACT_SCALAR the older one. They differ in how a key with special characters is written: JSON_VALUE(j, '$."a.b"') against JSON_EXTRACT_SCALAR(j, "$['a.b']").

