Validación de JSON en CI: un pipeline con jsonlint, ajv y jq
En resumen: La validación de sintaxis demuestra que un archivo se puede parsear; la validación de schema demuestra que los campos siguen el contrato. En CI, lo que importa son códigos de salida inequívocos, localizaciones de error y rutas de reparación.

En resumen: en CI, deja que jq empty se encargue de la sintaxis, ajv-cli de la estructura (check-jsonschema para equipos multiformato) y Prettier del estilo; encadénalos por capas y detente en el primer fallo.
Pasar el parser de tu editor local no significa que el archivo pertenezca a CI; pasar CI tampoco significa que los campos coincidan con el contrato.
1. Separa la validación de sintaxis de la validación de schema
La expresión "validación de JSON" se usa de forma demasiado amplia, y por eso los equipos a menudo eligen una sola herramienta para resolver dos problemas distintos:
| Capa | Pregunta que responde | Herramientas típicas | Qué significa un fallo |
|---|---|---|---|
| Sintaxis | ¿Este archivo es JSON válido? | jq empty, jsonlint, python -m json.tool |
Los corchetes, comas, comillas o la codificación están rotos |
| Schema | ¿Los campos, tipos y propiedades obligatorias coinciden con el contrato? | ajv-cli, check-jsonschema |
El archivo se parsea, pero la estructura de negocio es incorrecta |
| Estilo | ¿La indentación y el orden de las claves son consistentes? | Prettier, jq -S |
El formato es inconsistente y los diffs son ruidosos |
Estas capas no pueden sustituirse unas a otras. Un archivo que pasa la sintaxis puede fallar el schema: {"price": "12.5"} es JSON válido, pero si el schema exige que price sea un número, no es aceptable. A la inversa, por completo que sea el schema, un archivo al que le falta una llave de cierre nunca compilará contra él.
Así que el pipeline debe ejecutar sintaxis primero, estructura después y estilo al final. Detente en cuanto falle una capa, de lo contrario un error de parseo se disfraza de error de campo.
2. Capa de sintaxis: jq empty es la opción con menos dependencias
Si jq ya está en la imagen de CI, la validación de sintaxis no cuesta casi nada:
jq empty data.json
jq empty no produce salida; solo parsea el archivo. Sale con 0 en caso de éxito y con un código distinto de cero más información de línea/columna en caso de fallo. Eso lo hace más ligero que traer un árbol de dependencias completo de Node, y una buena primera barrera.
Al validar por lotes archivos JSON de un repositorio, excluye node_modules, la salida de build y las cachés de terceros:
find . -name '*.json' \
-not -path './node_modules/*' \
-not -path './dist/*' \
-print0 | xargs -0 -n1 jq empty
-print0 y xargs -0 manejan espacios en los nombres de archivo, mientras que -n1 garantiza un archivo por invocación para que el mensaje de error identifique al culpable. No pases cientos de archivos a un solo proceso; los mensajes de error se pisarán unos a otros.
jsonlint es otra opción habitual, normalmente invocada como npx jsonlint -q data.json, donde -q imprime solo los errores. Reporta números de línea y columna, pero su mantenimiento es menos activo que jq, y distintas versiones toleran de forma diferente BOM y comentarios. Trátalo como un comprobador de sintaxis más legible, no como un validador de schema.
3. Capa de schema: lo básico de ajv-cli validate
Ajv es el validador de schema más usado en el ecosistema JavaScript, y ajv-cli lo expone como herramienta de línea de comandos:
ajv validate -s schema.json -d data.json
-s apunta al archivo de schema y -d a los datos que se validan. En caso de fallo el proceso sale con un código distinto de cero, así que CI falla de inmediato. Los proyectos reales suelen añadir dos flags:
npx ajv-cli validate \
-s schemas/order.json \
-d 'fixtures/orders/*.json' \
--strict=false \
--errors=text
--strict=false ayuda con schemas antiguos: el modo estricto trata las palabras clave desconocidas y las declaraciones required sospechosas como errores, y muchos schemas heredados fallan por eso. --errors=text produce una salida legible por humanos, mientras que la estructura JSON predeterminada es mejor para máquinas. Elige según quién consuma el resultado.
La versión de draft del schema también debe coincidir. Las versiones mayores de Ajv soportan distintos drafts de forma predeterminada, y ajv-cli puede seleccionar uno con --spec=draft7, --spec=draft2019 o --spec=draft2020. Si un schema declara $schema: "https://json-schema.org/draft/2020-12/schema" pero la CLI lo compila como draft-07, algunas palabras clave se ignoran en silencio. Para más información sobre la creación de schemas y las diferencias entre versiones, consulta Primeros pasos con JSON Schema.
La validación de formatos es otro requisito frecuente. La palabra clave format es solo descriptiva de forma predeterminada; para que email, date-time y uri realmente impongan algo, carga ajv-formats:
npx ajv-cli validate -c ajv-formats -s schema.json -d data.json
Este paso es fácil de olvidar, y el resultado es que un campo de email acepta not-an-email. Antes de publicar, alimenta el pipeline con un registro deliberadamente inválido; eso demuestra que la regla está activa mucho mejor que leer la documentación diez veces.
check-jsonschema: múltiples formatos y errores más amigables
Si el equipo maneja YAML, TOML o configuración de GitHub Actions además de JSON, check-jsonschema es más cómodo:
check-jsonschema --schemafile schemas/config.json config/app.json
check-jsonschema --schemafile schemas/pipeline.json .github/workflows/ci.yml
Sus ventajas prácticas:
- Soporte nativo de YAML, que encaja con archivos que "tienen forma de JSON pero están escritos en YAML", como los manifiestos de Kubernetes y la configuración de CI.
- Los errores incluyen la ruta del archivo, el número de línea y la ruta del campo, manteniendo bajo el coste de localización.
- Proporciona un hook de pre-commit, así que la integración es barata.
- Puede obtener schemas comunes de SchemaStore, pero también acepta archivos locales, evitando una dependencia de red.
En equipos multilingües, Python suele estar ya presente, así que instalar y actualizar check-jsonschema es más ligero que mantener una cadena de herramientas Node. Deja que jq se encargue de la sintaxis y esta herramienta de la estructura; la combinación tiene un coste de mantenimiento muy bajo.
4. Los códigos de salida son la única señal fiable de CI
Por muy cuidada que sea la salida, CI solo lee códigos de salida. Tres trampas son comunes:
| Síntoma | Causa | Corrección |
|---|---|---|
| Hay errores pero CI está en verde | El comando termina con ` | |
| No hay errores pero CI está en rojo | diff trata "diferencias encontradas" como fallo |
Distingue "diferencia" de "error de ejecución" |
| La salida de error está truncada | La salida paralela de varios archivos se pisa a sí misma | Ejecuta en serie o imprime por archivo |
Recuerda la semántica de códigos de salida de jq: el parseo normal devuelve 0; con -e, un resultado de expresión false o null devuelve 1; los errores de uso y archivos inexistentes devuelven 2; los errores de compilación de script devuelven 3. Las herramientas de schema generalmente usan 0 para éxito, 1 para fallo de validación y 2 para errores de herramienta o argumentos. Al escribir un wrapper, no colapses cualquier código distinto de cero en "los datos están mal", o un fallo de entorno se diagnosticará erróneamente como un problema de contenido.
5. pre-commit: detecta problemas antes del commit
El valor de pre-commit no es "una comprobación más"; es dar feedback a los desarrolladores en un minuto en lugar de después de una ejecución de CI de diez minutos. Una configuración mínima se ve así:
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$
Dos detalles importan:
filesdebe coincidir solo con los directorios que necesitan validación. No ejecutes hooks contranode_modulesni archivos generados.- El hook local asume que
jqestá instalado en la máquina del desarrollador. Documenta ese requisito, o cambia a un hook en contenedor.
Si el proyecto ya usa Husky y lint-staged, combínalos directamente:
{
"lint-staged": {
"configs/*.json": ["jq empty", "prettier --check"]
}
}
Recuerda que lint-staged pasa una lista de archivos en staging, así que el comando debe aceptar múltiples rutas. Tanto jq empty como prettier --check lo hacen. No dejes que un hook escanee todo el repositorio; los commits se volverán notablemente más lentos.
6. GitHub Actions: un pipeline que puedes copiar
El workflow siguiente realiza validación de sintaxis, validación de schema y conserva el informe en caso de fallo:
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
El filtro paths evita que cambios solo de documentación disparen la validación. npx --yes encaja con repositorios sin dependencias fijadas; los proyectos de producción deberían añadir ajv-cli a devDependencies y fijar la versión. Sin un lock, una release upstream que cambie el draft o el modo estricto predeterminado puede poner el pipeline en rojo de la noche a la mañana.
7. Localización del error: de números de línea a rutas de campo
La mitad del valor de un validador es la detección; la otra mitad es la localización. Aprende a leer tres tipos de salida:
- Errores de sintaxis:
jqreporta línea y columna, a menudo con un fragmento cercano. Arregla primero corchetes y comas; no sospeches de la codificación de inmediato. - Errores de schema: el
instancePathde Ajv apunta a la ubicación de los datos (por ejemplo/items/0/price),schemaPathapunta a la regla (por ejemplo#/properties/items/items/properties/price/type) ykeywordnombra la restricción violada. - Errores de formato: cuando falla una comprobación de
format, el mensaje a menudo solo dice "must match format "email"". Combínalo coninstancePathpara identificar el campo.
Pega instancePath en el registro de investigación; es mucho más útil que un muro de JSON. En arrays, el índice numérico también indica qué registro falló. Si la clave de negocio es más intuitiva, añade después una línea de mapeo, como items[0].id = ORD-042.
Los errores de schema comunes, como abusar de additionalProperties: false o escribir ramas oneOf que en realidad se solapan, se tratan en Errores comunes al generar JSON Schema. Una regla de validación incorrecta es más peligrosa que ninguna validación, porque crea una falsa sensación de seguridad.
8. Tabla de selección de herramientas
| Necesidad | Herramienta recomendada | Por qué | Ojo con |
|---|---|---|---|
| Solo sintaxis | jq empty |
Cero dependencias, rápido, códigos de salida claros | Espacios en nombres de archivo durante ejecuciones por lotes |
| Errores de línea/columna más amigables | jsonlint |
Salida legible | Actividad de mantenimiento limitada |
| Validación con JSON Schema | ajv-cli |
Ecosistema maduro, errores estructurados | Alinear versiones de draft y cargar soporte de format |
| Repositorio mixto JSON y YAML | check-jsonschema |
Multiformato, compatible con pre-commit | Fuente del schema y acceso a red |
| Feedback rápido antes del commit | pre-commit / lint-staged | Solo procesa archivos en staging | Los hooks deben aceptar múltiples rutas |
| Formato consistente | Prettier | Ampliamente adoptado | Usa --check cuando no quieras reescribir archivos |
Una recomendación final: verifica que el pipeline realmente falla alimentándolo primero con un registro deliberadamente inválido. El mayor riesgo no es una configuración complicada; es una configuración que nunca llega a aplicarse. Tratar "puede fallar" como criterio de aceptación es más fiable que tratar "pasa" como tal. Para la cadena de herramientas de línea de comandos más amplia, continúa con Editores, línea de comandos y automatización.
FAQ
¿Debo usar jq empty o jsonlint?
Prefiere jq empty en CI: cero dependencias, inicio rápido y semántica clara de códigos de salida. Usa jsonlint cuando necesites mensajes de línea/columna más amigables o estés investigando localmente. Ambos realizan solo validación de sintaxis; ninguno comprueba la estructura de los campos.
¿Por qué ajv reporta una palabra clave desconocida?
Primero confirma que el draft de $schema del schema coincide con la versión que usa la CLI. Muchos errores de "palabra clave desconocida" son palabras clave de un draft nuevo encontradas por un compilador antiguo. Si las versiones coinciden, busca una errata. --strict=false puede mantener funcionando schemas heredados, pero no dependas de ello a largo plazo para ocultar problemas reales.
La validación de schema pasa, pero la API sigue fallando. ¿Por qué?
Un schema valida la estructura, no la semántica de negocio. Por ejemplo, status puede ser una cadena con un enum legal, mientras que la máquina de estados prohíbe pasar directamente de paid a refunded. Reglas así pertenecen al código o a restricciones de base de datos, no a JSON Schema.
¿Cómo localizo rápidamente el campo que falla?
Lee el instancePath o el número de línea de la salida de la herramienta. El instancePath de Ajv es un JSON Pointer que apunta directamente a los datos; check-jsonschema reporta archivo y línea. Registra la ruta y mapéala a la clave de negocio; localizar el problema suele llevar minutos.
¿Debería pre-commit ejecutar una validación completa?
No. pre-commit debería procesar solo archivos en staging para mantener rápidos los commits. Los escaneos completos pertenecen a CI o a trabajos programados. La división es simple: las ejecuciones locales favorecen la velocidad, CI favorece la exhaustividad.