Course outline

SQLite / Intermediate: query structure and transactional design / Lesson 12

JSON functions, scalar types, and JSONB

Use SQLite JSON data deliberately while distinguishing SQL NULL, JSON null, text values, arrays, and the opaque version-gated JSONB representation.

About 5 min readContent reviewed
On this page

Lesson goals

  • Validate document shape and extracted scalar meaning.
  • Distinguish missing paths from JSON null.
  • Explain JSONB compatibility and table-valued expansion.

JSON does not remove relational requirements

A JSON document can represent optional or evolving payload data, but stable identifiers, required relationships, and frequently constrained fields may belong in ordinary columns. Declare the document's version, required paths, scalar types, size limits, and unknown-field policy. A syntactically valid document can still violate every business requirement. json_valid checks syntax under its chosen flags, not a complete application schema.

JSON functions are built in by default from SQLite3.38.0, but builds can omit support. Check the deployed runtime. This course's3.45.1 environment supports the demonstrated functions and JSONB, which began in3.45.0. These boundaries identify tested features rather than promising that every older mobile or framework library contains them.

Inspect extracted SQL types and path existence

With a single path, json_extract can return an SQL integer, real, text, or NULL rather than always returning a JSON-formatted string. A JSON Boolean can become an integer1 or0. The first script extracts count2 and true as1. This behavior matters when comparing, binding, and aggregating results; do not assume another engine's JSON-returning conventions apply.

Both an explicit JSON null and a missing path can extract as SQL NULL. json_type distinguishes a present JSON null, returning the text null, from an absent path returning SQL NULL. The displayed uppercase NULL marker is the course's result formatter, not the stored JSON token. State which absence means “not provided” and which means an explicit null assignment.

sql Distinguish JSON null from an absent path
CREATE TABLE event(id INTEGER PRIMARY KEY,payload TEXT NOT NULL CHECK(json_valid(payload))) STRICT;
INSERT INTO event VALUES(1,'{"count":2,"active":true,"optional":null}');
SELECT json_extract(payload,'$.count') AS count,
 typeof(json_extract(payload,'$.count')) AS storage,
 json_extract(payload,'$.active') AS active,
 json_type(payload,'$.optional') AS present_null,
 json_type(payload,'$.absent') AS missing FROM event ORDER BY id;
Expected output
count|storage|active|present_null|missing
2|integer|1|null|NULL

count is SQL integer2 and true becomes1. json_type returns the text null for the present path and SQL NULL for the missing one.

Keep JSONB opaque and version-aware

SQLite JSONB stores an internal binary representation as a BLOB and is not PostgreSQL's JSONB format. Applications should not manufacture or reverse-engineer it as a portable wire format. Use documented functions and a deliberate interchange representation. JSONB can reduce parsing or storage work, but it does not promise constant-time lookup for every operation or automatic indexing of every path.

The second script constructs JSONB with jsonb(), validates it using the chosen strict JSONB flag8, and expands an array with json_each. The extracted count remains2 and the two array elements become separate rows. Persisted JSONB requires compatible readers and migrations; a file backup alone does not make an old SQLite library understand a newer feature.

sql Construct opaque JSONB and expand a known array
CREATE TABLE document(id INTEGER PRIMARY KEY,payload BLOB NOT NULL CHECK(json_valid(payload,8))) STRICT;
INSERT INTO document VALUES(1,jsonb('{"count":2,"tags":["x","y"]}'));
SELECT typeof(payload) AS storage,json_extract(payload,'$.count') AS count FROM document ORDER BY id;
SELECT d.id,j.key,j.value FROM document AS d, json_each(d.payload,'$.tags') AS j ORDER BY d.id,j.key;
Expected output
storage|count
blob|2

id|key|value
1|0|x
1|1|y

Storage is BLOB while scalar extraction remains integer2. Array positions0 and1 yield x and y in explicit order.

Separate a string value from a structured JSON value

JSON construction functions distinguish ordinary SQL value arguments from values produced as JSON by appropriate functions. Supplying the SQL text '[1,2]' to a value position can create a JSON string rather than an array; wrapping it with json() expresses structured JSON. Validate what shape the application expects and inspect json_type instead of relying on a familiar-looking printed substring.

Paths and keys need validation when selected dynamically, and ordinary values should remain bound through the host interface. JSON data does not excuse SQL string concatenation. A generated column or expression index can expose a frequently queried scalar under documented deterministic-expression and version rules, but extra indexes and constraints still carry write costs and must match the exact query expression.

Treat malformed data and legacy BLOB behavior carefully

Malformed JSON text can raise errors when functions attempt to interpret it. Validate at the boundary and distinguish syntax, path, type, and missingness failures. SQLite's documented legacy JSON-in-BLOB behavior changed around3.45 releases and has compatibility caveats; use intended TEXT JSON or constructed JSONB rather than depending on accidental BLOB interpretation. Do not treat the JSONB format as a place to insert arbitrary bytes.

Test explicit null, absent keys, arrays, scalar types, malformed input, and version compatibility with every consuming application. Keep original synthetic or approved input for diagnosis without exposing sensitive payloads. A good document contract explains which values are queried relationally, how schema evolution occurs, and what happens when a reader lacks the needed JSON capability.

Common errors and fixes

malformed JSON

Why it happens
A function received invalid document text or incompatible bytes.
How to fix it
Validate intended representation and reject or quarantine invalid input.
Prevent it next time
Test syntax and size at ingestion.

A supposed array is stored as a JSON string.

Why it happens
An SQL text value was mistaken for structured JSON input.
How to fix it
Use the documented JSON-producing function and verify json_type.
Prevent it next time
Test constructed shape, not only printed text.

An older reader fails on JSONB.

Why it happens
Feature compatibility was omitted from the file contract.
How to fix it
Use a compatible runtime or an explicitly migrated interchange/storage design.
Prevent it next time
Record JSON feature requirements per consumer.

Check your understanding

1. json_extract returns NULL for two paths. How can you distinguish explicit JSON null from an absent key?

Show worked solution

Use json_type for each path. A present JSON null yields the text null, while a missing path yields SQL NULL. Decide how those states map into the application contract rather than replacing both automatically with the same default.

2. Does storing JSONB guarantee PostgreSQL compatibility or constant-time lookup?

Show worked solution

No. It is SQLite's own opaque representation and must be handled through its documented APIs. Its efficiency characteristics do not imply universal O(1) lookup or automatic relational indexes. Choose interchange and indexing policies separately.

Key takeaways

  • JSON syntax validation is not a full domain schema.
  • SQL NULL, JSON null, missing paths, and text values differ.
  • JSONB has explicit version, representation, and interoperability limits.

Official references