SQL JSON

A Loja Aurora vai ganhar um aplicativo de celular. O programador do aplicativo pediu à Ana: "me mande os pedidos em JSON". JSON é o formato de texto que quase todo aplicativo e site usa para trocar dados. Nesta aula você vai fazer o próprio banco montar esse texto, ler um JSON guardado numa coluna e desmontar uma lista JSON em linhas de tabela. No fim, você monta o JSON completo de um pedido, com todos os seus itens, numa consulta só.

Você vai aprender
  • O que é JSON: objetos {…}, listas […] e valores.
  • Montar JSON com json_object() e json_array().
  • Ler um pedaço de um JSON com json_extract() e com os operadores -> e ->>, e a diferença entre os dois.
  • Guardar JSON numa coluna e garantir que ele é válido com json_valid().
  • Transformar uma lista JSON em linhas com json_each().
  • Juntar várias linhas numa lista JSON com json_group_array() e json_group_object().
  • Montar o JSON de um pedido com seus itens, e as mensagens reais de erro do SQLite.

Todos os exemplos rodam de verdade no seu navegador, no SQLite 3.50 com o banco da Loja Aurora (aula O Banco da Loja Aurora). As funções de JSON já vêm prontas no SQLite desde a versão 3.38: não é preciso instalar nada.


A ideia: uma ficha que viaja

Uma linha de tabela só faz sentido junto do cabeçalho: o valor 12.35 sozinho não diz nada; é a coluna preco que dá o sentido. Quando o dado sai do banco e vai para um aplicativo, ele precisa levar o "cabeçalho" junto. É como trocar a planilha por uma ficha preenchida, em que cada valor vem com o nome do campo ao lado:

{"id":1,"nome":"Caderno Pautado 96 folhas","preco":12.35}

Isso é JSON (de JavaScript Object Notation, "notação de objetos do JavaScript"; pronuncia-se "jêison"). Apesar do nome, qualquer linguagem lê JSON. Ele tem só três peças:

PeçaEscritaComparaçãoExemplo
Objetoentre chaves { }, pares "nome": valor separados por vírgulauma ficha com campos{"nome":"Ana","vip":1}
Lista (array)entre colchetes [ ], valores separados por vírgulauma lista de compras["livros","jardim"]
Valortexto entre aspas duplas, número, true, false, null, ou outro objeto/listao que vai escrito no campo"Teresina", 4.9, null

Para o SQLite, um JSON é só um texto (tipo TEXT). O que as funções desta aula fazem é escrever esse texto sem erro (com as aspas e vírgulas no lugar) e saber andar dentro dele.

Aspas duplas no JSON, aspas simples no SQL

Dentro do JSON, texto vai entre aspas duplas: "Ana". No SQL, texto vai entre aspas simples: 'Ana'. Por isso um JSON escrito dentro de um comando SQL fica assim: '{"nome":"Ana"}', aspas simples por fora (é um texto do SQL) e duplas por dentro (é o JSON).


1. Montando um objeto: json_object()

Primeiro, três produtos do jeito de sempre, em tabela:

SELECT id, nome, preco
FROM produtos
WHERE id <= 3;

Agora os mesmos três, cada um virando uma ficha JSON:

SELECT json_object('id', id, 'nome', nome, 'preco', preco) AS produto
FROM produtos
WHERE id <= 3;
json_object(
"objeto JSON": monta um objeto {…}. Os argumentos vêm em pares: nome do campo, valor do campo, nome, valor…
'id', id
O primeiro par. 'id' (com aspas simples) é o nome que o campo vai ter no JSON; id (sem aspas) é a coluna de onde vem o valor.
'nome', nome
Segundo par. O SQLite põe as aspas duplas em volta do texto sozinho.
'preco', preco
Terceiro par. Número vai sem aspas no JSON: 12.35.
AS produto
O apelido da coluna (aula Apelidos (AS)). O resultado é uma coluna só, com um texto JSON por linha.

O nome do campo no JSON não precisa ser igual ao da coluna: json_object('valor', preco) gera {"valor":12.35}. Quem escolhe é você (ou o programador do aplicativo, que vai ler o JSON).

E o NULL? Ele vira o null do JSON. O cliente 7 não tem email (é um dos casos do banco):

SELECT json_object('nome', nome, 'email', email, 'vip', vip) AS cliente
FROM clientes
WHERE id IN (1, 7);

Repare também no "vip":1: no banco da Aurora o VIP é guardado como 0 ou 1, e é esse número que vai para o JSON. O SQLite não tem um tipo "verdadeiro/falso" (aula Os Tipos no SQLite), então ele não adivinha que você queria true.

Missão: o aplicativo quer a lista dos clientes VIP, cada um como uma ficha JSON com dois campos: nome e cidade, nessa ordem. Troque as duas colunas soltas por um json_object().

SELECT nome, cidade
FROM clientes
WHERE vip = 1;
SELECT json_object('nome', nome, 'cidade', cidade) AS cliente
FROM clientes
WHERE vip = 1;

2. Montando uma lista: json_array()

O json_array() ("lista JSON") junta os valores que você passar, na ordem, entre colchetes:

SELECT json_array('Teresina', 'Fortaleza', 42, 4.9, NULL) AS lista;

Os textos ganharam aspas duplas, os números ficaram sem aspas e o NULL virou null. Uma lista pode ter valores de tipos diferentes misturados.

Um dentro do outro

O valor de um campo pode ser outra lista (ou outro objeto). Basta colocar uma função dentro da outra:

SELECT json_object('nome', 'Ana', 'gosta', json_array('livros', 'jardim')) AS ficha;

O json_object() percebeu que o valor de gosta já era JSON (veio de uma função de JSON) e o encaixou como lista. Guarde isso: em Quando dá errado você vai ver o que acontece quando o valor é um texto que só parece JSON.


3. Lendo um pedaço: json_extract()

Agora o caminho contrário: você tem um JSON e quer um valor lá de dentro. Para isso, diga o caminho (path) até ele:

SELECT json_extract('{"nome":"Ana","cidade":"Teresina","gosta":["livros","jardim"]}', '$.cidade') AS cidade;
json_extract(
"extrair do JSON". Recebe o JSON e o caminho, e devolve o valor que estiver lá.
'{"nome":"Ana",…}'
O JSON, como texto do SQL (aspas simples por fora).
'$.cidade'
O caminho. O $ é "o JSON inteiro"; .cidade é "o campo cidade dele". Leia como "do JSON, pegue a cidade".

Os caminhos podem descer quantos níveis precisar. Numa lista, a posição vai entre colchetes, e a contagem começa do zero:

SELECT
  json_extract('{"nome":"Ana","gosta":["livros","jardim"]}', '$.gosta')       AS lista,
  json_extract('{"nome":"Ana","gosta":["livros","jardim"]}', '$.gosta[0]')    AS primeiro,
  json_extract('{"nome":"Ana","gosta":["livros","jardim"]}', '$.gosta[#-1]')  AS ultimo,
  json_extract('{"nome":"Ana","gosta":["livros","jardim"]}', '$.idade')       AS idade;
CaminhoQuer dizerAqui deu
$o JSON inteiroo próprio JSON
$.gostao campo gostaa lista ["livros","jardim"]
$.gosta[0]o primeiro item da lista (posição 0)livros
$.gosta[#-1]o último item (# é o tamanho da lista; #-1, a última posição)jardim
$.idadeum campo que não existeNULL, sem erro
$.a.bo campo b de dentro do objeto a(desce um nível)

Repare na última coluna do resultado: pedir um campo que não existe não é erro, dá NULL. Isso é bom (um cliente sem idade não derruba a consulta) e é perigoso (um nome de campo digitado errado também dá NULL calado).


4. Os atalhos -> e ->>

Escrever json_extract(…) o tempo todo cansa. O SQLite tem dois operadores que fazem quase a mesma coisa, com uma setinha: -> (uma seta) e ->> (seta dupla). A diferença entre eles é o que volta:

SELECT
  '{"nome":"Ana"}' -> '$.nome'   AS seta,
  '{"nome":"Ana"}' ->> '$.nome'  AS seta_dupla,
  '{"nome":"Ana"}' ->> 'nome'    AS sem_cifrao;
->
Devolve o pedaço como JSON. Um texto no JSON tem aspas duplas, então volta "Ana", com as aspas. Serve para quando o resultado vai continuar sendo JSON (para encaixar em outro JSON, por exemplo).
->>
Devolve o pedaço como valor do SQL: o texto Ana, sem aspas; um número vira número de verdade. É o que você quer para comparar, somar, ordenar.
'nome' (sem $.)
Atalho dos atalhos: com as setas, um nome simples vale como '$.nome', e um número como 2 vale como '$[2]'. (No json_extract() isso não vale; lá o $ é obrigatório.)

O tipo do que volta mostra a diferença sem sombra de dúvida. A função typeof() diz o tipo de um valor:

SELECT
  typeof(json_extract('{"n":5}', '$.n')) AS com_extract,
  typeof('{"n":5}' -> '$.n')             AS com_seta,
  typeof('{"n":5}' ->> '$.n')            AS com_seta_dupla;

O json_extract() e o ->> devolvem o número 5 (integer); o -> devolve o texto JSON 5 (text). Para o dia a dia, a regra é simples: use ->>. Deixe o -> para quando você quer um pedaço de JSON para encaixar em outro JSON.

FormaTexto "Ana" volta comoLista volta comoAceita 'nome' sem $.
json_extract(j, '$.x')Ana (valor do SQL)texto JSONnão (erro)
j ->> '$.x'Ana (valor do SQL)texto JSONsim
j -> '$.x'"Ana" (JSON, com aspas)texto JSONsim

5. JSON guardado numa coluna

Até aqui o JSON estava escrito na própria consulta. Na vida real ele fica guardado numa coluna. A Ana quer guardar os carrinhos do aplicativo, as compras que o cliente começou e ainda não fechou. Cada carrinho chega do aplicativo como um JSON, com a origem, o cupom (se houver) e a lista de itens. Ela cria uma tabela com uma coluna TEXT para o JSON:

CREATE TABLE carrinhos (
  id         INTEGER PRIMARY KEY,
  cliente_id INTEGER REFERENCES clientes(id),
  dados      TEXT CHECK (json_valid(dados))
);

INSERT INTO carrinhos (id, cliente_id, dados) VALUES
  (1, 3,  '{"origem":"app","cupom":"AURORA10","itens":[{"produto_id":1,"qtd":2},{"produto_id":5,"qtd":1}]}'),
  (2, 18, '{"origem":"site","itens":[{"produto_id":3,"qtd":10}]}'),
  (3, 27, '{"origem":"app","itens":[]}');

SELECT id,
       dados ->> '$.origem'               AS origem,
       dados ->> '$.cupom'                AS cupom,
       json_array_length(dados, '$.itens') AS n_itens
FROM carrinhos;
dados TEXT
O JSON fica numa coluna de texto comum. O SQLite não tem um tipo "JSON" separado.
CHECK (json_valid(dados))
A trava (aula CHECK): json_valid() devolve 1 se o texto é um JSON válido e 0 se não é. Com ela, o banco recusa JSON quebrado na entrada, em vez de você descobrir o problema meses depois.
dados ->> '$.origem'
Lê o campo origem de cada linha. O carrinho 2 não tem cupom, então ali vem NULL.
json_array_length(dados, '$.itens')
"Tamanho da lista": quantos itens tem a lista itens. O carrinho 3 está vazio: [] tem 0 itens.
Por que cada exemplo cria a tabela de novo?

A tabela carrinhos não existe no banco da Aurora: ela é criada no exemplo. E cada Rodar do editor parte do banco limpo (aula O Editor do Guia). Por isso, nos exemplos seguintes, o CREATE TABLE e o INSERT se repetem antes da consulta.

Com o ->>, um campo do JSON entra no WHERE como qualquer coluna:

CREATE TABLE carrinhos (id INTEGER PRIMARY KEY, cliente_id INTEGER, dados TEXT CHECK (json_valid(dados)));
INSERT INTO carrinhos VALUES
  (1, 3,  '{"origem":"app","cupom":"AURORA10","itens":[{"produto_id":1,"qtd":2},{"produto_id":5,"qtd":1}]}'),
  (2, 18, '{"origem":"site","itens":[{"produto_id":3,"qtd":10}]}'),
  (3, 27, '{"origem":"app","itens":[]}');

SELECT id, cliente_id
FROM carrinhos
WHERE dados ->> '$.origem' = 'app';

Mudando um campo: json_set()

Para mudar um pedaço do JSON guardado, não é preciso reescrever o texto todo. O json_set() devolve o JSON com o campo trocado (ou acrescentado, se ele não existia), e o UPDATE grava:

CREATE TABLE carrinhos (id INTEGER PRIMARY KEY, cliente_id INTEGER, dados TEXT CHECK (json_valid(dados)));
INSERT INTO carrinhos VALUES
  (2, 18, '{"origem":"site","itens":[{"produto_id":3,"qtd":10}]}');

UPDATE carrinhos
SET dados = json_set(dados, '$.cupom', 'FRETEGRATIS')
WHERE id = 2;

SELECT dados FROM carrinhos WHERE id = 2;

O campo cupom não existia e foi acrescentado no fim. A família tem quatro irmãos, que diferem só no que fazem quando o campo já existe ou não:

SELECT
  json_set('{"a":1}', '$.b', 2)          AS set_novo,
  json_insert('{"a":1}', '$.a', 9)       AS insert_existente,
  json_replace('{"a":1}', '$.a', 9)      AS replace_existente,
  json_remove('{"a":1,"b":2}', '$.b')    AS remove;
FunçãoCampo já existeCampo não existe
json_set()troca o valorcria o campo
json_insert()não mexe (por isso o "a" continuou 1)cria o campo
json_replace()troca o valornão mexe
json_remove()apaga o camponão mexe

Missão: a Ana quer o id dos carrinhos que vieram do site. A consulta abaixo roda sem erro, mas devolve zero linhas, e existe um carrinho do site. Descubra o porquê (releia a seção 4) e conserte só o operador.

CREATE TABLE carrinhos (id INTEGER PRIMARY KEY, cliente_id INTEGER, dados TEXT CHECK (json_valid(dados)));
INSERT INTO carrinhos VALUES
  (1, 3,  '{"origem":"app","cupom":"AURORA10","itens":[{"produto_id":1,"qtd":2},{"produto_id":5,"qtd":1}]}'),
  (2, 18, '{"origem":"site","itens":[{"produto_id":3,"qtd":10}]}'),
  (3, 27, '{"origem":"app","itens":[]}');

SELECT id
FROM carrinhos
WHERE dados -> '$.origem' = 'site';
CREATE TABLE carrinhos (id INTEGER PRIMARY KEY, cliente_id INTEGER, dados TEXT CHECK (json_valid(dados)));
INSERT INTO carrinhos VALUES
  (1, 3,  '{"origem":"app","cupom":"AURORA10","itens":[{"produto_id":1,"qtd":2},{"produto_id":5,"qtd":1}]}'),
  (2, 18, '{"origem":"site","itens":[{"produto_id":3,"qtd":10}]}'),
  (3, 27, '{"origem":"app","itens":[]}');

SELECT id
FROM carrinhos
WHERE dados ->> '$.origem' = 'site';

6. Da lista para as linhas: json_each()

Uma lista JSON numa célula é ótima para guardar, e ruim para consultar: você não consegue fazer JOIN com uma lista. O json_each() ("cada um do JSON") resolve isso: ele desmonta a lista e devolve uma linha para cada item, como se fosse uma tabela:

SELECT * FROM json_each('["livros","jardim","papelaria"]');

Ele aparece no FROM, no lugar de uma tabela: é uma função de tabela (table-valued function). As colunas que importam no começo são duas:

key
"Chave": a posição do item na lista (0, 1, 2…) ou, num objeto, o nome do campo.
value
"Valor": o próprio item, já como valor do SQL.
type, atom, id, parent, fullkey, path
Detalhes para JSON complicado: o tipo do item (text, integer, object…), o caminho completo até ele (fullkey: $[0], $[1]…). Você raramente vai precisar.

Num objeto, cada campo vira uma linha, com o nome em key:

SELECT key, value
FROM json_each('{"nome":"Ana","cidade":"Teresina","vip":1}');

Os itens de cada carrinho

Agora o uso de verdade: abrir a lista itens de cada carrinho e juntar com a tabela produtos para saber o nome e o preço. O segundo argumento do json_each() é o caminho da lista dentro do JSON:

CREATE TABLE carrinhos (id INTEGER PRIMARY KEY, cliente_id INTEGER, dados TEXT CHECK (json_valid(dados)));
INSERT INTO carrinhos VALUES
  (1, 3,  '{"origem":"app","cupom":"AURORA10","itens":[{"produto_id":1,"qtd":2},{"produto_id":5,"qtd":1}]}'),
  (2, 18, '{"origem":"site","itens":[{"produto_id":3,"qtd":10}]}'),
  (3, 27, '{"origem":"app","itens":[]}');

SELECT c.id                  AS carrinho,
       p.nome,
       i.value ->> 'qtd'     AS qtd,
       p.preco
FROM carrinhos c
JOIN json_each(c.dados, '$.itens') i
JOIN produtos p ON p.id = i.value ->> 'produto_id';
JOIN json_each(c.dados, '$.itens') i
Para cada carrinho c, desmonta a lista itens dele. Cada item vira uma linha com o apelido i. O carrinho 3 tem a lista vazia, então não gera linha nenhuma.
i.value
O item da vez, que é um objeto: {"produto_id":1,"qtd":2}.
i.value ->> 'qtd'
Dentro do item, o campo qtd, como número.
ON p.id = i.value ->> 'produto_id'
Liga o item ao produto pelo id, como um JOIN comum (aula INNER JOIN).

Virou tabela, então todo o resto do SQL funciona. O total de cada carrinho, com SUM e GROUP BY:

CREATE TABLE carrinhos (id INTEGER PRIMARY KEY, cliente_id INTEGER, dados TEXT CHECK (json_valid(dados)));
INSERT INTO carrinhos VALUES
  (1, 3,  '{"origem":"app","cupom":"AURORA10","itens":[{"produto_id":1,"qtd":2},{"produto_id":5,"qtd":1}]}'),
  (2, 18, '{"origem":"site","itens":[{"produto_id":3,"qtd":10}]}'),
  (3, 27, '{"origem":"app","itens":[]}');

SELECT c.id AS carrinho,
       ROUND(SUM(p.preco * (i.value ->> 'qtd')), 2) AS total
FROM carrinhos c
JOIN json_each(c.dados, '$.itens') i
JOIN produtos p ON p.id = i.value ->> 'produto_id'
GROUP BY c.id;

O carrinho 1 dá 2 × 12.35 + 1 × 89.99 = 114.69; o carrinho 2, 10 × 1.15 = 11.5.

Experimente: troque a lista por outra (pode ser de números: '[10, 20, 30]'), por um objeto, ou some os valores com SELECT SUM(value) FROM json_each('[10, 20, 30]');.

SELECT key, value, type
FROM json_each('["livros", "jardim", 42, null]');

7. Das linhas para a lista: json_group_array()

O caminho de volta. O json_group_array() é uma função agregada, da mesma família do COUNT() e do SUM() (aula O que são Funções Agregadas): ela junta os valores de várias linhas numa lista JSON só.

SELECT json_group_array(nome) AS categorias
FROM categorias;

Oito linhas viraram uma lista com oito nomes. Como toda agregada, ela combina com GROUP BY (aula GROUP BY): uma lista por grupo. Os produtos de cada um dos três primeiros pedidos:

SELECT pedido_id, json_group_array(produto_id) AS produtos
FROM itens_pedido
WHERE pedido_id <= 3
GROUP BY pedido_id;

Quer a lista em ordem? Ponha um ORDER BY dentro dos parênteses da função:

SELECT json_group_array(nome ORDER BY nome) AS categorias
FROM categorias;

Um objeto em vez de lista: json_group_object()

O irmão json_group_object(nome, valor) junta as linhas num objeto: o primeiro argumento vira o nome do campo e o segundo, o valor. Os produtos da categoria Papelaria, com seus preços:

SELECT json_group_object(nome, preco) AS precos
FROM produtos
WHERE categoria_id = 1;

8. O JSON de um pedido com seus itens

Agora o pedido do programador do aplicativo: o pedido 1 inteiro, num JSON só, com o cliente e a lista de itens. Primeiro, as duas partes separadas, em tabela, para ver o que vai entrar:

SELECT p.id, p.data, p.frete, c.nome AS cliente
FROM pedidos p
JOIN clientes c ON c.id = p.cliente_id
WHERE p.id = 1;

SELECT pr.nome, ip.quantidade, ip.preco_unitario
FROM itens_pedido ip
JOIN produtos pr ON pr.id = ip.produto_id
WHERE ip.pedido_id = 1;

Os itens viram uma lista de objetos com json_group_array(json_object(…)): o json_object() monta a ficha de cada item, e o json_group_array() junta as fichas numa lista:

SELECT json_group_array(
         json_object('produto', pr.nome, 'qtd', ip.quantidade, 'preco', ip.preco_unitario)
       ) AS itens
FROM itens_pedido ip
JOIN produtos pr ON pr.id = ip.produto_id
WHERE ip.pedido_id = 1;

Por fim, essa lista entra como o campo itens do objeto do pedido, por uma subconsulta (aula Subconsultas) que roda para cada pedido:

SELECT json_object(
  'pedido',  p.id,
  'data',    p.data,
  'cliente', c.nome,
  'frete',   p.frete,
  'itens',   (SELECT json_group_array(
                       json_object('produto', pr.nome,
                                   'qtd',     ip.quantidade,
                                   'preco',   ip.preco_unitario))
              FROM itens_pedido ip
              JOIN produtos pr ON pr.id = ip.produto_id
              WHERE ip.pedido_id = p.id)
) AS pedido_json
FROM pedidos p
JOIN clientes c ON c.id = p.cliente_id
WHERE p.id = 1;
  1. A consulta de fora pega o pedido 1 e o nome do cliente (José Ribamar Silva), como sempre.
  2. Para esse pedido, a subconsulta entre parênteses monta a lista de itens. O WHERE ip.pedido_id = p.id liga a subconsulta ao pedido de fora.
  3. O json_object() de fora junta tudo. Como o valor de itens veio de uma função de JSON, ele entra como lista de verdade, e não como texto entre aspas.

Esse texto pode ir direto para o aplicativo. Para ler com calma, o json_pretty() devolve o mesmo JSON com quebras de linha e recuo. Ele é recente (SQLite 3.46) e serve para gente ler, não para guardar: SELECT json_pretty(json_object('pedido', 1, 'itens', json_array(1, 2)));.


9. No terminal: o modo JSON do sqlite3

O programa sqlite3 (aula Comandos de Ponto) tem um jeito de mostrar qualquer resultado como JSON, sem função nenhuma: a opção -json (ou o comando .mode json dentro dele). Cada linha vira um objeto, e o resultado inteiro vira uma lista:

$ sqlite3 -json loja.db "SELECT id, nome, preco FROM produtos WHERE id <= 2;"
[{"id":1,"nome":"Caderno Pautado 96 folhas","preco":12.34999999999999965},
{"id":2,"nome":"Caneta Esferográfica Azul","preco":2.790000000000000035}]
$ sqlite3 -json loja.db "SELECT nome, email FROM clientes WHERE id = 7;"
[{"nome":"Luís Otávio Brito","email":null}]
$ sqlite3 loja.db "SELECT json_object('id', id, 'preco', preco) FROM produtos WHERE id <= 2;"
{"id":1,"preco":12.35}
{"id":2,"preco":2.79}

Repare no preço: o modo JSON escreveu 12.34999999999999965, enquanto o json_object() escreveu 12.35. Não é erro de nenhum dos dois. Um REAL é guardado em binário, e 12.35 não tem representação exata em binário; o modo JSON mostra todos os dígitos, e as funções de JSON escrevem o número mais curto que volta ao mesmo valor. Para dinheiro, prefira montar o JSON com as funções. O .mode json é só do programa sqlite3: no editor do guia, ele não existe.


Formas diferentes de fazer a mesma coisa

Ler o campo origem do carrinho, de três jeitos:

SELECT
  json_extract('{"origem":"app"}', '$.origem') AS com_extract,
  '{"origem":"app"}' ->> '$.origem'            AS com_seta_dupla,
  '{"origem":"app"}' ->> 'origem'              AS curto;

E a lista de produtos do pedido 1, montada com json_group_array() ou escrita à mão com json_array():

SELECT json_group_array(produto_id) AS agregada
FROM itens_pedido WHERE pedido_id = 1;

SELECT json_array(1, 2, 3) AS a_mao;
Para…UsePor quê
Ler um valor para comparar, somar, ordenar->>Curto e devolve valor do SQL; é a forma que o MySQL e o PostgreSQL também entendem
Ler um valor num SQL que precisa rodar em versões antigas do SQLitejson_extract()É a forma mais antiga; as setas só chegaram no SQLite 3.38
Pegar um pedaço que vai continuar sendo JSON->Mantém as aspas e a forma de JSON
Montar uma lista com valores que você já sabejson_array()Os valores vão direto nos parênteses
Montar uma lista com as linhas de uma consultajson_group_array()É agregada: junta quantas linhas vierem
Despejar um resultado inteiro em JSON, no terminalsqlite3 -jsonSem escrever função nenhuma (cuidado com os decimais)
JSON numa coluna não substitui tabelas

É tentador guardar tudo em JSON: "os itens do pedido? põe uma lista na coluna". Mas o banco não consegue garantir nada lá dentro: não há FOREIGN KEY para o produto_id do JSON, nem NOT NULL na qtd. Os carrinhos da Ana são um bom uso, porque são rascunhos que chegam prontos do aplicativo. Os pedidos de verdade ficam em pedidos e itens_pedido, com cada coisa na sua coluna (aula Normalização). Use JSON para trocar dados e para guardar o que não tem forma fixa.


Quando dá errado

Comparou com -> e não achou nada
SELECT '{"nome":"Ana"}' -> '$.nome' = 'Ana'   AS com_seta,
       '{"nome":"Ana"}' ->> '$.nome' = 'Ana'  AS com_seta_dupla;

O que acontece: nenhum erro, e é isso que torna este o engano mais comum. A comparação com -> dá 0 (falso), porque o -> devolve "Ana" com as aspas, e "Ana" não é igual a Ana. Num WHERE, a consulta volta vazia e você acha que não existe o dado.

Como consertar: para comparar, use ->> (ou json_extract()).

O texto não é JSON
SELECT json('isso nao e json');

A mensagem: malformed JSON, "JSON malformado". As funções que precisam entender o JSON param a consulta com esse erro. A função json() do exemplo só confere o texto e o devolve arrumado; é um bom jeito de testar um JSON.

Na tabela com a trava CHECK (json_valid(dados)), um JSON quebrado nem entra. O texto abaixo foi cortado no meio:

CREATE TABLE carrinhos (id INTEGER PRIMARY KEY, cliente_id INTEGER, dados TEXT CHECK (json_valid(dados)));
INSERT INTO carrinhos VALUES (4, 1, '{"origem":"app",');

A mensagem: CHECK constraint failed: json_valid(dados), "a trava CHECK falhou". Melhor ouvir isso na hora de gravar do que achar o JSON quebrado depois.

Um detalhe: o SQLite (desde o 3.42) aceita na leitura algumas folgas do JSON5, como uma vírgula sobrando no fim ('{"nome":"Ana",}'). Mas o json_valid() sem opções segue a regra estrita do JSON e diz 0 para esse texto. Outros programas também podem recusá-lo, então escreva sempre o JSON certinho.

Esqueceu o $ no caminho
SELECT json_extract('{"nome":"Ana"}', 'nome');

A mensagem: bad JSON path: 'nome', "caminho JSON ruim". No json_extract(), todo caminho começa com $. O mesmo erro aparece para '$nome' (faltou o ponto) e '$.a.' (ponto sobrando no fim).

Como consertar: '$.nome'. Só as setas aceitam o nome sozinho: '{"nome":"Ana"}' ->> 'nome' funciona.

Faltou um valor no json_object()
SELECT json_object('nome', nome, 'cidade') FROM clientes;

A mensagem: json_object() requires an even number of arguments, "json_object() exige um número par de argumentos". Os argumentos vêm em pares nome-valor; aqui o campo 'cidade' ficou sem valor. Outro parecido: json_object(1, 'Ana') dá json_object() labels must be TEXT, "os nomes dos campos têm de ser texto".

Como consertar: complete o par: 'cidade', cidade.

Aspas duplas no nome do campo

Como o JSON usa aspas duplas, é comum escrever o nome do campo com elas também:

SELECT json_object("nome", 'Ana');

A mensagem: no such column: "nome" - should this be a string literal in single-quotes?, "não existe a coluna "nome"; isto devia ser um texto entre aspas simples?". No SQL, aspas duplas marcam nome de coluna, não texto. O próprio SQLite sugere o conserto.

Como consertar: no SQL, aspas simples: json_object('nome', 'Ana'). As aspas duplas do JSON quem põe é a função.

Texto que parece JSON vira texto
SELECT json_object('nome', 'Ana', 'gosta', '["livros","jardim"]') AS errado,
       json_object('nome', 'Ana', 'gosta', json('["livros","jardim"]')) AS certo;

O que acontece: sem erro, mas na primeira coluna a lista virou um texto entre aspas, cheio de \" (a barra é como o JSON escreve uma aspa dentro de um texto). Para o json_object(), '["livros","jardim"]' é um texto como outro qualquer. O aplicativo recebe uma frase, não uma lista.

Como consertar: passe o valor por json() (ou monte com json_array()): assim o SQLite sabe que aquilo é JSON. Valores tirados de uma coluna de JSON com -> também entram como JSON.

Nos outros bancos

Todo banco grande tem JSON, mas os nomes mudam. As setas do SQLite foram feitas para serem compatíveis com o MySQL e o PostgreSQL. Os trechos abaixo são ilustrativos: não rodam no SQLite.

  • MySQL: JSON_OBJECT(), JSON_EXTRACT() e as setas coluna->'$.nome' e coluna->>'$.nome'. Na documentação, à esquerda da seta vai o nome de uma coluna, não uma expressão qualquer.
  • PostgreSQL: tem os tipos json e jsonb. As setas recebem o nome do campo ou a posição, sem $: dados ->> 'origem', lista -> 2. Para montar: json_build_object() e json_agg(); para desmontar uma lista: json_array_elements().
  • SQL Server: JSON_VALUE(coluna, '$.nome') para um valor simples e JSON_QUERY() para um pedaço que é objeto ou lista; OPENJSON() faz o papel do json_each().
-- PostgreSQL (ilustrativo)
SELECT id, dados ->> 'origem' AS origem
FROM carrinhos;

-- SQL Server (ilustrativo)
SELECT id, JSON_VALUE(dados, '$.origem') AS origem
FROM carrinhos;

Referência rápida

EscritaO que fazExemplo → resultado
json_object(n1, v1, n2, v2…)Monta um objeto com pares nome-valorjson_object('a', 1) → {"a":1}
json_array(v1, v2…)Monta uma listajson_array(1, 'x') → [1,"x"]
json_extract(j, caminho)Lê um valor (como valor do SQL)json_extract('{"a":1}', '$.a') → 1
j ->> caminhoLê um valor como valor do SQL'{"a":"x"}' ->> 'a' → x
j -> caminhoLê um pedaço como JSON'{"a":"x"}' -> 'a' → "x"
json_each(j [, caminho])Função de tabela: uma linha por item (colunas key, value…)SELECT value FROM json_each('[1,2]')
json_group_array(x)Agregada: junta as linhas numa listajson_group_array(nome)
json_group_object(n, v)Agregada: junta as linhas num objetojson_group_object(nome, preco)
json_valid(j)1 se é JSON válido, 0 se nãoCHECK (json_valid(dados))
json(j)Confere e devolve o JSON arrumado; marca o texto como JSONjson('[1, 2]') → [1,2]
json_array_length(j [, caminho])Quantos itens tem a listajson_array_length('[1,2,3]') → 3
json_type(j [, caminho])O tipo do pedaço: object, array, text, integer…json_type('{"a":[1]}', '$.a') → array
json_set / json_insert / json_replace / json_removeDevolvem o JSON com um campo trocado, criado ou apagadojson_set('{}', '$.a', 1) → {"a":1}
json_pretty(j)JSON com quebras de linha e recuo, para ler(3.46 ou mais novo)
Mensagem do SQLiteQuer dizer
malformed JSONO texto não é um JSON válido
bad JSON path: 'nome'O caminho está mal escrito (falta o $, falta ou sobra ponto)
json_object() requires an even number of argumentsUm nome de campo ficou sem valor
json_object() labels must be TEXTO nome do campo não é texto
CHECK constraint failed: json_valid(dados)A trava recusou um JSON inválido

Exercícios

1. Montar uma ficha

Complete para gerar {"nome":"Lápis Grafite HB"} a partir do produto 3:

SELECT ('nome', nome) FROM produtos WHERE id = 3;

2. O caminho

Complete o caminho para ler o campo cupom com json_extract():

SELECT json_extract(dados, '') FROM carrinhos;

3. Qual é o resultado de SELECT '{"cidade":"Picos"}' -> '$.cidade';?

4. Você quer transformar a lista JSON '[4, 8, 15]' em três linhas, para fazer um JOIN. Qual função usa?

5. O que dá json_extract('{"nome":"Ana"}', '$.idade')?

6. Por que json_object('gosta', '["livros"]') não gera uma lista dentro do objeto?

Missão final: o pedido 2 em JSON

Missão: a consulta abaixo lista os itens do pedido 2 em tabela. Transforme-a numa linha só, com um JSON assim: campos pedido (o id), cliente (o nome) e itens, uma lista de objetos com produto (o nome do produto) e qtd (a quantidade), nessa ordem. Dica: siga a seção 8, com uma subconsulta para os itens.

SELECT p.id, c.nome, pr.nome, ip.quantidade
FROM pedidos p
JOIN clientes c      ON c.id = p.cliente_id
JOIN itens_pedido ip ON ip.pedido_id = p.id
JOIN produtos pr     ON pr.id = ip.produto_id
WHERE p.id = 2;
SELECT json_object(
  'pedido',  p.id,
  'cliente', c.nome,
  'itens',   (SELECT json_group_array(json_object('produto', pr.nome, 'qtd', ip.quantidade))
              FROM itens_pedido ip
              JOIN produtos pr ON pr.id = ip.produto_id
              WHERE ip.pedido_id = p.id)
) AS pedido_json
FROM pedidos p
JOIN clientes c ON c.id = p.cliente_id
WHERE p.id = 2;

Se deu certo, a resposta tem uma linha só, com a Maria Clara Mendes e três itens: Marca-texto Amarelo, Mochila Escolar Azul (2 unidades) e Estojo Duplo. Troque o WHERE p.id = 2 por WHERE p.id <= 3 e veja três pedidos, cada um no seu JSON.


Resumo

  • JSON é texto com objetos {"nome": valor} e listas […]. No SQLite, ele fica numa coluna TEXT.
  • json_object('nome', valor, …) monta um objeto; json_array(…), uma lista. Uma função pode ir dentro da outra.
  • Para ler: json_extract(j, '$.campo') ou j ->> 'campo', que devolvem valor do SQL. O -> devolve JSON (texto com aspas): não use para comparar.
  • Campo que não existe dá NULL; caminho mal escrito dá bad JSON path; texto que não é JSON dá malformed JSON.
  • CHECK (json_valid(dados)) barra JSON quebrado na entrada.
  • json_each() desmonta uma lista em linhas (vai no FROM); json_group_array() e json_group_object() juntam linhas em JSON.
  • O JSON de um pedido com itens: json_object(…, 'itens', (SELECT json_group_array(json_object(…)) …)).