FluentDBFluentDB
SQLite guides

JSON in SQLite: columns, functions, JSONB and indexes

By Kevin Piacentini, Software Engineer & Founder of FluentDB·

The first thing to know is the thing most guides skip: SQLite has no JSON column type. It can store NULL, integers, floats, text and BLOBs, and that is the whole list. JSON is text that happens to be JSON, and the JSON functions are what give it meaning.

That sounds like a limitation, and in one way it is, but it also means there is very little to learn. This page covers where JSON is actually stored, how to read values out of it, how to keep invalid JSON out of your table, what JSONB changed, and how to index it so queries stay fast.

Versions, so you know what you can use

JSON support arrived in stages, and the version you are running decides which of the following applies:

text
3.38.0  (2022-02-22)  JSON functions built in by default; -> and ->> operators added
3.45.0  (2024-01-15)  JSONB, the binary on-disk format, introduced

Check what you have with:

sql
SELECT sqlite_version();

Before 3.38.0 the JSON functions lived in an optional extension called JSON1, which had to be compiled in. On anything current you do not need to install or enable anything.

Storing JSON: it is just a text column

You can write JSON as the declared type, and it is good documentation, but SQLite applies TEXT affinity to it and does not validate anything:

sql
CREATE TABLE events (
  id      INTEGER PRIMARY KEY,
  payload TEXT
);

INSERT INTO events (payload) VALUES ('{"user_id": 7, "type": "signup"}');
INSERT INTO events (payload) VALUES ('this is not json at all');  -- accepted

That second insert succeeds. If you want the database to reject nonsense, say so explicitly with a CHECK constraint:

sql
CREATE TABLE events (
  id      INTEGER PRIMARY KEY,
  payload TEXT NOT NULL CHECK (json_valid(payload))
);
This CHECK is the single highest-value line in this article. Without it, one bad write puts a row in your table that every subsequent json_extract will silently return NULL for, and you will debug the query instead of the data.

Reading values out: -> and ->> versus json_extract

There are three ways to pull a value out, and the difference between them matters:

sql
SELECT
  json_extract(payload, '$.user_id') AS a,   -- SQL value: 7
  payload -> '$.user_id'             AS b,   -- JSON:      7  (as JSON text)
  payload ->> '$.user_id'            AS c    -- SQL value: 7
FROM events;

->> and json_extract give you a normal SQL value: an integer, a real, or text. -> gives you a JSON representation, which is what you want when you are extracting a nested object or array to pass to another JSON function.

The trap is comparisons. A JSON string value carries its quotes, so this returns nothing:

sql
-- wrong: '"signup"' (JSON) is never equal to 'signup' (SQL text)
SELECT * FROM events WHERE payload -> '$.type' = 'signup';

-- right
SELECT * FROM events WHERE payload ->> '$.type' = 'signup';

Path syntax is the usual thing: $.a.b for nested objects, $.items[0] for array elements, and $.items[#-1] for the last element of an array.

Querying arrays with json_each and json_tree

These two turn JSON into rows, which is what makes JSON in SQLite genuinely useful rather than just storable. json_each walks one level, json_tree walks the whole structure recursively:

sql
-- one row per tag, joined back to the event
SELECT e.id, t.value AS tag
FROM events e, json_each(e.payload, '$.tags') t;

-- find every event that has a given tag anywhere in the document
SELECT DISTINCT e.id
FROM events e, json_tree(e.payload) t
WHERE t.value = 'urgent';

Both are table-valued functions, so they go in the FROM clause and join like any other table. This is how you filter on array contents, which is otherwise awkward.

Indexing JSON

A query that filters on json_extract will scan every row unless you give it an index. SQLite supports indexes on expressions, which is exactly what is needed:

sql
CREATE INDEX idx_events_user
  ON events (json_extract(payload, '$.user_id'));

-- uses the index
SELECT * FROM events WHERE json_extract(payload, '$.user_id') = 7;
The expression in the query must match the expression in the index. An index built on json_extract(payload, '$.user_id') will not be used by a query written with payload ->> '$.user_id', even though the two return the same value. Pick one form and use it consistently.

Confirm the planner is actually using it rather than assuming, with EXPLAIN QUERY PLAN in front of your SELECT. If the output says SCAN rather than SEARCH, the index is not being used.

JSONB: what changed in 3.45

SQLite 3.45.0, released on 15 January 2024, added JSONB: the parsed internal representation of JSON, stored on disk as a BLOB rather than re-parsed from text on every read.

sql
-- store the binary form
CREATE TABLE events (id INTEGER PRIMARY KEY, payload BLOB);
INSERT INTO events (payload) VALUES (jsonb('{"user_id": 7}'));

-- read it back as text when you need to see it
SELECT json(payload) FROM events;

-- the extraction functions work on both forms
SELECT payload ->> '$.user_id' FROM events;

Two things worth being clear about:

  • It is not human-readable. Select the column directly and you get a blob. Wrap it in json() to get text back.
  • It is not PostgreSQL's JSONB.The name is shared, the format is not, and SQLite's documentation describes it as an internal format rather than an interchange one. Do not move the bytes between the two databases.

Use it when a column is read far more often than it is written and the documents are large enough for parsing to cost something. For small payloads, plain text is simpler and the difference will not be measurable.

Building JSON out of query results

The functions work in both directions. json_object builds an object per row and json_group_array aggregates rows into an array:

sql
SELECT json_group_array(json_object('id', id, 'name', name)) AS users
FROM users;

-- [{"id":1,"name":"Ada"},{"id":2,"name":"Grace"}]

This is also how you nest a child collection inside a parent row, returning a whole object graph from a single query instead of running one query per parent:

sql
SELECT json_object(
         'id', u.id,
         'name', u.name,
         'orders', (SELECT json_group_array(json_object('id', o.id, 'total', o.total))
                    FROM orders o WHERE o.user_id = u.id)
       ) AS user
FROM users u;

You can try any of these against your own database in our free browser-based SQLite viewer, which runs read-only queries with nothing to install.

Run these against a real database

Our free SQLite viewer opens a .sqlite, .db or .sqlite3 file in your browser, with nothing to install and no upload, so you can try the queries on this page against your own data.

Open the free SQLite viewer

Or stop memorising pragmas

FluentDB shows your SQLite schema without a single command, edits rows in place, and has an AI co-pilot that writes the SQL when you ask in plain English.

Download FluentDB for Mac

SQLite JSON FAQ

Does SQLite have a JSON column type?

No. SQLite can only store NULL, integers, floats, text, and BLOBs, so JSON is stored as ordinary text, or as a BLOB if you use the JSONB format added in 3.45.0. You can declare a column as JSON, but SQLite treats that declaration as TEXT affinity; nothing validates it unless you add a CHECK constraint.

What is JSONB in SQLite?

JSONB is SQLite's internal binary representation of parsed JSON, stored on disk as a BLOB. It was introduced in SQLite 3.45.0, released on 15 January 2024. It skips the parsing step on every read, so it is faster to query, at the cost of not being human-readable and not being the same format as PostgreSQL's JSONB.

How do I extract a value from JSON in SQLite?

Use json_extract(column, '$.path'), or the -> and ->> operators added in SQLite 3.38.0. The ->> operator returns a plain SQL value such as text or a number, while -> returns a JSON representation, which is the distinction that catches people out in WHERE clauses.

Can I index a JSON field in SQLite?

Yes, with an expression index on the extraction. CREATE INDEX idx ON events (json_extract(payload, '$.user_id')) will be used by queries that filter on exactly that same expression. The expression in the query has to match the one in the index.

Do I need to install anything to use the JSON functions?

No, not on any current version. The JSON functions have been built into SQLite by default since version 3.38.0, released on 22 February 2022. On older builds they lived in an optional extension called JSON1.

How do I turn query results into JSON?

Use json_object() to build an object per row and json_group_array() to aggregate rows into a JSON array. Together they turn any result set into a single JSON document, which is useful for exporting or for returning a nested structure from one query.