Validação JSON no CI: um pipeline com jsonlint, ajv e jq
Resumo: A validação de sintaxe prova que um arquivo é analisável; a validação de schema prova que os campos seguem o contrato. No CI, o que importa são códigos de saída inequívocos, localização de erros e caminhos de correção.

Resumo da ópera: no CI, deixe o jq empty cuidar da sintaxe, o ajv-cli cuidar da estrutura (check-jsonschema para equipes multi-formato) e o Prettier cuidar do estilo; encadeie-os por camada e pare na primeira falha.
Passar no analisador do seu editor local não significa que o arquivo pertence ao CI; passar no CI também não significa que os campos correspondem ao contrato.
1. Separe a validação de sintaxe da validação de schema
A expressão "validação JSON" é usada de forma ampla demais, e é por isso que as equipes costumam escolher uma ferramenta para resolver dois problemas diferentes:
| Camada | Pergunta respondida | Ferramentas típicas | O que uma falha significa |
|---|---|---|---|
| Sintaxe | Este arquivo é JSON válido? | jq empty, jsonlint, python -m json.tool |
Colchetes, vírgulas, aspas ou codificação estão quebrados |
| Schema | Campos, tipos e propriedades obrigatórias correspondem ao contrato? | ajv-cli, check-jsonschema |
O arquivo é analisado, mas a estrutura de negócio está errada |
| Estilo | Indentação e ordem das chaves são consistentes? | Prettier, jq -S |
A formatação está inconsistente e os diffs são barulhentos |
Essas camadas não substituem umas às outras. Um arquivo que passa a sintaxe ainda pode falhar no schema: {"price": "12.5"} é JSON válido, mas se o schema exige que price seja número, ele não é aceitável. Inversamente, por mais completo que seja o schema, um arquivo sem chave de fechamento nunca vai compilar contra ele.
Então o pipeline deve rodar sintaxe primeiro, estrutura depois e estilo por último. Pare assim que uma camada falhar, caso contrário um erro de análise se disfarça de erro de campo.
2. Camada de sintaxe: jq empty é a opção de dependência mínima
Se o jq já está na imagem do CI, a validação de sintaxe não custa quase nada:
jq empty data.json
O jq empty não produz saída; ele apenas analisa o arquivo. Sai com 0 em caso de sucesso e com um código não zero mais informações de linha/coluna em caso de falha. Isso o torna mais leve do que puxar uma árvore de dependências Node inteira, e um bom primeiro portão.
Ao validar em lote os arquivos JSON de um repositório, exclua node_modules, saída de build e caches de terceiros:
find . -name '*.json' \
-not -path './node_modules/*' \
-not -path './dist/*' \
-print0 | xargs -0 -n1 jq empty
-print0 e xargs -0 lidam com espaços em nomes de arquivo, enquanto -n1 garante um arquivo por invocação para que a mensagem de erro identifique o culpado. Não passe centenas de arquivos para um único processo; as mensagens de erro vão se sobrepor.
O jsonlint é outra escolha comum, normalmente invocado como npx jsonlint -q data.json, onde -q imprime apenas erros. Ele relata números de linha e coluna, mas sua manutenção é menos ativa que a do jq, e versões diferentes toleram BOM e comentários de formas diferentes. Trate-o como um verificador de sintaxe mais legível, não como um validador de schema.
3. Camada de schema: os fundamentos do ajv-cli validate
O Ajv é o validador de schema mais usado no ecossistema JavaScript, e o ajv-cli o expõe como ferramenta de linha de comando:
ajv validate -s schema.json -d data.json
-s aponta para o arquivo de schema e -d para os dados a validar. Em caso de falha, o processo sai não zero, então o CI falha imediatamente. Projetos reais normalmente adicionam duas flags:
npx ajv-cli validate \
-s schemas/order.json \
-d 'fixtures/orders/*.json' \
--strict=false \
--errors=text
--strict=false ajuda com schemas antigos: o modo estrito trata palavras-chave desconhecidas e declarações required suspeitas como erros, o que muitos schemas legados reprovam. --errors=text produz saída legível por humanos, enquanto a estrutura JSON padrão é melhor para máquinas. Escolha conforme quem consome o resultado.
A versão de draft do schema também precisa bater. Versões principais do Ajv suportam drafts diferentes por padrão, e o ajv-cli pode selecionar um com --spec=draft7, --spec=draft2019 ou --spec=draft2020. Se um schema declara $schema: "https://json-schema.org/draft/2020-12/schema" mas a CLI o compila como draft-07, algumas palavras-chave são ignoradas silenciosamente. Para mais sobre autoria de schema e diferenças de versão, veja Começando com JSON Schema.
A validação de formato é outro requisito frequente. A palavra-chave format é apenas anotação por padrão; para fazer email, date-time e uri realmente imporem algo, carregue ajv-formats:
npx ajv-cli validate -c ajv-formats -s schema.json -d data.json
Essa etapa é fácil de esquecer, e o resultado é que um campo de e-mail aceita not-an-email. Antes de publicar, entregue ao pipeline um registro deliberadamente inválido; isso prova que a regra está ativa muito melhor do que ler a documentação dez vezes.
check-jsonschema: vários formatos e erros mais amigáveis
Se a equipe lida com YAML, TOML ou configuração de GitHub Actions além de JSON, o check-jsonschema é mais conveniente:
check-jsonschema --schemafile schemas/config.json config/app.json
check-jsonschema --schemafile schemas/pipeline.json .github/workflows/ci.yml
Suas vantagens práticas:
- Suporte nativo a YAML, o que convém a arquivos "com forma de JSON, mas escritos em YAML", como manifests do Kubernetes e configuração de CI.
- Os erros incluem caminho do arquivo, número da linha e caminho do campo, mantendo o custo de localização baixo.
- Há um hook de pre-commit disponível, então a integração é barata.
- Ele consegue buscar schemas comuns do SchemaStore, mas também aceita arquivos locais, evitando dependência de rede.
Em equipes multilinguagem, o Python costuma já estar presente, então instalar e atualizar o check-jsonschema é mais leve do que manter uma cadeia Node. Deixe o jq cuidar da sintaxe e esta ferramenta cuidar da estrutura; a combinação tem custo de manutenção muito baixo.
4. Códigos de saída são o único sinal confiável do CI
Não importa quão polida seja a saída, o CI só lê códigos de saída. Três armadilhas são comuns:
| Sintoma | Causa | Correção |
|---|---|---|
| Há erros mas o CI está verde | O comando termina com ` | |
| Não há erros mas o CI está vermelho | O diff trata "diferenças encontradas" como falha |
Distinga "diferença" de "erro de execução" |
| A saída de erro está truncada | A saída paralela multiprotocolo se sobrescreve | Rode em série ou imprima por arquivo |
Lembre-se da semântica de código de saída do jq: análise normal retorna 0; com -e, um resultado de expressão false ou null retorna 1; erros de uso e arquivos ausentes retornam 2; erros de compilação de script retornam 3. Ferramentas de schema geralmente usam 0 para sucesso, 1 para falha de validação e 2 para erros de ferramenta ou argumento. Ao escrever um wrapper, não colapse todo código não zero em "os dados estão errados", ou uma falha de ambiente será diagnosticada erroneamente como problema de conteúdo.
5. pre-commit: pegue os problemas antes do commit
O valor do pre-commit não é "mais uma verificação"; é dar feedback aos desenvolvedores em um minuto em vez de depois de uma execução de CI de dez minutos. Uma configuração mínima fica assim:
repos:
- repo: https://github.com/python-jsonschema/check-jsonschema
rev: 0.33.0
hooks:
- id: check-jsonschema
files: ^configs/.*\.json$
args:
- --schemafile
- schemas/config.json
- repo: local
hooks:
- id: jq-syntax
name: jq syntax check
entry: jq empty
language: system
files: ^configs/.*\.json$
Dois detalhes importam:
filesdeve corresponder apenas aos diretórios que precisam de validação. Não rode hooks contranode_modulesnem arquivos gerados.- O hook local pressupõe que o
jqesteja instalado na máquina do desenvolvedor. Documente esse requisito, ou mude para um hook containerizado.
Se o projeto já usa Husky e lint-staged, combine-os diretamente:
{
"lint-staged": {
"configs/*.json": ["jq empty", "prettier --check"]
}
}
Lembre que o lint-staged passa uma lista de arquivos preparados, então o comando precisa aceitar vários caminhos. Tanto jq empty quanto prettier --check aceitam. Não deixe um hook varrer o repositório inteiro; os commits ficarão notavelmente mais lentos.
6. GitHub Actions: um pipeline que você pode copiar
O workflow abaixo faz validação de sintaxe, validação de schema e preserva o relatório em caso de falha:
name: json-validate
on:
push:
paths:
- 'schemas/**'
- 'fixtures/**'
pull_request:
jobs:
validate:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 20
- name: Syntax check
run: |
find fixtures -name '*.json' -print0 | xargs -0 -n1 jq empty
- name: Schema check
run: |
npx --yes ajv-cli validate \
-s schemas/order.json \
-d 'fixtures/orders/*.json' \
--strict=false \
--errors=text
O filtro paths evita que mudanças apenas de documentação disparem a validação. npx --yes convém a repositórios sem dependências fixadas; projetos de produção devem adicionar ajv-cli a devDependencies e fixar a versão. Sem lock, um lançamento upstream que mude o draft padrão ou o modo estrito pode deixar o pipeline vermelho da noite para o dia.
7. Localização de erros: de números de linha a caminhos de campo
Metade do valor de um validador é a detecção; a outra metade é a localização. Aprenda a ler três tipos de saída:
- Erros de sintaxe: o
jqrelata linha e coluna, muitas vezes com um trecho próximo. Corrija colchetes e vírgulas primeiro; não suspeite de codificação imediatamente. - Erros de schema: o
instancePathdo Ajv aponta para a localização dos dados (por exemplo/items/0/price), oschemaPathaponta para a regra (por exemplo#/properties/items/items/properties/price/type), ekeywordnomeia a restrição violada. - Erros de formato: quando uma verificação
formatfalha, a mensagem muitas vezes só diz "must match format "email"". Combine-a cominstancePathpara identificar o campo.
Cole o instancePath no registro de investigação; ele é bem mais útil do que uma parede de JSON. Para arrays, o índice numérico também diz qual registro falhou. Se a chave de negócio for mais intuitiva, adicione uma linha de mapeamento depois, como items[0].id = ORD-042.
Erros comuns de schema, como abusar de additionalProperties: false ou escrever ramos de oneOf que na verdade se sobrepõem, estão cobertos em Armadilhas comuns de geração de JSON Schema. Uma regra de validação errada é mais perigosa do que nenhuma validação, porque cria uma falsa sensação de segurança.
8. Tabela de seleção de ferramentas
| Necessidade | Ferramenta recomendada | Por quê | Cuidado |
|---|---|---|---|
| Apenas sintaxe | jq empty |
Zero dependências, rápido, códigos de saída claros | Espaços em nomes de arquivo em execuções em lote |
| Erros de linha/coluna mais amigáveis | jsonlint |
Saída legível | Atividade de manutenção limitada |
| Validação de JSON Schema | ajv-cli |
Ecossistema maduro, erros estruturados | Alinhe versões de draft e carregue o suporte a format |
| Repositório com JSON e YAML misturados | check-jsonschema |
Multi-formato, amigável ao pre-commit | Origem do schema e acesso à rede |
| Feedback rápido antes do commit | pre-commit / lint-staged | Processa apenas arquivos preparados | Os hooks precisam aceitar vários caminhos |
| Formatação consistente | Prettier | Amplamente adotado | Use --check quando não quiser reescrever arquivos |
Uma recomendação final: verifique que o pipeline realmente falha entregando-lhe primeiro um registro deliberadamente inválido. O maior risco não é uma configuração complicada; é uma configuração que nunca entra em vigor. Tratar "ele consegue falhar" como critério de aceitação é mais confiável do que tratar "ele passa" como tal. Para a cadeia de ferramentas de linha de comando mais ampla, continue em Editores, linha de comando e automação.
Perguntas frequentes
Devo usar jq empty ou jsonlint?
Prefira jq empty no CI: zero dependências, inicialização rápida e semântica de código de saída clara. Use jsonlint quando precisar de mensagens de linha/coluna mais amigáveis ou estiver investigando localmente. Ambos fazem apenas validação de sintaxe; nenhum verifica a estrutura dos campos.
Por que o ajv relata uma palavra-chave desconhecida?
Primeiro confirme se o draft $schema do schema corresponde à versão usada pela CLI. Muitos erros de "palavra-chave desconhecida" são palavras-chave de draft novo encontradas por um compilador mais antigo. Se as versões correspondem, verifique se há erro de digitação. --strict=false pode manter schemas legados funcionando, mas não conte com isso a longo prazo para esconder problemas reais.
A validação de schema passa, mas a API ainda falha. Por quê?
Um schema valida estrutura, não semântica de negócio. Por exemplo, status pode ser uma string com um enum legal, enquanto a máquina de estados proíbe passar diretamente de paid para refunded. Regras assim pertencem ao código ou a restrições de banco de dados, não ao JSON Schema.
Como localizo rapidamente o campo que falhou?
Leia o instancePath ou o número de linha da saída da ferramenta. O instancePath do Ajv é um JSON Pointer que aponta diretamente para os dados; o check-jsonschema relata arquivo e linha. Registre o caminho e mapeie-o para a chave de negócio; localizar o problema geralmente leva minutos.
O pre-commit deve rodar uma validação completa?
Não. O pre-commit deve processar apenas arquivos preparados para manter os commits rápidos. Varreduras completas pertencem ao CI ou a jobs agendados. A divisão é simples: execuções locais privilegiam velocidade, o CI privilegia completude.