# JSON functions
> Check, read, build and change JSON values, like company profiles and report numbers.
- Category: Functions — JSON and bytes
- Keywords: JSON, JSONB, json path, $., \[last\], nested data, document, profile, settings, key, array, object, -\>, -\>\>, @\>, \<@, ?, ?|, ?&, key exists, GIN, USING GIN, JSON index, jsonpath, SQL/JSON path, jsonb\_path\_exists, jsonb\_path\_match, jsonb\_path\_query, jsonb\_path\_query\_array, jsonb\_path\_query\_first, @?, @@, filter JSON, like\_regex, keyvalue, jsonb\_pretty, pretty JSON, jsonb\_strip\_nulls, json\_strip\_nulls, remove nulls, jsonb\_set\_lax, jsonb\_insert, jsonb\_object, array\_to\_json, get key, change JSON, build JSON, delete key, #-, remove key, JSON to rows, jsonb\_each, json\_each, jsonb\_each\_text, jsonb\_array\_elements, json\_array\_elements, jsonb\_array\_elements\_text, jsonb\_object\_keys, json\_to\_record, json\_to\_recordset, jsonb\_populate\_record, jsonb\_populate\_recordset, bulk insert from JSON
- See also: [Bytes, hex and base64](functions-bytes.md), [Column types](column-types.md), [UPDATE and DELETE](update-delete.md), [Conversion functions](functions-conversion.md), [Text functions](functions-text.md)
JSON is a text format for data with names and lists: `{"branches": 270, "tags": ["bank"]}`.
Some data is different for every row. A bank has branches; a hydropower company has megawatts.
Put such data in a `JSON` column, and read it with the functions on this page.
- A **path** says which part you want: `'$.office.city'` or `'$.tags[0]'` (see [JSON paths](functions-json.md#json-paths)).
- A path that is not in the document gives `NULL`. It is not an error.
- Most functions also take **text** that holds JSON. KarelaDB checks the text first; bad JSON is an error.
- A `NULL` argument gives `NULL` (except `json_object` and `json_array`, which turn `NULL` into JSON `null`).
- The result tables show a JSON value after reading it: a JSON string shows without its quotes, JSON `null` shows as `NULL`, and `2.50` can show as `2.5`. HTTP answers in JSON do the same. To see the exact saved text, add `::STRING` (some examples do).
Every example on this page has two small tables, `companies` and `fundamentals` (open "Setup" under an example to see them).
[Chapter 18](../tutorial/18-content-data.md) of the tutorial shows the same tables step by step.
Setup used by the examples below:
```sql
CREATE TABLE companies (
symbol STRING PRIMARY KEY,
name STRING NOT NULL,
sector STRING NOT NULL,
profile JSON DEFAULT '{}'
);
INSERT INTO companies VALUES
('CITY', 'City Bank Limited', 'Banking',
'{"website": "https://www.citybank.example.com", "office": {"city": "Kathmandu", "phone": "01-5970015"}, "branches": 270, "tags": ["bank", "A class"]}'),
('COAST', 'Coastal Bank Limited', 'Banking',
'{"website": "https://www.coastalbank.example.com", "office": {"city": "Kathmandu"}, "branches": 380, "tags": ["bank"]}'),
('VOLT', 'Volt River Hydropower Limited', 'Energy',
'{"website": "https://www.voltriver.example.com", "capacity_mw": 456, "rivers": ["Volt River"], "name_ne": "भोल्ट रिभर"}');
CREATE TABLE fundamentals (
id LONG PRIMARY KEY AUTO,
symbol STRING NOT NULL,
fiscal_year STRING NOT NULL,
quarter INT NOT NULL,
eps DECIMAL(2),
extra JSON DEFAULT '{}'
);
INSERT INTO fundamentals (symbol, fiscal_year, quarter, eps, extra) VALUES
('CITY', '2082/83', 3, 11.40, '{"npl_pct": 2.75, "car_pct": 13.1, "deposits": 442900000000}'),
('CITY', '2082/83', 4, 12.10, '{"npl_pct": 2.60, "car_pct": 13.4, "deposits": 455000000000, "audited": false}'),
('COAST', '2082/83', 4, 6.35, '{"npl_pct": 4.10, "car_pct": 12.2}'),
('VOLT', '2082/83', 4, 3.20, '{"energy_gwh": 2105.4, "plant_factor_pct": 52.7}');
```
**Example: Read the company profiles**
```sql
SELECT symbol,
json_value(profile, '$.office.city') AS city,
profile ->> 'website' AS website,
json_value(profile, '$.branches') AS branches,
profile -> 'rivers' ->> 0 AS river
FROM companies
ORDER BY symbol;
```
Result:
| symbol (STRING) | city (STRING) | website (STRING) | branches (LONG) | river (STRING) |
|---|---|---|---|---|
| CITY | Kathmandu | https://www.citybank.example.com | 270 | NULL |
| COAST | Kathmandu | https://www.coastalbank.example.com | 380 | NULL |
| VOLT | NULL | https://www.voltriver.example.com | NULL | Volt River |
(3 rows)
## JSON column type
Syntax:
```sql
column_name JSON [DEFAULT '{}']
column_name JSONB
```
A `JSON` column keeps checked JSON. `JSONB` is another name for the same type. The [column types](column-types.md) topic lists all types.
- **Check:** KarelaDB refuses text that is not valid JSON. The error says the row, the column and the character.
- **Spaces:** spaces between the parts are removed. `{ "a" : 1 }` is saved as `{"a":1}`.
- **Order:** the order of the keys stays. The digits of numbers stay (`2.60` stays `2.60`).
- **Values:** a number, `TRUE` or `FALSE` put into a `JSON` column becomes JSON (`5`, `true`). Text must be JSON text.
- **NULL:** SQL `NULL` is "no value". The JSON text `'null'` is a value: `profile IS NULL` is false for it.
- **Size:** at most 16 MB per value, and at most 256 levels of `{ }` and `[ ]` inside each other.
- **Compare:** two JSON values are equal when their text (without spaces) is equal. So `{"a":1,"b":2}` is **not** equal to `{"b":2,"a":1}`. A JSON number compared with a SQL number is compared by value.
- **Notes:** `ORDER BY`, `min` and `max` on JSON values compare the JSON **text**, so `10` comes before `9`. To sort by a number inside JSON, use `json_value_double(...)` or `json_value(...)`.
**Example: Spaces are removed; SQL NULL and JSON null are different**
Setup:
```sql
CREATE TABLE app_settings (user_id LONG PRIMARY KEY, settings JSON);
INSERT INTO app_settings VALUES
(1, '{ "theme" : "dark", "lang" : "ne" }'),
(2, 'null'),
(3, NULL),
(4, 5);
```
```sql
SELECT user_id, settings::STRING AS saved_text, settings IS NULL AS is_sql_null, json_type(settings) AS kind
FROM app_settings
ORDER BY user_id;
```
Result:
| user_id (LONG) | saved_text (STRING) | is_sql_null (BOOL) | kind (STRING) |
|---|---|---|---|
| 1 | {"theme":"dark","lang":"ne"} | false | object |
| 2 | null | false | null |
| 3 | NULL | true | NULL |
| 4 | 5 | false | number |
(4 rows)
**Example: Bad JSON is refused** (shows an error)
```sql
INSERT INTO fundamentals (symbol, fiscal_year, quarter, extra)
VALUES ('VOLT', '2082/83', 3, '{"energy_gwh": 1850.2,}');
```
Result:
Error: `column 'extra' (JSON): not valid JSON: a JSON object cannot end with ',' at character 23 (near '}')`
**Example: Most read news: sort by the number, not by the JSON**
Setup:
```sql
CREATE TABLE news (id LONG PRIMARY KEY, title STRING, stats JSON);
INSERT INTO news VALUES
(1, 'Market index gains 12 points', '{"views": 9}'),
(2, 'City Bank profit rises 18%', '{"views": 120}'),
(3, 'Volt River output up', '{"views": 10}');
```
```sql
SELECT id, title, stats -> 'views' AS views FROM news ORDER BY stats -> 'views' DESC;
SELECT id, title, json_value_long(stats, '$.views') AS views FROM news ORDER BY views DESC;
```
Result:
`SELECT id, title, stats -> 'views' AS views FROM news ORDER BY stats -> 'views' DESC`
| id (LONG) | title (STRING) | views (JSON) |
|---|---|---|
| 2 | City Bank profit rises 18% | 120 |
| 3 | Volt River output up | 10 |
| 1 | Market index gains 12 points | 9 |
(3 rows)
`SELECT id, title, json_value_long(stats, '$.views') AS views FROM news ORDER BY views DESC`
| id (LONG) | title (STRING) | views (LONG) |
|---|---|---|
| 2 | City Bank profit rises 18% | 120 |
| 3 | Volt River output up | 10 |
| 1 | Market index gains 12 points | 9 |
(3 rows)
## JSON paths
Syntax:
```sql
'$' the whole document
'$.key' a key of an object
'$.key.inner' a key inside a key
'$.list[0]' element 0 of an array (the first one)
'$.list[-1]' the last element ([-2] is the one before it)
'$.list[last]' the last element, too
'$."key with spaces"' a key with spaces, dots or other signs
'$["key"]' '$[''key'']' the same, in brackets
```
A path is text that points to one part of a JSON value. It always starts with `$`. It is used by `json_extract`, `json_value`, `json_set` and the other functions with a `path` argument.
| Part | Meaning |
|---|---|
| `$` | the whole document |
| `.name` | the key `name` of an object |
| `[n]` | element `n` of an array; `0` is the first |
| `[-n]`, `[last]` | counted from the end; `[-1]` and `[last]` are the last element |
| `."..."`, `["..."]`, `['...']` | a key written in quotes |
- **Missing:** a key or element that is not there gives `NULL`. A number on an object, or a key on an array, also gives `NULL`.
- **Notes:** a key written twice in one object: the path finds the **last** one.
- **Not supported:** wildcards (`$.items[*]`, `$.*`), ranges, filters and `..`. They give an error. `[-0]` is an error (the last element is `[-1]`).
PostgreSQL's full path language, with all of these, is in [jsonb_path_query and @?](functions-json.md#jsonpath).
- A path must be text in quotes. A column or a number in its place is an error.
**Example: Different paths on one company profile**
```sql
SELECT json_extract(profile, '$.office.city') AS city,
json_extract(profile, '$.tags[0]') AS first_tag,
json_extract(profile, '$.tags[-1]') AS last_tag,
json_extract(profile, '$.tags[last]') AS also_last,
json_extract(profile, '$.tags[5]') AS no_such_tag
FROM companies
WHERE symbol = 'CITY';
```
Result:
| city (JSON) | first_tag (JSON) | last_tag (JSON) | also_last (JSON) | no_such_tag (STRING) |
|---|---|---|---|---|
| "Kathmandu" | "bank" | "A class" | "A class" | NULL |
(1 row)
**Example: Keys with spaces or dots need quotes**
```sql
SELECT json_extract('{"net profit": 3270.1, "q4.audited": true}', '$."net profit"') AS a,
json_extract('{"net profit": 3270.1, "q4.audited": true}', '$["q4.audited"]') AS b,
json_extract('{"net profit": 3270.1, "q4.audited": true}', '$.q4.audited') AS without_quotes;
```
Result:
| a (JSON) | b (JSON) | without_quotes (STRING) |
|---|---|---|
| 3270.1 | true | NULL |
(1 row)
**Example: Wildcards are not supported** (shows an error)
```sql
SELECT json_extract(profile, '$.tags[*]') FROM companies;
```
Result:
Error: `line 1, column 8: json_extract: JSON path '$.tags[*]' is not valid: wildcards (*) are not supported (paths look like '$.company.sector', '$.items[0]' or '$."key with spaces"')`
**Example: A path must start with $** (shows an error)
```sql
SELECT json_extract(profile, 'office.city') FROM companies;
```
Result:
Error: `line 1, column 8: json_extract: JSON path 'office.city' is not valid: it must start with $ (paths look like '$.company.sector', '$.items[0]' or '$."key with spaces"')`
## json
Syntax:
```sql
json(text)
```
Checks that `text` is valid JSON and gives it as a `JSON` value, without extra spaces. `CAST(text AS JSON)` and `text::JSON` do the same.
| Argument | Type | Meaning |
|---|---|---|
| `text` | `STRING` (or `JSON`) | JSON text, like `'{"a": 1}'` |
- **Returns:** `JSON`.
- **NULL:** `json(NULL)` is `NULL`. `json('null')` is the JSON value `null`.
- **Notes:** bad JSON is an error that says what is wrong and at which character. Keys and strings need double quotes (`"a"`), and `true`, `false`, `null` are lower case.
- Use `json(...)` when you put JSON **inside** other JSON with `json_object`, `json_array` or `json_set`. Without it, the text is saved as a JSON string.
**Example**
```sql
SELECT json('{ "symbol" : "CITY", "close" : 512.50 }')::STRING AS saved_text,
json('[1, 2, 3]') AS list,
CAST('{"a": 1}' AS JSON) AS with_cast,
json('{"name_ne": "सिटी बैंक"}') AS nepali;
```
Result:
| saved_text (STRING) | list (JSON) | with_cast (JSON) | nepali (JSON) |
|---|---|---|---|
| {"symbol":"CITY","close":512.50} | [1,2,3] | {"a":1} | {"name_ne":"सिटी बैंक"} |
(1 row)
**Example: Single quotes are not JSON** (shows an error)
```sql
SELECT json('{''symbol'': ''CITY''}');
```
Result:
Error: `line 1, column 8: json: argument 1 is not valid JSON: expected a key in double quotes at character 2 (near ''symbol': 'C...')`
**Example: true, false and null are lower case** (shows an error)
```sql
SELECT json('{"listed": TRUE}');
```
Result:
Error: `line 1, column 8: json: argument 1 is not valid JSON: expected a JSON value (true, false and null are lower case) at character 12 (near 'TRUE}')`
## to_json
Syntax:
```sql
to_json(value)
```
Turns one SQL value into JSON. Text becomes a JSON string with `"` around it. Numbers and `TRUE`/`FALSE` stay as they are.
| Argument | Type | Meaning |
|---|---|---|
| `value` | any type | the value to turn into JSON |
- **Returns:** `JSON`.
- **NULL:** `to_json(NULL)` is SQL `NULL` (not JSON `null`).
- **Notes:** a `DECIMAL` keeps its digits (`2.50`); a `DOUBLE` does not (`2.5`). A time becomes a string like `"2026-09-10T05:15:00.000Z"`. A `JSON` value stays the same. `BYTES` become a string like `"\\x0a0b"`.
- `to_json('{"a": 1}')` gives a JSON **string**, not an object. Use `json(...)` to read JSON text.
**Example**
```sql
SELECT to_json('City "Bank"')::STRING AS text_value,
to_json(512.50)::STRING AS double_value,
to_json(CAST(2.50 AS DECIMAL(2)))::STRING AS exact,
to_json(TRUE)::STRING AS yes,
to_json(CAST('2026-09-10T05:15:00Z' AS TIMESTAMP))::STRING AS time_value,
to_json('{"a": 1}')::STRING AS still_text;
```
Result:
| text_value (STRING) | double_value (STRING) | exact (STRING) | yes (STRING) | time_value (STRING) | still_text (STRING) |
|---|---|---|---|---|---|
| "City \\"Bank\\"" | 512.5 | 2.50 | true | "2026-09-10T05:15:00.000Z" | "{\\"a\\": 1}" |
(1 row)
## json_valid
Syntax:
```sql
json_valid(text)
```
`true` when `text` is valid JSON, else `false`. It never gives an error, so it is good for checking data before you insert it.
| Argument | Type | Meaning |
|---|---|---|
| `text` | `STRING` (or any value) | the text to check |
- **Returns:** `BOOL`.
- **NULL:** `json_valid(NULL)` is `NULL`.
- **Notes:** a number, `TRUE` or a `JSON` value is valid. `BYTES` are never valid. Empty text is not valid.
**Example**
```sql
SELECT json_valid('{"npl_pct": 2.75}') AS good,
json_valid('{"npl_pct": 2.75,}') AS extra_comma,
json_valid('{npl_pct: 2.75}') AS no_quotes,
json_valid('') AS empty,
json_valid(NULL) AS nothing;
```
Result:
| good (BOOL) | extra_comma (BOOL) | no_quotes (BOOL) | empty (BOOL) | nothing (STRING) |
|---|---|---|---|---|
| true | false | false | false | NULL |
(1 row)
**Example: Find bad rows in imported text before loading them**
Setup:
```sql
CREATE TABLE import_raw (line_no LONG PRIMARY KEY, symbol STRING, extra_text STRING);
INSERT INTO import_raw VALUES
(1, 'CITY', '{"npl_pct": 2.91}'),
(2, 'COAST', '{"npl_pct": 4.3,}'),
(3, 'VOLT', '{"energy_gwh": 1850.2}');
```
```sql
SELECT line_no, symbol, extra_text
FROM import_raw
WHERE NOT json_valid(extra_text);
```
Result:
| line_no (LONG) | symbol (STRING) | extra_text (STRING) |
|---|---|---|
| 2 | COAST | {"npl_pct": 4.3,} |
(1 row)
## json_extract
Syntax:
```sql
json_extract(doc, path)
```
The part of `doc` at `path`, as JSON. A string stays in quotes (`"Kathmandu"`), and objects and arrays stay whole.
| Argument | Type | Meaning |
|---|---|---|
| `doc` | `JSON` or JSON text | the document |
| `path` | `STRING` | a [JSON path](functions-json.md#json-paths) like `'$.office.city'` |
- **Returns:** `JSON`.
- **NULL:** `NULL` when an argument is `NULL`, or the path is not in the document.
- **Notes:** `doc -> 'key'` is a short way to write `json_extract(doc, '$.key')` (see [->, ->>](functions-json.md#arrow)). To get a plain SQL value (text without quotes, a number), use `json_value`.
**Example**
```sql
SELECT symbol,
json_extract(profile, '$.office') AS office,
json_extract(profile, '$.office.city')::STRING AS city_json_text,
json_value(profile, '$.office.city') AS city_value,
json_extract(profile, '$.tags') AS tags
FROM companies
ORDER BY symbol;
```
Result:
| symbol (STRING) | office (JSON) | city_json_text (STRING) | city_value (STRING) | tags (JSON) |
|---|---|---|---|---|
| CITY | {"city":"Kathmandu","phone":"01-5970015"} | "Kathmandu" | Kathmandu | ["bank","A class"] |
| COAST | {"city":"Kathmandu"} | "Kathmandu" | Kathmandu | ["bank"] |
| VOLT | NULL | NULL | NULL | NULL |
(3 rows)
**Example: The text must be valid JSON** (shows an error)
```sql
SELECT json_extract('CITY', '$.a');
```
Result:
Error: `line 1, column 8: json_extract: argument 1 is not valid JSON: expected a JSON value (an object, array, string, number, true, false or null) at character 1 (near 'CITY')`
## json_value
Syntax:
```sql
json_value(doc, path)
```
The part of `doc` at `path` as a normal SQL value. The type depends on the JSON value:
| JSON value | SQL result |
|---|---|
| `"text"` | `STRING` without quotes |
| a whole number like `270` | `LONG` |
| a number with a point or `e`, like `2.60` | `DOUBLE` (`2.6`) |
| `true`, `false` | `BOOL` |
| `null`, an object, an array | `NULL` |
| Argument | Type | Meaning |
|---|---|---|
| `doc` | `JSON` or JSON text | the document |
| `path` | `STRING` | a [JSON path](functions-json.md#json-paths) |
- **Returns:** `STRING`, `LONG`, `DOUBLE` or `BOOL`; `NULL` when the path is missing.
- **NULL:** `NULL` when an argument is `NULL`.
- **Notes:** a whole number too big for `LONG` comes back as `DOUBLE`. The result type can change from row to row. When you need one type, use `json_value_text`, `json_value_double`, `json_value_long` or `json_value_bool`.
- A `DOUBLE` is not exact. For money, take the text and cast it: `CAST(json_value_text(doc, '$.price') AS DECIMAL(2))`.
**Example**
```sql
SELECT json_value(profile, '$.office.city') AS city,
json_value(profile, '$.branches') AS branches,
json_value(profile, '$.tags') AS tags_is_array,
json_value(profile, '$.no_such_key') AS missing
FROM companies
WHERE symbol = 'CITY';
```
Result:
| city (STRING) | branches (LONG) | tags_is_array (STRING) | missing (STRING) |
|---|---|---|---|
| Kathmandu | 270 | NULL | NULL |
(1 row)
**Example: Banks with bad loans (NPL) above 3 percent**
```sql
SELECT f.symbol, c.name, json_value(f.extra, '$.npl_pct') AS npl_pct
FROM fundamentals f
JOIN companies c ON c.symbol = f.symbol
WHERE f.quarter = 4 AND json_value(f.extra, '$.npl_pct') > 3;
```
Result:
| symbol (STRING) | name (STRING) | npl_pct (DOUBLE) |
|---|---|---|
| COAST | Coastal Bank Limited | 4.1 |
(1 row)
**Example: Nepali text inside JSON: letters count as one**
```sql
SELECT json_value(profile, '$.name_ne') AS name_ne,
length(json_value(profile, '$.name_ne')) AS letters
FROM companies
WHERE symbol = 'VOLT';
```
Result:
| name_ne (STRING) | letters (LONG) |
|---|---|
| भोल्ट रिभर | 10 |
(1 row)
## json_value_text
Syntax:
```sql
json_value_text(doc, path)
```
The part at `path` always as text. A JSON string comes without quotes. A number keeps its digits as written (`2.60`). An object or array comes as its JSON text.
| Argument | Type | Meaning |
|---|---|---|
| `doc` | `JSON` or JSON text | the document |
| `path` | `STRING` | a [JSON path](functions-json.md#json-paths) |
- **Returns:** `STRING`.
- **NULL:** `NULL` when an argument is `NULL`, the path is missing, or the value is JSON `null`.
- **Notes:** this is the safe way to read exact numbers: `CAST(json_value_text(...) AS DECIMAL(2))`. `doc ->> 'key'` gives the same text for one key.
**Example: Exact percent values for a report**
```sql
SELECT symbol, quarter,
json_value_text(extra, '$.car_pct') AS car_text,
CAST(json_value_text(extra, '$.npl_pct') AS DECIMAL(2)) AS npl_exact,
json_value(extra, '$.npl_pct') AS npl_double
FROM fundamentals
WHERE symbol IN ('CITY', 'COAST')
ORDER BY symbol, quarter;
```
Result:
| symbol (STRING) | quarter (INT) | car_text (STRING) | npl_exact (DECIMAL(2)) | npl_double (DOUBLE) |
|---|---|---|---|---|
| CITY | 3 | 13.1 | 2.75 | 2.75 |
| CITY | 4 | 13.4 | 2.60 | 2.6 |
| COAST | 4 | 12.2 | 4.10 | 4.1 |
(3 rows)
**Example**
```sql
SELECT json_value_text(profile, '$.office') AS office_json,
json_value_text(profile, '$.branches') AS branches_text
FROM companies
WHERE symbol = 'CITY';
```
Result:
| office_json (STRING) | branches_text (STRING) |
|---|---|
| {"city":"Kathmandu","phone":"01-5970015"} | 270 |
(1 row)
## json_value_double
Syntax:
```sql
json_value_double(doc, path)
```
The part at `path` as a `DOUBLE` number. A JSON number works, and so does a JSON string that holds a number (`"12.5"`).
| Argument | Type | Meaning |
|---|---|---|
| `doc` | `JSON` or JSON text | the document |
| `path` | `STRING` | a [JSON path](functions-json.md#json-paths) |
- **Returns:** `DOUBLE`.
- **NULL:** `NULL` when an argument is `NULL`, the path is missing, or the value is not a number (`"abc"`, `true`, an object).
- **Notes:** it never gives an error for a wrong value, so it is safe in `WHERE` and `ORDER BY`.
**Example**
```sql
SELECT json_value_double('{"close": 512.5}', '$.close') AS number,
json_value_double('{"close": "512.5"}', '$.close') AS number_in_text,
json_value_double('{"close": "n/a"}', '$.close') AS not_a_number,
json_value_double('{"close": true}', '$.close') AS bool_value;
```
Result:
| number (DOUBLE) | number_in_text (DOUBLE) | not_a_number (STRING) | bool_value (STRING) |
|---|---|---|---|
| 512.5 | 512.5 | NULL | NULL |
(1 row)
**Example: Hydropower plants with a plant factor above 50 percent**
```sql
SELECT symbol, json_value_double(extra, '$.plant_factor_pct') AS plant_factor
FROM fundamentals
WHERE json_value_double(extra, '$.plant_factor_pct') > 50;
```
Result:
| symbol (STRING) | plant_factor (DOUBLE) |
|---|---|
| VOLT | 52.7 |
(1 row)
## json_value_long
Syntax:
```sql
json_value_long(doc, path)
```
The part at `path` as a `LONG` (a whole number). `12` and `12.0` work, and so does a string like `"42"`. `12.5` does not fit and gives `NULL`.
| Argument | Type | Meaning |
|---|---|---|
| `doc` | `JSON` or JSON text | the document |
| `path` | `STRING` | a [JSON path](functions-json.md#json-paths) |
- **Returns:** `LONG`.
- **NULL:** `NULL` when an argument is `NULL`, the path is missing, or the value is not a whole number (or too big for `LONG`).
**Example**
```sql
SELECT json_value_long('{"branches": 270}', '$.branches') AS a,
json_value_long('{"branches": 270.0}', '$.branches') AS b,
json_value_long('{"branches": "270"}', '$.branches') AS c,
json_value_long('{"branches": 270.5}', '$.branches') AS d,
json_value_long('{"branches": 99999999999999999999}', '$.branches') AS too_big;
```
Result:
| a (LONG) | b (LONG) | c (LONG) | d (STRING) | too_big (STRING) |
|---|---|---|---|---|
| 270 | 270 | 270 | NULL | NULL |
(1 row)
**Example: Total branches of all banks**
```sql
SELECT sum(json_value_long(profile, '$.branches')) AS branches
FROM companies
WHERE sector = 'Banking';
```
Result:
| branches (LONG) |
|---|
| 650 |
(1 row)
## json_value_bool
Syntax:
```sql
json_value_bool(doc, path)
```
The part at `path` as `BOOL`. JSON `true` and `false` work, and so do the strings `"true"` and `"false"`.
| Argument | Type | Meaning |
|---|---|---|
| `doc` | `JSON` or JSON text | the document |
| `path` | `STRING` | a [JSON path](functions-json.md#json-paths) |
- **Returns:** `BOOL`.
- **NULL:** `NULL` when an argument is `NULL`, the path is missing, or the value is something else (`1`, `"yes"`, an object).
**Example**
```sql
SELECT json_value_bool('{"audited": true}', '$.audited') AS a,
json_value_bool('{"audited": "false"}', '$.audited') AS b,
json_value_bool('{"audited": 1}', '$.audited') AS c,
json_value_bool('{"audited": "yes"}', '$.audited') AS d;
```
Result:
| a (BOOL) | b (BOOL) | c (STRING) | d (STRING) |
|---|---|---|---|
| true | false | NULL | NULL |
(1 row)
**Example: Quarter results that are not audited yet**
```sql
SELECT symbol, fiscal_year, quarter
FROM fundamentals
WHERE json_value_bool(extra, '$.audited') = FALSE;
```
Result:
| symbol (STRING) | fiscal_year (STRING) | quarter (INT) |
|---|---|---|
| CITY | 2082/83 | 4 |
(1 row)
## json_type
Syntax:
```sql
json_type(doc)
json_type(doc, path)
```
The kind of JSON value: `object`, `array`, `string`, `number`, `boolean` or `null`.
| Argument | Type | Meaning |
|---|---|---|
| `doc` | `JSON` or JSON text | the document |
| `path` | `STRING` | optional [JSON path](functions-json.md#json-paths); default `'$'` (the whole document) |
- **Returns:** `STRING`.
- **NULL:** SQL `NULL` when an argument is `NULL` or the path is missing. For a JSON `null` value it gives the text `'null'`.
- **Notes:** use it to tell "the key is missing" (`NULL`) from "the key is there with `null`" (`'null'`).
**Example**
```sql
SELECT json_type(profile) AS whole,
json_type(profile, '$.office') AS office,
json_type(profile, '$.tags') AS tags,
json_type(profile, '$.branches') AS branches,
json_type(profile, '$.website') AS website,
json_type(profile, '$.capacity_mw') AS missing
FROM companies
WHERE symbol = 'CITY';
```
Result:
| whole (STRING) | office (STRING) | tags (STRING) | branches (STRING) | website (STRING) | missing (STRING) |
|---|---|---|---|---|---|
| object | object | array | number | string | NULL |
(1 row)
**Example: Which quarters have the audited key at all**
```sql
SELECT symbol, quarter, json_type(extra, '$.audited') AS audited_type
FROM fundamentals
ORDER BY symbol, quarter;
```
Result:
| symbol (STRING) | quarter (INT) | audited_type (STRING) |
|---|---|---|
| CITY | 3 | NULL |
| CITY | 4 | boolean |
| COAST | 4 | NULL |
| VOLT | 4 | NULL |
(4 rows)
## json_array_length
Syntax:
```sql
json_array_length(doc)
json_array_length(doc, path)
```
The number of elements in a JSON array. Elements inside inner arrays are not counted.
| Argument | Type | Meaning |
|---|---|---|
| `doc` | `JSON` or JSON text | the document |
| `path` | `STRING` | optional [JSON path](functions-json.md#json-paths) to the array; default `'$'` |
- **Returns:** `LONG`.
- **NULL:** `NULL` when an argument is `NULL`, the path is missing, or the value is not an array.
**Example**
```sql
SELECT symbol,
json_array_length(profile, '$.tags') AS tags,
json_array_length(profile, '$.rivers') AS rivers
FROM companies
ORDER BY symbol;
```
Result:
| symbol (STRING) | tags (LONG) | rivers (LONG) |
|---|---|---|
| CITY | 2 | NULL |
| COAST | 1 | NULL |
| VOLT | NULL | 1 |
(3 rows)
**Example**
```sql
SELECT json_array_length('[1, [2, 3], {"a": [4]}]') AS top_level_only,
json_array_length('[]') AS empty,
json_array_length('{"a": 1}') AS not_an_array;
```
Result:
| top_level_only (LONG) | empty (LONG) | not_an_array (STRING) |
|---|---|---|
| 3 | 0 | NULL |
(1 row)
## json_keys
Syntax:
```sql
json_keys(doc)
json_keys(doc, path)
```
The keys of a JSON object, as a JSON array of strings, in the order they are written.
| Argument | Type | Meaning |
|---|---|---|
| `doc` | `JSON` or JSON text | the document |
| `path` | `STRING` | optional [JSON path](functions-json.md#json-paths) to the object; default `'$'` |
- **Returns:** `JSON` (an array).
- **NULL:** `NULL` when an argument is `NULL`, the path is missing, or the value is not an object.
- **Notes:** a key written twice is listed once.
**Example: Which extra numbers each company reports**
```sql
SELECT symbol, quarter, json_keys(extra) AS keys
FROM fundamentals
ORDER BY symbol, quarter;
```
Result:
| symbol (STRING) | quarter (INT) | keys (JSON) |
|---|---|---|
| CITY | 3 | ["npl_pct","car_pct","deposits"] |
| CITY | 4 | ["npl_pct","car_pct","deposits","audited"] |
| COAST | 4 | ["npl_pct","car_pct"] |
| VOLT | 4 | ["energy_gwh","plant_factor_pct"] |
(4 rows)
**Example**
```sql
SELECT json_keys(profile, '$.office') AS office_keys,
json_array_length(json_keys(profile)) AS number_of_keys
FROM companies
WHERE symbol = 'CITY';
```
Result:
| office_keys (JSON) | number_of_keys (LONG) |
|---|---|
| ["city","phone"] | 4 |
(1 row)
## json_object
Syntax:
```sql
json_object(key, value [, key, value ...])
```
Builds a JSON object from pairs of key and value. Text values become JSON strings; numbers, `TRUE`/`FALSE` and `JSON` values are put in as they are.
| Argument | Type | Meaning |
|---|---|---|
| `key` | `STRING` | the name of the key (a number is turned into text) |
| `value` | any type | the value; `NULL` becomes JSON `null` |
- **Returns:** `JSON`. `json_object()` with no arguments is `{}`.
- **NULL:** a `NULL` value becomes `null`. A `NULL` key is an error.
- **Notes:** an odd number of arguments is an error. When a key is written twice, the last value wins. At most 64 arguments.
- To put JSON inside, pass a `JSON` value (a JSON column, or `json('...')`). Plain text like `'["bank"]'` becomes a string.
**Example: A JSON answer for an app, made from table columns**
```sql
SELECT json_object('symbol', symbol,
'name', name,
'city', json_value(profile, '$.office.city'),
'tags', profile -> 'tags',
'listed', TRUE) AS api_answer
FROM companies
WHERE symbol = 'COAST';
```
Result:
| api_answer (JSON) |
|---|
| {"symbol":"COAST","name":"Coastal Bank Limited","city":"Kathmandu","tags":["bank"],"listed":true} |
(1 row)
**Example: Text is not JSON until you use json()**
```sql
SELECT json_object('tags', '["bank"]') AS as_text,
json_object('tags', json('["bank"]')) AS as_json,
json_object('note', NULL, 'note', 'second') AS last_wins;
```
Result:
| as_text (JSON) | as_json (JSON) | last_wins (JSON) |
|---|---|---|
| {"tags":"[\\"bank\\"]"} | {"tags":["bank"]} | {"note":"second"} |
(1 row)
**Example: Keys and values must come in pairs** (shows an error)
```sql
SELECT json_object('symbol', 'CITY', 'price');
```
Result:
Error: `line 1, column 8: json_object: needs pairs of key and value, like json_object('symbol', 'CITY', 'price', 512.5)`
## json_array
Syntax:
```sql
json_array([value, value ...])
```
Builds a JSON array from the values, in order.
| Argument | Type | Meaning |
|---|---|---|
| `value` | any type | an element; `NULL` becomes JSON `null` |
- **Returns:** `JSON`. `json_array()` with no arguments is `[]`.
- **NULL:** a `NULL` value becomes `null`.
- **Notes:** text becomes a JSON string; use `json('...')` to put an object or array inside. At most 64 values.
**Example**
```sql
SELECT json_array('CITY', 512.5, TRUE, NULL) AS mixed,
json_array(json('{"q": 3}'), json('{"q": 4}')) AS objects,
json_array() AS empty;
```
Result:
| mixed (JSON) | objects (JSON) | empty (JSON) |
|---|---|---|
| ["CITY",512.5,true,null] | [{"q":3},{"q":4}] | [] |
(1 row)
**Example: Open, high, low and close of one day as a small array** (sample data: sample)
```sql
SELECT time, json_array(open, high, low, close) AS ohlc
FROM ochl_day
WHERE symbol = 'CITY'
LIMIT 3;
```
Result:
| time (TIMESTAMP) | ohlc (JSON) |
|---|---|
| 2025-12-28T00:00:00.000Z | [540.3,542.5,539.2,542.2] |
| 2025-12-29T00:00:00.000Z | [540.6,542.1,538.8,539.7] |
| 2025-12-30T00:00:00.000Z | [538.5,540.5,536.4,539.3] |
(3 rows)
## json_set
Syntax:
```sql
json_set(doc, path, value [, path, value ...])
```
Changes `doc`: puts `value` at `path`. A key that is there gets the new value; a key that is not there is added. The other keys stay. Use it in `UPDATE`.
| Argument | Type | Meaning |
|---|---|---|
| `doc` | `JSON` or JSON text | the document to change |
| `path` | `STRING` | a [JSON path](functions-json.md#json-paths) |
| `value` | any type | the new value (text becomes a JSON string; use `json('...')` for JSON) |
- **Returns:** `JSON` (the changed document; the table is changed only by `UPDATE`).
- **NULL:** `NULL` when `doc` or a `path` is `NULL`. A `NULL` value puts JSON `null`.
- **Notes:** when the parent of the path is not there (`$.a.b` without `a`), nothing changes and there is no error. An array index after the end (`$.tags[9]`) adds at the end. More pairs are done one after the other.
- **Differences:** works like `JSON_SET` in MySQL. PostgreSQL's `jsonb_set` is different (paths like `'{a,b}'`).
**Example: Mark a quarter as audited and add the auditor**
Setup:
```sql
UPDATE fundamentals
SET extra = json_set(extra, '$.audited', TRUE, '$.auditor', 'CA firm A')
WHERE symbol = 'CITY' AND quarter = 4;
```
```sql
SELECT extra::STRING AS extra FROM fundamentals WHERE symbol = 'CITY' AND quarter = 4;
```
Result:
| extra (STRING) |
|---|
| {"npl_pct":2.60,"car_pct":13.4,"deposits":455000000000,"audited":true,"auditor":"CA firm A"} |
(1 row)
**Example: Change, add, and a missing parent**
```sql
SELECT json_set('{"theme": "light"}', '$.theme', 'dark') AS changed,
json_set('{"theme": "light"}', '$.lang', 'ne') AS added,
json_set('{"theme": "light"}', '$.notify.email', FALSE) AS no_parent,
json_set('{"tags": ["bank"]}', '$.tags[9]', 'A class') AS array_end;
```
Result:
| changed (JSON) | added (JSON) | no_parent (JSON) | array_end (JSON) |
|---|---|---|---|
| {"theme":"dark"} | {"theme":"light","lang":"ne"} | {"theme":"light"} | {"tags":["bank","A class"]} |
(1 row)
**Example: Paths and values must come in pairs** (shows an error)
```sql
SELECT json_set('{"a": 1}', '$.a', 2, '$.b');
```
Result:
Error: `line 1, column 8: json_set: needs pairs of path and value after the document, like json_set(doc, '$.price', 512.5)`
## json_insert
Syntax:
```sql
json_insert(doc, path, value [, path, value ...])
```
Like `json_set`, but it only **adds**. A key or array element that is already there keeps its old value.
| Argument | Type | Meaning |
|---|---|---|
| `doc` | `JSON` or JSON text | the document to change |
| `path` | `STRING` | a [JSON path](functions-json.md#json-paths) |
| `value` | any type | the value to add |
- **Returns:** `JSON`.
- **NULL:** `NULL` when `doc` or a `path` is `NULL`. A `NULL` value adds JSON `null`.
- **Notes:** good for default values: "set it only when it is not set yet".
**Example: Add a default language, but do not change a user's choice**
```sql
SELECT json_insert('{"theme": "dark"}', '$.lang', 'en') AS new_key,
json_insert('{"theme": "dark", "lang": "ne"}', '$.lang', 'en') AS kept,
json_insert('{"tags": ["bank"]}', '$.tags[1]', 'A class') AS array_end;
```
Result:
| new_key (JSON) | kept (JSON) | array_end (JSON) |
|---|---|---|
| {"theme":"dark","lang":"en"} | {"theme":"dark","lang":"ne"} | {"tags":["bank","A class"]} |
(1 row)
## json_replace
Syntax:
```sql
json_replace(doc, path, value [, path, value ...])
```
Like `json_set`, but it only **changes** what is there. A missing key is not added.
| Argument | Type | Meaning |
|---|---|---|
| `doc` | `JSON` or JSON text | the document to change |
| `path` | `STRING` | a [JSON path](functions-json.md#json-paths) |
| `value` | any type | the new value |
- **Returns:** `JSON`.
- **NULL:** `NULL` when `doc` or a `path` is `NULL`. A `NULL` value puts JSON `null`.
**Example**
```sql
SELECT json_replace('{"branches": 270}', '$.branches', 272) AS changed,
json_replace('{"branches": 270}', '$.atms', 400) AS not_added;
```
Result:
| changed (JSON) | not_added (JSON) |
|---|---|
| {"branches":272} | {"branches":270} |
(1 row)
**Example: Update the phone only for companies that have one**
Setup:
```sql
UPDATE companies
SET profile = json_replace(profile, '$.office.phone', '01-5970099');
```
```sql
SELECT symbol, profile -> 'office' AS office FROM companies ORDER BY symbol;
```
Result:
| symbol (STRING) | office (JSON) |
|---|---|
| CITY | {"city":"Kathmandu","phone":"01-5970099"} |
| COAST | {"city":"Kathmandu"} |
| VOLT | NULL |
(3 rows)
## json_remove
Syntax:
```sql
json_remove(doc, path [, path ...])
```
Removes the keys or array elements at the paths.
| Argument | Type | Meaning |
|---|---|---|
| `doc` | `JSON` or JSON text | the document to change |
| `path` | `STRING` | a [JSON path](functions-json.md#json-paths); up to 63 paths |
- **Returns:** `JSON`.
- **NULL:** `NULL` when `doc` or a `path` is `NULL`.
- **Notes:** a path that is not there changes nothing. Paths are done one after the other, so after removing `[0]` the next element becomes `[0]`. Removing `'$'` (the whole document) is an error.
**Example**
```sql
SELECT json_remove('{"npl_pct": 2.6, "car_pct": 13.4, "draft": true}', '$.draft') AS one_key,
json_remove('{"tags": ["bank", "old", "A class"]}', '$.tags[1]') AS one_element,
json_remove('{"a": 1}', '$.no_such_key') AS nothing_to_remove;
```
Result:
| one_key (JSON) | one_element (JSON) | nothing_to_remove (JSON) |
|---|---|---|
| {"npl_pct":2.6,"car_pct":13.4} | {"tags":["bank","A class"]} | {"a":1} |
(1 row)
**Example: Remove a key from every row**
Setup:
```sql
UPDATE fundamentals SET extra = json_remove(extra, '$.deposits');
```
```sql
SELECT symbol, quarter, extra::STRING AS extra FROM fundamentals ORDER BY symbol, quarter;
```
Result:
| symbol (STRING) | quarter (INT) | extra (STRING) |
|---|---|---|
| CITY | 3 | {"npl_pct":2.75,"car_pct":13.1} |
| CITY | 4 | {"npl_pct":2.60,"car_pct":13.4,"audited":false} |
| COAST | 4 | {"npl_pct":4.10,"car_pct":12.2} |
| VOLT | 4 | {"energy_gwh":2105.4,"plant_factor_pct":52.7} |
(4 rows)
**Example: The whole document cannot be removed** (shows an error)
```sql
SELECT json_remove('{"a": 1}', '$');
```
Result:
Error: `line 1, column 8: json_remove: cannot remove the whole JSON document ($)`
## json_array_append
Syntax:
```sql
json_array_append(doc, path, value)
```
Adds `value` at the end of the array at `path`.
| Argument | Type | Meaning |
|---|---|---|
| `doc` | `JSON` or JSON text | the document to change |
| `path` | `STRING` | a [JSON path](functions-json.md#json-paths) to an array (`'$'` when `doc` itself is the array) |
| `value` | any type | the new element; use `json('...')` to add an object or array |
- **Returns:** `JSON`.
- **NULL:** `NULL` when `doc` or `path` is `NULL`. A `NULL` value adds JSON `null`.
- **Notes:** exactly one path and one value. When the path is missing or is not an array, nothing changes (MySQL would make an array; KarelaDB does not).
**Example: Add a tag to a company**
Setup:
```sql
UPDATE companies
SET profile = json_array_append(profile, '$.tags', 'dividend 2082/83')
WHERE symbol = 'CITY';
```
```sql
SELECT profile -> 'tags' AS tags FROM companies WHERE symbol = 'CITY';
```
Result:
| tags (JSON) |
|---|
| ["bank","A class","dividend 2082/83"] |
(1 row)
**Example**
```sql
SELECT json_array_append('[1, 2]', '$', 3) AS number,
json_array_append('[1, 2]', '$', json('[3, 4]')) AS inner_array,
json_array_append('{"tags": "bank"}', '$.tags', 'x') AS not_an_array;
```
Result:
| number (JSON) | inner_array (JSON) | not_an_array (JSON) |
|---|---|---|
| [1,2,3] | [1,2,[3,4]] | {"tags":"bank"} |
(1 row)
## ->, ->>
Syntax:
```sql
doc -> 'key' doc -> 0 doc -> -1
doc ->> 'key' doc ->> 0
doc -> 'a' -> 'b' ->> 0
```
Short ways to take one step into JSON. `->` gives the part as `JSON`. `->>` gives it as text: a string without quotes, other values as their JSON text.
| Right side | Meaning |
|---|---|
| `'key'` | the key of an object |
| `0`, `1`, ... | an element of an array (`0` is the first) |
| `-1`, `-2`, ... | counted from the end (`-1` is the last) |
- **Returns:** `->` gives `JSON`; `->>` gives `STRING`.
- **NULL:** `NULL` when either side is `NULL`, or the key or element is not there. `->>` on JSON `null` gives `NULL`.
- **Notes:** each arrow takes **one** step. `doc -> 'office.city'` looks for a key named `office.city`, and `doc -> '$.office'` looks for a key named `$.office`: both give `NULL`. Chain arrows, or use a [path](functions-json.md#json-paths) function.
- The left side can be a `JSON` value or text that holds JSON (bad JSON is an error).
- The result of `->>` is text, so `doc ->> 'n' + 1` is an error. Use `CAST(doc ->> 'n' AS LONG)`, or `json_value(...)`.
- **Differences:** the same as PostgreSQL for one step. MySQL's `->` takes a path (`doc -> '$.a'`); KarelaDB's does not. The PostgreSQL operators `#>` and `#>>` take a whole path at once and work too: `profile #> '{office,city}'` is [json_extract_path](functions-json.md#json_extract_path) and `#>>` is [json_extract_path_text](functions-json.md#json_extract_path_text). The key tests `?`, `?|`, `?&` and the containment tests `@>`, `<@` are in [the next section](functions-json.md#contains).
- **Speed:** `doc ->> 'key' = 'text'` (also `<>`, `<`, `IN (...)`) in a `WHERE` is read straight from the stored JSON of each row, without building the row first. A [GIN index](functions-json.md#gin) makes it faster still when few rows match.
**Example: Company website, city and first tag**
```sql
SELECT symbol,
profile ->> 'website' AS website,
profile -> 'office' ->> 'city' AS city,
CAST(profile -> 'tags' -> 0 AS STRING) AS first_tag_json,
profile -> 'tags' ->> -1 AS last_tag_text
FROM companies
ORDER BY symbol;
```
Result:
| symbol (STRING) | website (STRING) | city (STRING) | first_tag_json (STRING) | last_tag_text (STRING) |
|---|---|---|---|---|
| CITY | https://www.citybank.example.com | Kathmandu | "bank" | A class |
| COAST | https://www.coastalbank.example.com | Kathmandu | "bank" | bank |
| VOLT | https://www.voltriver.example.com | NULL | NULL | NULL |
(3 rows)
**Example: One step at a time**
```sql
SELECT profile -> 'office' -> 'city' AS chained,
profile -> 'office.city' AS dotted_key,
profile -> '$.office' AS path_text
FROM companies
WHERE symbol = 'CITY';
```
Result:
| chained (JSON) | dotted_key (STRING) | path_text (STRING) |
|---|---|---|
| "Kathmandu" | NULL | NULL |
(1 row)
**Example: Filter and group by a JSON key**
```sql
SELECT profile -> 'office' ->> 'city' AS city, count(*) AS companies
FROM companies
GROUP BY profile -> 'office' ->> 'city'
ORDER BY city;
```
Result:
| city (STRING) | companies (LONG) |
|---|---|
| Kathmandu | 2 |
| NULL | 1 |
(2 rows)
**Example: ->> gives text: cast it before you calculate** (shows an error)
```sql
SELECT profile ->> 'branches' + 10 FROM companies WHERE symbol = 'CITY';
```
Result:
Error: `line 1, column 31: '270' and '10' are not both numbers`
## @>, <@, ?, ?|, ?&
Syntax:
```sql
doc @> '{"key": value}' doc holds all of this
'{"key": value}' <@ doc the same, the other way round
doc ? 'key' doc has this top-level key
doc ?| ARRAY['a', 'b'] doc has at least one of these keys
doc ?& ARRAY['a', 'b'] doc has all of these keys
```
The PostgreSQL `jsonb` tests. Each gives `TRUE` or `FALSE`, so they go in a `WHERE`.
| Test | True when |
|---|---|
| `a @> b` | every key of `b` is in `a` with a value that holds `b`'s value, at every level. A list in `b` is held when each of its elements is in `a`'s list, in any order. |
| `a <@ b` | `b @> a` |
| `doc ? 'k'` | an object with the key `k` at the top, a list with the string `"k"`, or the string `"k"` |
| `doc ?\| keys` | `doc ? k` for at least one `k` of the array |
| `doc ?& keys` | `doc ? k` for every `k` of the array (an empty array: always) |
- **Numbers** compare by value: `{"n": 1}` holds `{"n": 1.0}`. A string never equals a number: `{"n": 1}` does not hold `{"n": "1"}`.
- **Lists:** `[1, 2, [1, 3]] @> [1, 3]` is false (the 3 is only inside the inner list); `@> [[1, 3]]` is true. At the top, a list holds a single value: `'["a", "b"]' @> '"a"'`.
- `?` looks at the **top level** only: `'{"a": {"b": 1}}' ? 'b'` is false.
- The keys of `?|` and `?&` are a text array: `ARRAY['a', 'b']` or `'{a,b}'`.
- **NULL:** `NULL` when either side is `NULL`.
- On two arrays, `@>` and `<@` work as before: `ARRAY[1, 2, 3] @> ARRAY[3, 1]`.
- **Speed:** a [GIN index](functions-json.md#gin) on the column finds the rows without reading every document.
**Example: Banks with an A class tag, and companies with a capacity**
```sql
SELECT symbol FROM companies WHERE profile @> '{"tags": ["A class"]}';
SELECT symbol FROM companies WHERE profile @> '{"office": {"city": "Kathmandu"}}' ORDER BY symbol;
SELECT symbol FROM companies WHERE profile ? 'capacity_mw';
```
Result:
`SELECT symbol FROM companies WHERE profile @> '{"tags": ["A class"]}'`
| symbol (STRING) |
|---|
| CITY |
(1 row)
`SELECT symbol FROM companies WHERE profile @> '{"office": {"city": "Kathmandu"}}' ORDER BY symbol`
| symbol (STRING) |
|---|
| CITY |
| COAST |
(2 rows)
`SELECT symbol FROM companies WHERE profile ? 'capacity_mw'`
| symbol (STRING) |
|---|
| VOLT |
(1 row)
**Example: Any or all of some keys**
```sql
SELECT symbol,
profile ?| ARRAY['rivers', 'branches'] AS has_one,
profile ?& ARRAY['office', 'branches'] AS has_both,
'{"branches": 270}' <@ profile AS has_270
FROM companies
ORDER BY symbol;
```
Result:
| symbol (STRING) | has_one (BOOL) | has_both (BOOL) | has_270 (BOOL) |
|---|---|---|---|
| CITY | true | true | true |
| COAST | true | true | false |
| VOLT | true | false | false |
(3 rows)
## SQL/JSON path: jsonb_path_query, @?, @@
Syntax:
```sql
jsonb_path_exists(document, 'path' [, vars [, silent]]) TRUE when the path finds anything
jsonb_path_match(document, 'path' [, vars [, silent]]) the TRUE / FALSE the path gives
jsonb_path_query(document, 'path' [, vars [, silent]]) one row per item the path finds
jsonb_path_query_array(document, 'path' [, vars [, silent]]) the items as one JSON list
jsonb_path_query_first(document, 'path' [, vars [, silent]]) the first item, or NULL
document @? 'path' jsonb_path_exists, never an error
document @@ 'path' jsonb_path_match, never an error
```
PostgreSQL's JSON path language: find parts of a document, keep the ones a filter likes, and do sums on them.
It is bigger than the simple `'$.a.b'` [paths](functions-json.md#json-paths) of `json_extract`.
| Path part | Meaning |
|---|---|
| `$` | the whole document |
| `.key`, `."key with spaces"`, `.*` | a key; any key |
| `[0]`, `[last]`, `[last - 1]`, `[1 to 3]`, `[0, 2]`, `[*]` | elements of a list (`0` is the first) |
| `.**`, `.**{2}`, `.**{1 to last}` | every value below, at any level (or those levels) |
| `? (condition)` | keeps the items the condition is true for; `@` is the item |
| `==`, `!=`, `<>`, `<`, `<=`, `>`, `>=` | compare (numbers by value, text by its characters) |
| `&&`, `\|\|`, `!( )` | and, or, not |
| `exists (path)`, `(condition) is unknown` | a path finds something; a comparison could not be made |
| `like_regex "pattern" flag "i"`, `starts with "text"` | text tests (flags `i`, `s`, `q`) |
| `+`, `-`, `*`, `/`, `%` | sums on single numbers |
| `.type()`, `.size()`, `.double()`, `.abs()`, `.floor()`, `.ceiling()`, `.keyvalue()` | methods |
| `$name` | a variable from `vars` (a JSON object: `'{"min": 100}'`) |
- **Lax** (the default): a key step on a list looks inside its elements, an element step on a single value treats it as a list of one, and a key that is not there finds nothing. Write `strict $...` to make such mistakes errors.
- **Returns:** `BOOL` for `_exists`, `_match`, `@?` and `@@`; `JSON` for the others (PostgreSQL's `jsonb`: Postgres clients get it as `jsonb`, written `{"a": 1}`).
- **NULL:** a `NULL` document, path or vars gives `NULL` (and `jsonb_path_query` no rows). `@@` gives `NULL` when the answer is unknown.
- **Errors:** with the SQLSTATE PostgreSQL uses, such as `2203A` (`strict` and no such key), `22038` (a sum on more than one number) and `42601` (a mistake in the path). `silent => true` (the fourth argument) turns the "not found" and "wrong type" errors into no result, as `@?` and `@@` always do.
- **Speed:** the path is read once per statement, not once per row. `doc @? '...'`, `doc @@ '...'` and the calls with `silent` true run in the column engine on the stored text, like `->>`.
- **Differences:** `.datetime()` and the `like_regex` flags `m` and `x` are an error (PostgreSQL has no `x` either).
**Example: Companies with more than 300 branches, and every tag as a row**
```sql
SELECT symbol FROM companies WHERE profile @? '$.branches ? (@ > 300)';
SELECT symbol, jsonb_path_query(profile, '$.tags[*]') AS tag FROM companies ORDER BY symbol;
```
Result:
`SELECT symbol FROM companies WHERE profile @? '$.branches ? (@ > 300)'`
| symbol (STRING) |
|---|
| COAST |
(1 row)
`SELECT symbol, jsonb_path_query(profile, '$.tags[*]') AS tag FROM companies ORDER BY symbol`
| symbol (STRING) | tag (JSON) |
|---|---|
| CITY | "bank" |
| CITY | "A class" |
| COAST | "bank" |
(3 rows)
**Example: A condition as the whole path, and a value given as a variable**
```sql
SELECT symbol,
profile @@ '$.branches > 300' AS many_branches,
jsonb_path_exists(profile, '$.branches ? (@ >= $min)', '{"min": 270}') AS at_least_270
FROM companies
ORDER BY symbol;
```
Result:
| symbol (STRING) | many_branches (BOOL) | at_least_270 (BOOL) |
|---|---|---|
| CITY | false | true |
| COAST | true | true |
| VOLT | false | false |
(3 rows)
**Example: Lists and sums**
```sql
SELECT jsonb_path_query_array('{"eps": [12.1, 6.35, 3.2]}', '$.eps[*] ? (@ > 5)') AS big,
jsonb_path_query_first('{"eps": [12.1, 6.35, 3.2]}', '$.eps[last]') AS last_one,
jsonb_path_query_first('{"eps": [12.1, 6.35, 3.2]}', '$.eps.size()') AS how_many,
jsonb_path_query_first('{"eps": [12.1, 6.35, 3.2]}', '$.eps[0] / 2') AS half;
```
Result:
| big (JSON) | last_one (JSON) | how_many (JSON) | half (JSON) |
|---|---|---|---|
| [12.1,6.35] | 3.2 | 3 | 6.0500000000000000 |
(1 row)
**Example: Every key and value of an object, and text tests**
```sql
SELECT jsonb_path_query('{"npl_pct": 2.6, "car_pct": 13.4}', '$.keyvalue()') AS pair;
SELECT symbol FROM companies WHERE profile @? '$.website ? (@ like_regex "coastal|volt" flag "i")' ORDER BY symbol;
```
Result:
`SELECT jsonb_path_query('{"npl_pct": 2.6, "car_pct": 13.4}', '$.keyvalue()') AS pair`
| pair (JSON) |
|---|
| {"id":0,"key":"car_pct","value":13.4} |
| {"id":0,"key":"npl_pct","value":2.6} |
(2 rows)
`SELECT symbol FROM companies WHERE profile @? '$.website ? (@ like_regex "coastal|volt" flag "i")' O`
| symbol (STRING) |
|---|
| COAST |
| VOLT |
(2 rows)
**Example: strict: a key that is not there is an error** (shows an error)
```sql
SELECT jsonb_path_query('{"branches": 270}', 'strict $.office');
```
Result:
Error: `line 1, column 8: jsonb_path_query: JSON object does not contain key "office"`
**Example: silent: the same mistake gives no result**
```sql
SELECT count(*) FROM jsonb_path_query('{"branches": 270}', 'strict $.office', '{}', true);
```
Result:
| count(*) (LONG) |
|---|
| 0 |
(1 row)
## GIN index on a JSON column
Syntax:
```sql
CREATE INDEX name ON table USING GIN (json_column)
```
An index of the keys and values inside every document of the column. It finds the rows for:
| Condition | Example |
|---|---|
| `@>` a JSON value | `extra @> '{"category": "auth"}'` |
| `?`, `?\|`, `?&` | `extra ? 'error'` |
| `->>` equal to text, or `IN` a list of texts | `extra ->> 'category' = 'auth'`, `extra -> 'user' ->> 'role' IN ('admin', 'owner')` |
- The index finds the rows that **may** match; the condition is then checked on those rows. So the answer is always the same as without the index.
- `EXPLAIN` shows `use index name (JSON keys and values ...)` when it is used. It is not used for `<>`, `<`, `>`, or for `@> '{}'` (nothing to look up).
- Like every KarelaDB index it lives in memory and is built from the rows when first needed (see [indexes](indexes.md)). It costs roughly the size of the keys and values it holds.
- `USING GIN` on a column that is not `JSON` makes an ordinary index, as before.
- An index made with `USING GIN` before 2026-09-24 is an ordinary index on the whole value: drop it and create it again to get this kind.
**Example: Find log rows by a key inside the JSON**
Setup:
```sql
CREATE TABLE app_logs (id LONG PRIMARY KEY, message STRING, extra JSON);
INSERT INTO app_logs VALUES
(1, 'login ok', '{"category": "auth", "user": {"id": 7}}'),
(2, 'feed loaded', '{"category": "feed", "items": 20}'),
(3, 'login failed', '{"category": "auth", "user": {"id": 9}, "error": "bad password"}');
CREATE INDEX app_logs_extra ON app_logs USING GIN (extra);
```
```sql
SELECT id, message FROM app_logs WHERE extra ->> 'category' = 'auth' ORDER BY id;
SELECT id, message FROM app_logs WHERE extra @> '{"user": {"id": 9}}';
SELECT id, message FROM app_logs WHERE extra ? 'error';
```
Result:
`SELECT id, message FROM app_logs WHERE extra ->> 'category' = 'auth' ORDER BY id`
| id (LONG) | message (STRING) |
|---|---|
| 1 | login ok |
| 3 | login failed |
(2 rows)
`SELECT id, message FROM app_logs WHERE extra @> '{"user": {"id": 9}}'`
| id (LONG) | message (STRING) |
|---|---|
| 3 | login failed |
(1 row)
`SELECT id, message FROM app_logs WHERE extra ? 'error'`
| id (LONG) | message (STRING) |
|---|---|
| 3 | login failed |
(1 row)
## json_build_object, jsonb_build_object
Syntax:
```sql
json_build_object(key, value [, key, value ...])
jsonb_build_object(key, value [, key, value ...])
```
Builds a JSON object from pairs: first a key, then its value, then the next pair. `jsonb_build_object` is the same function under the PostgreSQL `jsonb` name.
| Argument | Type | Meaning |
|---|---|---|
| `key` | `STRING` (a number is turned into text) | the key name |
| `value` | any | the value for that key; `JSON` stays JSON |
- **Returns:** `JSON` (an object). With no arguments: `{}`.
- **NULL:** a `NULL` **value** becomes JSON `null`. A `NULL` **key** is an error.
- **Notes:** an odd number of arguments is an error. [json_object](functions-json.md#json_object) does the same thing with the same shape.
**Example: A company card for the app**
```sql
SELECT json_build_object('symbol', symbol, 'name', name, 'sector', sector,
'city', profile ->> 'office.city') AS card
FROM companies
WHERE symbol = 'CITY';
```
Result:
| card (JSON) |
|---|
| {"symbol":"CITY","name":"City Bank Limited","sector":"Banking","city":null} |
(1 row)
**Example: Pairs must be complete** (shows an error)
```sql
SELECT json_build_object('symbol');
```
Result:
Error: `line 1, column 8: json_build_object needs pairs of key and value (an even number of arguments)`
## json_build_array, jsonb_build_array
Syntax:
```sql
json_build_array(value [, value ...])
jsonb_build_array(value [, value ...])
```
Builds a JSON list from the values, in order. `jsonb_build_array` is the same function under the PostgreSQL `jsonb` name.
| Argument | Type | Meaning |
|---|---|---|
| `value` | any | one element of the list; `JSON` stays JSON |
- **Returns:** `JSON` (a list). With no arguments: `[]`.
- **NULL:** a `NULL` value becomes JSON `null`. The result is never `NULL`.
- **Notes:** `ARRAY[...]` builds the same value and is shorter to write (see [arrays](functions-postgres.md#arrays)).
**Example: Quarter results as a small list**
```sql
SELECT symbol, json_build_array(fiscal_year, quarter, eps) AS row_as_list
FROM fundamentals
WHERE symbol = 'CITY'
ORDER BY quarter;
```
Result:
| symbol (STRING) | row_as_list (JSON) |
|---|---|
| CITY | ["2082/83",3,11.40] |
| CITY | ["2082/83",4,12.10] |
(2 rows)
## to_jsonb
Syntax:
```sql
to_jsonb(value)
```
Turns one SQL value into JSON: text becomes a JSON string with `"` around it, a number stays a number, `TRUE`/`FALSE` stay `true`/`false`, and a `JSON` value stays as it is.
| Argument | Type | Meaning |
|---|---|---|
| `value` | any | the value to turn into JSON |
- **Returns:** `JSON`.
- **NULL:** `to_jsonb(NULL)` is `NULL` (SQL NULL, not JSON `null`).
- **Notes:** the same as [to_json](functions-json.md#to_json); PostgreSQL apps write `to_jsonb`. Use it to make the new value for [jsonb_set](functions-json.md#jsonb_set), so text is not read as JSON by mistake.
**Example**
```sql
SELECT to_jsonb(symbol) AS as_json_string,
to_jsonb(eps) AS as_number,
to_jsonb(extra) AS json_stays_json
FROM fundamentals
WHERE symbol = 'VOLT';
```
Result:
| as_json_string (JSON) | as_number (JSON) | json_stays_json (JSON) |
|---|---|---|
| "VOLT" | 3.20 | {"energy_gwh":2105.4,"plant_factor_pct":52.7} |
(1 row)
## row_to_json, to_json_row
Syntax:
```sql
row_to_json(value)
to_json_row(value)
```
Turns a value into JSON, the way a PostgreSQL app calls it on a row. KarelaDB has no whole-row value, so give it a list or an object you built yourself.
| Argument | Type | Meaning |
|---|---|---|
| `value` | any | the value to turn into JSON |
- **Returns:** `JSON`.
- **NULL:** `row_to_json(NULL)` is `NULL`.
- **Notes:** for a whole row, build it with `json_build_object('column', column, ...)`. `SELECT row_to_json(t) FROM table t`, the usual PostgreSQL way, does not work here.
**Example: One row of the company list as JSON**
```sql
SELECT row_to_json(json_build_object('symbol', symbol, 'name', name)) AS row_json
FROM companies
ORDER BY symbol;
```
Result:
| row_json (JSON) |
|---|
| {"symbol":"CITY","name":"City Bank Limited"} |
| {"symbol":"COAST","name":"Coastal Bank Limited"} |
| {"symbol":"VOLT","name":"Volt River Hydropower Limited"} |
(3 rows)
## jsonb_set
Syntax:
```sql
jsonb_set(document, path_array, new_value)
jsonb_set(document, path_array, new_value, create_missing)
```
The document with one part set to `new_value`. The path is an **array of steps** (`'{office,city}'`), not a `$.` path.
| Argument | Type | Meaning |
|---|---|---|
| `document` | `JSON` | the document to change |
| `path_array` | array or array text | the steps down to the part, like `'{office,city}'` or `ARRAY['tags', 0]` |
| `new_value` | `JSON` | the new value; use `to_jsonb(x)` for text or a number |
| `create_missing` | `BOOL` | `TRUE` (default): add the key when it is not there. `FALSE`: only change a key that is already there. |
- **Returns:** `JSON`. The saved row does not change until you `UPDATE` it.
- **NULL:** a `NULL` document gives `NULL`. A `NULL` path gives the document back. A `NULL` new value gives `NULL` (as PostgreSQL does).
- **Notes:** a new value that is not JSON is an error - `'A'` is not JSON, `to_jsonb('A')` is. [json_set](functions-json.md#json_set) does the same with a `'$.office.city'` path.
**Example: Mark a quarter as audited**
Setup:
```sql
UPDATE fundamentals
SET extra = jsonb_set(extra, '{audited}', 'true')
WHERE symbol = 'COAST' AND quarter = 4;
```
```sql
SELECT symbol, quarter, extra FROM fundamentals WHERE symbol = 'COAST';
```
Result:
| symbol (STRING) | quarter (INT) | extra (JSON) |
|---|---|---|
| COAST | 4 | {"npl_pct":4.10,"car_pct":12.2,"audited":true} |
(1 row)
**Example: create_missing = FALSE changes only keys that are there**
```sql
SELECT jsonb_set(extra, '{npl_pct}', '3.00') AS changed,
jsonb_set(extra, '{rating}', to_jsonb('A'), FALSE) AS not_added,
jsonb_set(extra, '{rating}', to_jsonb('A')) AS added
FROM fundamentals
WHERE symbol = 'COAST' AND quarter = 4;
```
Result:
| changed (JSON) | not_added (JSON) | added (JSON) |
|---|---|---|
| {"npl_pct":3.00,"car_pct":12.2} | {"npl_pct":4.10,"car_pct":12.2} | {"npl_pct":4.10,"car_pct":12.2,"rating":"A"} |
(1 row)
**Example: The new value must be JSON** (shows an error)
```sql
SELECT jsonb_set(extra, '{rating}', 'A') FROM fundamentals WHERE symbol = 'COAST';
```
Result:
Error: `line 1, column 8: jsonb_set: the new value is not JSON (use to_jsonb(...) for text)`
## jsonb_set_lax
Syntax:
```sql
jsonb_set_lax(document, path_array, new_value [, create_missing [, null_value_treatment]])
```
[jsonb_set](functions-json.md#jsonb_set) that says what a `NULL` new value means, instead of giving `NULL`.
| `null_value_treatment` | A `NULL` new value ... |
|---|---|
| `'use_json_null'` (default) | sets the part to JSON `null` |
| `'delete_key'` | removes the part (as `#-`) |
| `'return_target'` | leaves the document as it is |
| `'raise_exception'` | is an error (SQLSTATE `22004`) |
- **Returns:** `JSON` (`jsonb`). A new value that is not `NULL` works exactly as in `jsonb_set`.
- **NULL:** a `NULL` document, path or `create_missing` gives `NULL`. A `NULL` treatment, or a word not in the table, is an error.
**Example: Clear a key when the app sends no value**
```sql
SELECT jsonb_set_lax('{"phone": "01-5970015", "city": "Kathmandu"}', '{phone}', NULL, true, 'delete_key') AS removed,
jsonb_set_lax('{"phone": "01-5970015", "city": "Kathmandu"}', '{phone}', NULL) AS json_null;
```
Result:
| removed (JSON) | json_null (JSON) |
|---|---|
| {"city":"Kathmandu"} | {"phone":null,"city":"Kathmandu"} |
(1 row)
## jsonb_insert
Syntax:
```sql
jsonb_insert(document, path_array, new_value [, insert_after])
```
The document with `new_value` added at the path, never replacing anything (PostgreSQL).
- **In a list:** the last step is a place; the value goes **before** it (`insert_after` true: after it). `-1` is the last element. A place past either end adds at that end.
- **In an object:** the last step is a new key. A key that is already there is an error (SQLSTATE `22023`): use [jsonb_set](functions-json.md#jsonb_set) to change it.
- **Returns:** `JSON` (`jsonb`). When the path's parent is not there, the document comes back unchanged.
- **NULL:** any `NULL` argument gives `NULL`.
**Example: Add a tag at the front, at the end, and a new key**
```sql
SELECT jsonb_insert(profile, '{tags,0}', '"listed"') AS first_tag,
jsonb_insert(profile, '{tags,-1}', '"retail"', true) AS last_tag,
jsonb_insert(profile, '{office,fax}', '"01-5970016"') AS with_fax
FROM companies
WHERE symbol = 'CITY';
```
Result:
| first_tag (JSON) | last_tag (JSON) | with_fax (JSON) |
|---|---|---|
| {"website":"https://www.citybank.example.com","office":{"city":"Kathmandu","phone":"01-5970015"},"branches":270,"tags":["listed","bank","A class"]} | {"website":"https://www.citybank.example.com","office":{"city":"Kathmandu","phone":"01-5970015"},"branches":270,"tags":["bank","A class","retail"]} | {"website":"https://www.citybank.example.com","office":{"city":"Kathmandu","phone":"01-5970015","fax":"01-5970016"},"branches":270,"tags":["bank","A class"]} |
(1 row)
**Example: An existing key is not replaced** (shows an error)
```sql
SELECT jsonb_insert('{"branches": 270}', '{branches}', '300');
```
Result:
Error: `line 1, column 8: jsonb_insert: cannot replace existing key (use jsonb_set to replace the value of a key)`
## jsonb_strip_nulls, json_strip_nulls
Syntax:
```sql
jsonb_strip_nulls(document)
json_strip_nulls(document)
```
The document without the keys whose value is JSON `null`, at every level. A `null` inside a list stays.
- **Returns:** `JSON` (`jsonb_strip_nulls`: `jsonb`). **NULL:** `NULL` gives `NULL`.
- **Notes:** a key written twice counts once in `jsonb_strip_nulls`, with its last value (`{"a":1,"a":null}` gives `{}`); `json_strip_nulls` keeps both and removes only the `null` one.
**Example**
```sql
SELECT jsonb_strip_nulls('{"eps": 12.1, "dividend": null, "notes": {"audit": null}, "q": [null, 1]}') AS stripped;
```
Result:
| stripped (JSON) |
|---|
| {"eps":12.1,"notes":{},"q":[null,1]} |
(1 row)
## jsonb_pretty
Syntax:
```sql
jsonb_pretty(document)
```
The document as text on many lines, four spaces per level, exactly as PostgreSQL writes it (keys in `jsonb` order: shorter keys first).
- **Returns:** `STRING`. **NULL:** `NULL` gives `NULL`.
**Example**
```sql
SELECT jsonb_pretty(profile) FROM companies WHERE symbol = 'COAST';
```
Result:
| jsonb_pretty(profile) (STRING) |
|---|
| { "tags": [ "bank" ], "office": { "city": "Kathmandu" }, "website": "https://www.coastalbank.example.com", "branches": 380 } |
(1 row)
## jsonb_object
Syntax:
```sql
jsonb_object('{key, value, key, value ...}')
jsonb_object('{{key, value}, {key, value} ...}')
jsonb_object(keys_array, values_array)
```
A JSON object from a text array of pairs, or from a list of keys and a list of values (PostgreSQL). Every value becomes a JSON **string**.
- **Returns:** `JSON` (`jsonb`). A key given twice keeps its last value.
- **NULL:** a `NULL` value becomes JSON `null`; a `NULL` key is an error (`22004`). An odd number of elements, rows that are not pairs, or lists of different lengths are errors (`2202E`).
**Example**
```sql
SELECT jsonb_object('{city,Kathmandu,branches,270}') AS from_pairs,
jsonb_object(ARRAY['city', 'branches'], ARRAY['Pokhara', '12']) AS from_two_lists;
```
Result:
| from_pairs (JSON) | from_two_lists (JSON) |
|---|---|
| {"city":"Kathmandu","branches":"270"} | {"city":"Pokhara","branches":"12"} |
(1 row)
## array_to_json
Syntax:
```sql
array_to_json(array [, pretty])
```
An array as a JSON list: `ARRAY[1, 2]` gives `[1,2]`, `ARRAY['a', 'b']` gives `["a","b"]`.
- **Returns:** `JSON`. With `pretty` true: the text PostgreSQL gives, a line break after each comma of the outer list (as `STRING`, since KarelaDB keeps JSON values compact).
- **NULL:** `NULL` gives `NULL`.
**Example**
```sql
SELECT array_to_json(ARRAY[513, 515.6, 527.1]) AS prices;
```
Result:
| prices (JSON) |
|---|
| [513,515.6,527.1] |
(1 row)
## json_extract_path, jsonb_extract_path
Syntax:
```sql
json_extract_path(document, key [, key ...])
jsonb_extract_path(document, key [, key ...])
```
The part of the document at those steps, **as JSON**. Each step is one argument: a key of an object, or the number of a place in a list (counting from `0`).
| Argument | Type | Meaning |
|---|---|---|
| `document` | `JSON` | the document to read |
| `key` | `STRING` | one step down: a key name, or a number for a list |
- **Returns:** `JSON`. With no steps: the whole document.
- **NULL:** a step that is not there gives `NULL`. A `NULL` document gives `NULL`.
- **Notes:** the same as the operator `document #> '{office,city}'`. For plain text instead of JSON use [json_extract_path_text](functions-json.md#json_extract_path_text). For a `'$.office.city'` path use [json_extract](functions-json.md#json_extract).
**Example: City and first tag of a company**
```sql
SELECT json_extract_path(profile, 'office', 'city') AS city,
json_extract_path(profile, 'tags', '0') AS first_tag,
json_extract_path(profile, 'office', 'street') AS not_there
FROM companies
WHERE symbol = 'CITY';
```
Result:
| city (JSON) | first_tag (JSON) | not_there (STRING) |
|---|---|---|
| "Kathmandu" | "bank" | NULL |
(1 row)
## json_extract_path_text, jsonb_extract_path_text
Syntax:
```sql
json_extract_path_text(document, key [, key ...])
jsonb_extract_path_text(document, key [, key ...])
```
The same as `json_extract_path`, but the answer is **text**: a JSON string comes back without its quotes.
| Argument | Type | Meaning |
|---|---|---|
| `document` | `JSON` | the document to read |
| `key` | `STRING` | one step down: a key name, or a number for a list |
- **Returns:** `STRING`.
- **NULL:** a step that is not there, or a JSON `null`, gives `NULL`.
- **Notes:** the same as the operator `document #>> '{office,city}'`. Text cannot be used in sums: cast it first (`::DOUBLE`).
**Example: Company office cities as plain text**
```sql
SELECT symbol,
json_extract_path_text(profile, 'office', 'city') AS city,
json_extract_path_text(profile, 'website') AS website
FROM companies
ORDER BY symbol;
```
Result:
| symbol (STRING) | city (STRING) | website (STRING) |
|---|---|---|
| CITY | Kathmandu | https://www.citybank.example.com |
| COAST | Kathmandu | https://www.coastalbank.example.com |
| VOLT | NULL | https://www.voltriver.example.com |
(3 rows)
## json_typeof, jsonb_typeof
Syntax:
```sql
json_typeof(value)
jsonb_typeof(value)
```
What kind of JSON the value is: `object`, `array`, `string`, `number`, `boolean` or `null`.
| Argument | Type | Meaning |
|---|---|---|
| `value` | `JSON` (or text that holds JSON) | the value to look at |
- **Returns:** `STRING`.
- **NULL:** a SQL `NULL` gives `NULL`. The JSON value `null` gives the text `null`.
- **Notes:** the same as [json_type](functions-json.md#json_type). Use it before you read a part whose shape is not the same in every row.
**Example: Which profiles have a list of tags?**
```sql
SELECT symbol,
json_typeof(profile) AS profile_is,
jsonb_typeof(profile -> 'tags') AS tags_is
FROM companies
ORDER BY symbol;
```
Result:
| symbol (STRING) | profile_is (STRING) | tags_is (STRING) |
|---|---|---|
| CITY | object | array |
| COAST | object | array |
| VOLT | object | NULL |
(3 rows)
## jsonb_array_length
Syntax:
```sql
jsonb_array_length(json_array)
```
How many elements a JSON list has.
| Argument | Type | Meaning |
|---|---|---|
| `json_array` | `JSON` | a JSON list |
- **Returns:** `LONG`.
- **NULL:** a `NULL` value gives `NULL`.
- **Notes:** the PostgreSQL name for [json_array_length](functions-json.md#json_array_length); both give the same number. A value that is not a list gives `NULL`, as in PostgreSQL.
**Example: How many tags does a company have**
```sql
SELECT symbol, jsonb_array_length(profile -> 'tags') AS tags
FROM companies
WHERE symbol = 'CITY';
```
Result:
| symbol (STRING) | tags (LONG) |
|---|---|
| CITY | 2 |
(1 row)
**Example: The same number three ways**
```sql
SELECT symbol,
json_array_length(profile -> 'tags') AS tags,
cardinality(profile -> 'tags') AS same_number
FROM companies
ORDER BY symbol;
```
Result:
| symbol (STRING) | tags (LONG) | same_number (LONG) |
|---|---|---|
| CITY | 2 | 2 |
| COAST | 1 | 1 |
| VOLT | NULL | NULL |
(3 rows)
## -, #- (delete from a document)
Syntax:
```sql
document - 'key'
document - ARRAY['key1', 'key2']
document - position
document #- '{step,step,...}'
```
The document without some part of it (PostgreSQL). The saved row does not change until you `UPDATE` it.
| Form | What goes |
|---|---|
| `doc - 'key'` | that key of an object; in a list, every string element equal to `'key'` |
| `doc - ARRAY['a', 'b']` | each of those keys |
| `doc - 2` | the element at that place of a list, counting from `0`; `-1` is the last |
| `doc #- '{office,city}'` | the part the path points to; a number step is a place in a list |
- **Returns:** `JSON`. A key or place that is not there leaves the document as it is.
- **NULL:** a `NULL` document or argument gives `NULL`.
- **Notes:** a number on an object (`'{"a":1}'::jsonb - 0`) and anything on a plain value (`'5'::jsonb - 'a'`) is an error, as in PostgreSQL. A path step that is not a number where the document has a list is an error too. [json_remove](functions-json.md#json_remove) does the same with `'$.a'` paths.
**Example: Leave out keys, list elements and a nested part**
```sql
SELECT profile - 'website' AS no_website,
profile - ARRAY['website', 'office'] AS two_keys_less,
profile -> 'tags' - 0 AS tags_without_first,
profile #- '{office,phone}' AS no_phone
FROM companies
WHERE symbol = 'CITY';
```
Result:
| no_website (JSON) | two_keys_less (JSON) | tags_without_first (JSON) | no_phone (JSON) |
|---|---|---|---|
| {"office":{"city":"Kathmandu","phone":"01-5970015"},"branches":270,"tags":["bank","A class"]} | {"branches":270,"tags":["bank","A class"]} | ["A class"] | {"website":"https://www.citybank.example.com","office":{"city":"Kathmandu"},"branches":270,"tags":["bank","A class"]} |
(1 row)
**Example: A number on an object is an error** (shows an error)
```sql
SELECT '{"a": 1}'::jsonb - 0;
```
Result:
Error: `line 1, column 26: cannot delete from object using integer index`
## JSON to rows: jsonb_each, jsonb_array_elements, ...
Syntax:
```sql
SELECT * FROM jsonb_each(document) -- key, value (JSON)
SELECT * FROM jsonb_each_text(document) -- key, value (text)
SELECT * FROM jsonb_array_elements(list) -- value (JSON)
SELECT * FROM jsonb_array_elements_text(list) -- value (text)
SELECT * FROM jsonb_object_keys(document) -- jsonb_object_keys
```
These functions make **rows** from one JSON value, as in PostgreSQL: one row per key of an object, or per element of a list. The `json_` names (`json_each`, `json_array_elements`, ...) are the same functions.
| Function | Rows | Columns |
|---|---|---|
| `jsonb_each(doc)` | one per key | `key` (text), `value` (JSON) |
| `jsonb_each_text(doc)` | one per key | `key`, `value` (text: a string without quotes, JSON `null` as `NULL`) |
| `jsonb_array_elements(list)` | one per element | `value` (JSON) |
| `jsonb_array_elements_text(list)` | one per element | `value` (text) |
| `jsonb_object_keys(doc)` | one per key | `jsonb_object_keys` (text) |
- **In FROM:** the argument is a value: a constant, a `$1` parameter, or a subquery like `(SELECT profile FROM companies WHERE symbol = 'CITY')`. A column of another table in the same `FROM` (PostgreSQL's `FROM t, jsonb_each(t.doc)`) is **not** supported yet.
- **In the SELECT list:** `jsonb_array_elements`, `jsonb_array_elements_text` and `jsonb_object_keys` also work, one row per element for each row, like [unnest](functions-postgres.md#unnest). That is the way to split a column of every row.
- **NULL:** a `NULL` value gives no rows.
- **Notes:** a value of the wrong shape is an error, as in PostgreSQL: `jsonb_each` of a list, `jsonb_array_elements` of an object. `AS e(k, v)` renames the columns.
**Example: Every key and value of a profile**
```sql
SELECT key, value
FROM jsonb_each_text((SELECT profile FROM companies WHERE symbol = 'COAST'));
```
Result:
| key (STRING) | value (STRING) |
|---|---|
| website | https://www.coastalbank.example.com |
| office | {"city":"Kathmandu"} |
| branches | 380 |
| tags | ["bank"] |
(4 rows)
**Example: One row per tag of every company (SELECT list)**
```sql
SELECT symbol, jsonb_array_elements_text(profile -> 'tags') AS tag
FROM companies
WHERE profile ? 'tags'
ORDER BY symbol, tag;
```
Result:
| symbol (STRING) | tag (STRING) |
|---|---|
| CITY | A class |
| CITY | bank |
| COAST | bank |
(3 rows)
**Example: The keys of a document**
```sql
SELECT * FROM jsonb_object_keys('{"npl_pct": 2.6, "car_pct": 13.4}');
```
Result:
| jsonb_object_keys (STRING) |
|---|
| npl_pct |
| car_pct |
(2 rows)
**Example: Not an object** (shows an error)
```sql
SELECT * FROM jsonb_each('[1, 2]');
```
Result:
Error: `line 1, column 15: jsonb_each: cannot deconstruct an array as an object`
## json_to_record, json_to_recordset
Syntax:
```sql
SELECT * FROM json_to_record(document) AS r(column TYPE, ...)
SELECT * FROM json_to_recordset(list_of_objects) AS r(column TYPE, ...)
```
A JSON object as one row, or a list of objects as rows, with the columns and types you write after the alias (PostgreSQL). An app can send many rows as one JSON parameter and insert them with one statement. `jsonb_to_record` and `jsonb_to_recordset` are the same.
| Argument | Type | Meaning |
|---|---|---|
| `document` / `list_of_objects` | `JSON` or text | `{"id": 1, ...}` or `[{"id": 1, ...}, {"id": 2, ...}]` |
| `AS r(column TYPE, ...)` | | each column and its type; a column is read from the key with its name |
- **Returns:** rows with those columns. A key that is missing, or JSON `null`, gives `NULL`. Keys without a column are left out.
- **Types:** each value is converted as `CAST(value AS TYPE)`: `"2.5"` and `2.5` both give a `DOUBLE`; a `JSON` column keeps the part as JSON; a `TEXT` column gets a string without its quotes, anything else as JSON text. A value that does not fit the type is an error that names the column and the row.
- **NULL:** a `NULL` value gives no rows.
**Example: Rows from a JSON list, as an app would send them**
```sql
SELECT *
FROM json_to_recordset('[{"symbol": "CITY", "eps": 12.1, "audited": true},
{"symbol": "COAST", "eps": "6.35"}]') AS r(symbol TEXT, eps DOUBLE, audited BOOL);
```
Result:
| symbol (STRING) | eps (DOUBLE) | audited (BOOL) |
|---|---|---|
| CITY | 12.1 | true |
| COAST | 6.35 | NULL |
(2 rows)
**Example: Each column needs a type** (shows an error)
```sql
SELECT * FROM json_to_record('{"id": 1}') AS r(id);
```
Result:
Error: `line 1, column 15: json_to_record: column 'id' needs a type, as in AS r(id INT, sym TEXT)`
## jsonb_populate_record, jsonb_populate_recordset
Syntax:
```sql
SELECT * FROM jsonb_populate_record(NULL::table_name, document)
SELECT * FROM jsonb_populate_recordset(NULL::table_name, list_of_objects)
```
Like [json_to_record](#json_to_record), but the columns and their types are those of a **table** (PostgreSQL's row type). `json_populate_record(set)` is the same.
- **Returns:** rows with the table's columns, each value converted as an `INSERT` into that column would do it. Missing keys give `NULL`.
- **Notes:** the first argument must be written `NULL::table_name`; KarelaDB does not take a row of values there. It is only for `FROM` (and `INSERT INTO t SELECT * FROM ...`).
**Example: Insert rows sent as JSON into a table**
Setup:
```sql
INSERT INTO fundamentals (id, symbol, fiscal_year, quarter, eps)
SELECT id, symbol, fiscal_year, quarter, eps
FROM jsonb_populate_recordset(NULL::fundamentals,
'[{"id": 90, "symbol": "VOLT", "fiscal_year": "2083/84", "quarter": 1, "eps": "1.10"}]');
```
```sql
SELECT id, symbol, quarter, eps FROM fundamentals WHERE id = 90;
```
Result:
| id (LONG) | symbol (STRING) | quarter (INT) | eps (DECIMAL(2)) |
|---|---|---|---|
| 90 | VOLT | 1 | 1.10 |
(1 row)