Working with JSON¶
SQLite stores JSON in one of two ways, and it matters which one you have:
- JSON text — an ordinary
TEXTvalue that happens to hold JSON. Readable in any tool, and you can see it without decoding anything. - JSONB — a
BLOBholding SQLite's own binary form of the same document. It is faster for SQLite to read, since it does not have to parse text every time, but it is a blob: anything that has not decoded it sees only bytes.
Every JSON function accepts either form, so most of what follows works the same whichever you have.
The demo database has one table of each kind:
| Table | Column | Stored as |
|---|---|---|
country_json |
data |
JSON text, indented across several lines |
country_json |
data_b |
JSONB — the same document as data |
orders_json |
data |
JSONB — one document per order, with a nested object and an array |
country_json exists to compare the two storage forms; orders_json shows the shape JSON is actually useful for.
Tip
You do not have to read JSON as raw text. Right-click a cell and choose JSON Editor to see it laid out, whichever form it is stored in — see Editing table data. To see JSONB in the grid rather than as (blob), turn on Show JSONB as readable JSON.
Reading a value out¶
json_extract takes a path and returns what it finds:
select name,
json_extract(data, '$.capital') as capital,
json_extract(data, '$.population') as population
from country_json;
The ->> operator is usually easier to read, and there are two of them:
select name,
data ->> '$.capital' as as_text, -- Brasilia
data -> '$.capital' as as_json -- "Brasilia"
from country_json;
->> returns an ordinary SQL value; -> returns JSON. Note the quotes in the second column: it is still a JSON string. Use ->> when you want to compare, sort or display the value, and -> when you are feeding the result into another JSON function.
Both storage forms answer identically — the only difference is what the column holds:
select name,
data ->> '$.capital' as from_text,
data_b ->> '$.capital' as from_jsonb,
typeof(data) as text_type, -- text
typeof(data_b) as jsonb_type -- blob
from country_json
where name = 'Brazil';
Searching, and making it fast¶
A JSON field can be used anywhere an expression can:
select name, data ->> '$.population' as population
from country_json
where data ->> '$.continent' = 'South America'
order by 2 desc;
That reads every row. An expression index fixes it, and the demo database has one:
explain query plan
select name from country_json where json_extract(data, '$.continent') = 'Europe';
-- SEARCH country_json USING INDEX idx_country_json_continent (<expr>=?)
Warning
The query has to spell the expression the same way the index does. json_extract(data,'$.continent') uses the index above; the same test written as data ->> '$.continent' does not, even though it returns the same answer. Pick one spelling and use it in both places.
Working with nested documents¶
orders_json holds one document per order, each with a customer object and an items array:
{
"orderNo": 1003,
"saleDate": "1988-04-12 00:00:00.000",
"customer": { "custNo": 1351, "company": "Sight Diver", "city": "Kato Paphos",
"state": null, "country": "Cyprus" },
"shipVia": "UPS", "terms": "FOB", "payment": "Credit", "total": 1250,
"items": [ { "itemNo": 1, "partNo": 1313, "qty": 5, "discount": 0.0 } ]
}
Reach into a nested object with a longer path, and into an array by position:
select data ->> '$.customer.company' as company,
data ->> '$.items[0].partNo' as first_part,
json_array_length(data, '$.items') as line_count
from orders_json
where orderNo = 1003;
Because it is just an expression, a nested field groups and aggregates like any column:
select data ->> '$.customer.country' as country,
count(*) as orders,
round(sum(data ->> '$.total'), 2) as total
from orders_json
group by 1
order by orders desc;
Turning an array back into rows¶
json_each gives one row per element, which is how a document goes back to being relational:
select o.orderNo,
i.value ->> '$.partNo' as partNo,
i.value ->> '$.qty' as qty
from orders_json o, json_each(o.data, '$.items') i
where o.orderNo = 1003;
And once it is rows, it joins to ordinary tables:
select p.Description,
sum(i.value ->> '$.qty') as units
from orders_json o, json_each(o.data, '$.items') i
join parts p on p.PartNo = i.value ->> '$.partNo'
group by p.Description
order by units desc
limit 10;
json_tree is the same idea but walks the whole document, however deep, giving a fullkey such as $.customer.city or $.items[0].qty:
Building JSON from rows¶
The reverse direction uses json_object and json_group_array:
select json_group_array(json_object('name', Name, 'capital', Capital)) as countries
from country
where Continent = 'South America';
jsonb_object and jsonb_group_array are the same functions producing JSONB, which is what you want when the result is going into a blob column. This is how the demo tables themselves were built — the script is Tools/dbdemos_json.sql in the source distribution.
Changing a value¶
json_set adds or replaces, json_insert only adds, json_replace only replaces, and json_remove deletes:
update country_json
set data = json_set(data, '$.currency', 'BRL', '$.population', 214000000)
where name = 'Brazil';
For a JSONB column use the jsonb_ form, so the column keeps its storage form:
Using plain json_set there would turn the blob into text, and the column would quietly stop being JSONB.
These functions reformat
Every json_* function rebuilds the value it returns, so whatever line breaks and indenting the original had are gone. country_json.data is stored indented across several lines; after a json_set it comes back on one line.
This is why the JSON Editor does not use these functions on stored text: it writes back exactly what you left in the editor, so formatting survives being edited.
Checking what you have¶
json_valid answers without ever raising, which makes it the safe thing to test with — json_extract and json() raise malformed JSON on input they cannot parse:
select json_valid(data) as is_json_text, -- 1
json_valid(data, 4) as is_jsonb, -- 0, it is text
json_valid(data_b, 4) as blob_is_jsonb, -- 1
json_type(data) as top_level -- object
from country_json
where name = 'Brazil';
The second argument selects what to test for: 1 for JSON text, 4 for JSONB. These are the same checks SQLite Expert makes when deciding whether the JSON Editor can open a cell.
json(x) converts either form to text, which is the way to read a JSONB value in a tool that does not decode it for you:
See also: Editing table data · Changing options · Exporting data to JSON