🛠️Herramientas

jq y yq: cómo consultar y transformar archivos JSON y YAML desde la terminal

Felipe••15 min de lectura
Aprende a consultar JSON con jq y editar YAML con yq de Mike Farah: filtros, conversiones, curl y ejemplos seguros para Kubernetes y CI/CD.

Recibes una respuesta HTTP con decenas de objetos y solo necesitas los nombres de los servicios que fallan. Después debes cambiar una imagen dentro de un manifiesto YAML, conservando el resto de su estructura. Abrir un editor y buscar manualmente funciona una vez; repetirlo durante una investigación o un despliegue resulta más incómodo y propenso a errores.

jq y yq permiten expresar esas operaciones sobre los datos: recorrer una propiedad, seleccionar elementos y construir una salida distinta. No buscan palabras sueltas como lo haría una búsqueda de texto; interpretan la estructura del documento.

Vamos a trabajar con archivos ficticios y una respuesta HTTP local. El objetivo es aprender expresiones pequeñas que puedas leer y comprobar, y reconocer cuándo una cadena de comandos ya necesita convertirse en un programa con pruebas.

Qué hace jq y cuál de los yq vamos a utilizar

jq procesa JSON mediante filtros. Recibe uno o más valores, aplica una expresión y escribe los resultados. El filtro más sencillo, ., devuelve el valor recibido; por defecto, jq lo presenta con formato legible. El tutorial oficial de jq introduce ese modelo.

yq no identifica un único programa. Dos implementaciones conocidas comparten el nombre del ejecutable, pero no todos sus argumentos:

HerramientaImplementaciónDiferencia importante
jqProyecto jqlang, escrito en CProcesa JSON con su propio lenguaje de filtros
yq de Mike FarahBinario escrito en GoProcesa YAML y otros formatos con sintaxis parecida a jq; no requiere instalar jq
yq de kislyukHerramienta Python que utiliza jqConvierte YAML para procesarlo con jq y puede volver a emitir YAML

Este artículo utiliza yq de Mike Farah, versión 4. Su documentación aclara que su lenguaje no implementa exactamente todo jq. La implementación de kislyuk tiene otras opciones, como -y para producir YAML. No copies indistintamente los comandos de ambas.

Si un tutorial falla con una opción desconocida, comprueba primero yq --version y la procedencia del paquete. Tener un ejecutable llamado yq no basta para identificarlo.

Instalación en macOS y Linux

En macOS, con Homebrew:

brew install jq yq

La fórmula yq corresponde a Mike Farah; la alternativa Python se distribuye como python-yq en Homebrew. En Linux, instala jq mediante el gestor de tu distribución. Por ejemplo, en Debian o Ubuntu:

sudo apt-get update
sudo apt-get install jq

Para yq de Mike Farah, utiliza una opción de su guía oficial de instalación: Homebrew en Linux, Snap o un binario publicado para tu arquitectura. En sistemas con Snap:

sudo snap install yq

Ese paquete utiliza confinamiento estricto; sus restricciones de acceso a archivos pueden importar fuera de tu directorio de trabajo. No presupongas que apt install yq instala la misma implementación. Las descargas de jq también ofrecen binarios independientes.

Comprueba ambas herramientas:

jq --version
yq --version

Los ejemplos se verificaron con jq 1.8.2 y Mike Farah yq 4.54.1. Utilizaremos Bash para los scripts, curl y Python 3 únicamente para servir los archivos locales. En CI, fija versiones verificadas en lugar de depender silenciosamente de lo que incluya el runner.

Preparar una respuesta JSON ficticia

Crea una carpeta vacía llamada jq-yq-lab y trabaja dentro de ella. Guarda lo siguiente como respuesta.json:

{
  "requestId": "lab-001",
  "meta": { "environment": "local", "total": 3 },
  "data": {
    "services": [
      { "name": "api", "status": "healthy", "replicas": 2, "latencyMs": 34 },
      { "name": "worker", "status": "healthy", "replicas": 1, "latencyMs": 12 },
      { "name": "legacy", "status": "degraded", "replicas": 0, "latencyMs": 180 }
    ]
  }
}

Estos valores son datos de prueba, no métricas de una aplicación real. Primero comprueba que jq puede interpretar el archivo:

jq '.' respuesta.json

Que el JSON sea sintácticamente válido no demuestra que cumpla el contrato de una API. Puede faltar data, cambiar el tipo de services o aparecer un estado inesperado. La validación de datos en tiempo de ejecución aborda ese problema desde TypeScript; aquí comprobaremos condiciones concretas sobre la respuesta.

Propiedades anidadas, arrays y filtros

Para obtener el entorno y el primer servicio:

jq '.meta.environment' respuesta.json
jq '.data.services[0].name' respuesta.json

Los resultados son "local" y "api". Los índices comienzan en cero. Si necesitas el texto sin comillas JSON, utiliza -r:

jq -r '.meta.environment' respuesta.json

Ahora la salida es local. La diferencia importa al enviar el resultado a otro comando: -r cambia la representación de las cadenas, no convierte la respuesta en un dato validado o seguro para ejecutar.

Para recorrer todos los servicios y seleccionar los que no están saludables:

jq '.data.services[] | select(.status != "healthy")' respuesta.json

El resultado es el objeto legacy. Dentro de la expresión, | pasa cada resultado al filtro siguiente. Después de .data.services[], el contexto es cada servicio, no el documento completo.

Para extraer nombres:

jq -r '.data.services[] | .name' respuesta.json

La salida obtenida es:

api
worker
legacy

Eso no es un array JSON: son tres resultados escritos sucesivamente. Si necesitas un único array, recógelos entre corchetes:

jq '[.data.services[] | .name]' respuesta.json

También puedes trabajar sobre el array mediante map:

jq '.data.services | map(select(.replicas > 0))' respuesta.json

En estos datos devuelve un array con api y worker. La referencia de filtros de jq documenta el acceso a objetos y arrays, los pipes y la construcción de resultados.

Transformar la respuesta y extraer campos

Para preparar un resumen, construye un objeto nuevo con los campos que necesitas:

jq '{
  entorno: .meta.environment,
  serviciosActivos: [
    .data.services[]
    | select(.replicas > 0)
    | {nombre: .name, replicas: .replicas}
  ]
}' respuesta.json

El resultado verificado es:

{
  "entorno": "local",
  "serviciosActivos": [
    { "nombre": "api", "replicas": 2 },
    { "nombre": "worker", "replicas": 1 }
  ]
}

La entrada no se modifica. Para conservar la transformación, redirige la salida a otro archivo:

jq '.data.services | map({name, status})' respuesta.json > resumen.json

Para una vista breve de terminal, puedes generar columnas separadas por tabulaciones:

jq -r '.data.services[] | [.name, .status] | @tsv' respuesta.json

Así obtienes un inventario de nombres y estados sin copiar manualmente cada registro. El formato elegido debe corresponder al destino: usa JSON cuando otro programa necesite preservar estructura y tipos.

Pasar parámetros sin construir código con cadenas

No incrustes directamente un valor variable dentro de la expresión. Entrégalo como dato:

servicio_buscado='api'

jq --arg nombre "$servicio_buscado" '
  .data.services[]
  | select(.name == $nombre)
' respuesta.json

Las comillas simples protegen la expresión frente a la expansión del shell. $nombre pertenece a jq; $servicio_buscado pertenece al shell. Para un valor JSON, como un número, existe --argjson:

jq --argjson minimo 2 '
  [.data.services[] | select(.replicas >= $minimo) | .name]
' respuesta.json

Con el archivo del ejercicio obtenemos ["api"]. Las opciones --arg, --argjson y -r se describen en el manual de invocación de jq.

Combinar curl y jq con una respuesta HTTP local

Desde la carpeta del laboratorio, abre una terminal y ejecuta:

python3 -m http.server 8798 --bind 127.0.0.1

El servidor publica los archivos de esa carpeta en localhost. Utiliza únicamente la carpeta del ejercicio, sin documentos privados. En otra terminal, situada en el mismo directorio:

set -o pipefail

curl --fail --silent --show-error --max-time 10 \
  http://127.0.0.1:8798/respuesta.json |
  jq '[.data.services[] | select(.status != "healthy") | .name]'

Obtendrás ["legacy"]. Aquí hay dos pipes distintos: el del shell conecta la salida de curl con la entrada de jq; el que aparece dentro de las comillas conecta filtros del lenguaje jq.

--fail hace que curl falle ante respuestas HTTP de error; --silent --show-error oculta el progreso conservando los mensajes de error, y --max-time limita la duración de la transferencia. Estas opciones están documentadas en el manual de curl.

pipefail evita que un éxito del último comando oculte un fallo anterior. No evita que los comandos posteriores comiencen a ejecutarse: si el siguiente paso escribe archivos o despliega, separa la descarga, la validación y la acción.

Para guardar un informe YAML a partir de la misma respuesta HTTP, puedes añadir yq al final:

curl --fail --silent --show-error --max-time 10 \
  http://127.0.0.1:8798/respuesta.json |
  jq '{pendientes: [.data.services[] | select(.status != "healthy") | .name]}' |
  yq -p=json -o=yaml -P '.' > informe.yaml

informe.yaml contiene una propiedad pendientes con legacy como único elemento. Cada herramienta tiene una función concreta: curl obtiene la respuesta, jq selecciona datos y yq cambia el formato. El archivo es un candidato de salida; no debe consumirse si falla el pipeline.

En una API real, esta combinación sirve para extraer identificadores, resumir estados o preparar un informe. Aun así, debes considerar autenticación, paginación y límites: procesar una respuesta no equivale a haber descargado todas las páginas. No uses eval para ejecutar contenido recibido ni imprimas tokens en el historial o los logs.

Leer y modificar YAML con yq

Guarda este archivo como config.yaml:

app:
  name: pedidos
  environment: local
server:
  port: 3000
  timeoutSeconds: 10
features:
  audit: false
replicas: 2

Consulta una propiedad anidada:

yq '.server.port' config.yaml

Devuelve 3000. Puedes proponer un cambio sin escribir sobre la entrada:

yq '.server.port = 8080 | .features.audit = true' config.yaml

La salida contiene el documento actualizado, pero config.yaml conserva sus valores. Para probar la modificación directa, trabaja sobre una copia:

cp config.yaml config.editada.yaml

yq -i '.server.port = 8080 | .features.audit = true' config.editada.yaml

-i modifica el archivo indicado. La referencia de eval describe ese comportamiento; eval es el comando predeterminado en las versiones v4 actuales.

Si una configuración llega desde el entorno, decide si debe conservarse como texto o interpretarse como un valor YAML:

APP_ENV=staging yq '.app.environment = strenv(APP_ENV)' config.yaml

REPLICAS=3 yq '.replicas = env(REPLICAS)' config.yaml

strenv produce una cadena; env interpreta el valor como YAML, de modo que 3 se convierte en un número. Esa flexibilidad también exige validar entradas: no todas las variables externas deben convertirse automáticamente en mapas, listas o booleanos. Consulta los operadores de variables de entorno.

Convertir entre JSON y YAML

Para convertir nuestro JSON en YAML y nuestra configuración en JSON:

yq -p=json -o=yaml -P '.' respuesta.json > respuesta.yaml

yq -o=json '.' config.yaml > config.json

-p indica el formato de entrada y -o el de salida; -P solicita una presentación YAML legible. Indicar formatos explícitos evita depender de la extensión o de la detección de entrada.

También puedes combinar ambos procesadores:

yq -o=json '.' config.yaml |
  jq '{aplicacion: .app.name, puerto: .server.port}'

El resultado conserva puerto como número. Pero convertir no es preservar todos los detalles del archivo: JSON no representa comentarios YAML y los alias pueden convertirse en valores expandidos. Un recorrido YAML → JSON → YAML no garantiza recuperar el formato original. La guía de conversión de yq muestra esos efectos.

Incluso al editar YAML directamente, yq intenta conservar comentarios y espacios, pero no resuelve todos los casos. Revisa el diff si el archivo utiliza anclas, estilos especiales o comentarios importantes.

Un cambio acotado en un manifiesto Kubernetes

En Kubernetes es frecuente inspeccionar JSON producido por kubectl o actualizar un manifiesto antes de revisarlo. Practicaremos la segunda operación sin conectarnos a un clúster.

Guarda este manifiesto ficticio como deployment.yaml. Las imágenes usan un dominio reservado para ejemplos y no están destinadas a desplegarse:

apiVersion: apps/v1
kind: Deployment
metadata:
  name: pedidos
spec:
  replicas: 2
  selector:
    matchLabels:
      app: pedidos
  template:
    metadata:
      labels:
        app: pedidos
    spec:
      containers:
        - name: api
          image: registry.example.invalid/pedidos:1.0.0
        - name: auxiliar
          image: registry.example.invalid/auxiliar:1.0.0

Primero comprueba que existe exactamente un contenedor llamado api:

yq -e '
  [.spec.template.spec.containers[] | select(.name == "api")]
  | length == 1
' deployment.yaml

Después genera un candidato, conservando el archivo original:

IMAGEN='registry.example.invalid/pedidos:2.0.0' yq '
  (.spec.template.spec.containers[]
    | select(.name == "api")
    | .image) = strenv(IMAGEN)
' deployment.yaml > deployment.candidato.yaml

Los paréntesis delimitan la ruta que se actualiza. Sin ellos, una expresión que primero filtra el contenedor puede terminar devolviendo solo ese fragmento y perder el resto del manifiesto. La documentación explica la precedencia de operadores y asignaciones.

Comprueba ambas imágenes:

yq '.spec.template.spec.containers[] | .image' deployment.candidato.yaml

El contenedor api queda en 2.0.0; auxiliar conserva 1.0.0. La comprobación previa importa porque una selección sin coincidencias puede dejar el documento sin cambios sin representar un error de ejecución.

En un entorno autorizado, la referencia de kubectl documenta la salida JSON. Por ejemplo, este patrón de lectura permite resumir nombres y fases de los pods de un namespace:

kubectl --context contexto-laboratorio -n laboratorio get pods -o json |
  jq -r '.items[] | [.metadata.name, .status.phase] | @tsv'

Ese comando requiere un contexto y namespace reales que hayas autorizado; no forma parte de la ejecución local anterior. La fase de un pod tampoco equivale a toda su salud: Running no asegura que todos sus contenedores estén listos. Para una inspección interactiva complementaria, puedes usar k9s para diagnosticar Kubernetes.

Códigos de salida: un filtro útil para CI/CD

Imprimir una lista vacía puede ser un resultado correcto para jq, aunque represente un problema para tu pipeline. Con -e, jq hace depender el código de salida del último resultado: false o null fallan, y no producir resultados también tiene un estado no satisfactorio. Un array vacío sigue siendo un valor distinto de false y null.

Por eso conviene expresar una condición explícita. Guarda este script como comprobar-servicios.sh:

#!/usr/bin/env bash
set -euo pipefail

curl --fail --silent --show-error --max-time 10 \
  http://127.0.0.1:8798/respuesta.json |
  jq -e '
    .data.services
    | type == "array"
      and length > 0
      and all(.[]; .status == "healthy")
  '

Ejecútalo con Bash:

bash comprobar-servicios.sh

Con nuestros datos imprime false y termina con código 1, porque legacy está degradado. Ese fallo es el resultado esperado del control. Si todos los servicios están saludables, produce true y código 0; una lista vacía tampoco supera la condición.

Esto comprueba una regla específica, no todo el contrato de la API. En un pipeline real pueden hacer falta validación de campos, tiempos de espera, reintentos acotados y decisiones distintas para errores de red y de negocio.

En yq, -e también permite fallar cuando la expresión no encuentra resultados o devuelve null o false. Para automatizar la edición del manifiesto, detén el script si falla la comprobación de unicidad, genera el candidato, valídalo y revisa el diff antes de aplicar cambios. Un parser YAML no demuestra por sí solo que el manifiesto sea válido para la API de Kubernetes.

Precauciones antes de automatizar modificaciones

No redirijas la salida sobre el mismo archivo que estás leyendo. Por ejemplo, el patrón jq '...' archivo.json > archivo.json es peligroso: el shell puede truncar la entrada antes de que jq la lea. Usa un archivo candidato distinto y reemplaza el original únicamente tras validar el resultado. Para cambios directos con yq -i, conserva una copia o trabaja en una rama de Git revisable.

También conviene:

  • Comprobar coincidencias y tipos. «El comando terminó» no demuestra que cambió el elemento correcto.
  • Revisar archivos con varios documentos YAML: una expresión de eval se aplica a cada documento. No trates un manifiesto compuesto como si contuviera un único objeto.
  • Evitar expresiones construidas con texto externo. Usa parámetros y variables como datos; no ejecutes expresiones suministradas por una respuesta HTTP.
  • Conservar identificadores grandes como cadenas cuando el contrato lo permita y comprobar la precisión necesaria antes de hacer cálculos o conversiones.
  • Evitar imprimir configuraciones completas si contienen secretos y limitar las columnas de los informes.
  • Fijar versiones en automatizaciones y verificar el cambio con git diff antes de incorporarlo al repositorio.

En este laboratorio no modificamos servicios ni configuraciones del sistema. Al terminar, detén el servidor local con Ctrl+C; puedes conservar los archivos de prueba o borrar la carpeta del ejercicio.

Cuándo dejar la expresión y escribir un script

Una selección, una proyección o un cambio de propiedad suelen encajar bien en jq o yq. Si la expresión crece, empieza por darle saltos de línea y nombres claros. jq permite guardar el filtro en un archivo y ejecutarlo con jq -f filtro.jq respuesta.json, sin meter todo el programa dentro del comando.

Cuando aparecen múltiples peticiones paginadas, reintentos, credenciales, varias escrituras coordinadas o reglas de negocio, un script en TypeScript o Python puede resultar más fácil de probar y mantener. No necesitas abandonar jq: puede seguir siendo una parte pequeña y explícita de ese proceso.

El criterio es que otra persona pueda entender qué entra, qué se transforma y qué ocurre cuando algo falla. Una línea corta que oculta esos tres puntos no es necesariamente más simple que un programa de veinte líneas.

Referencias

#jq#yq#JSON#YAML#curl#terminal#Kubernetes#CI/CD

Artículos relacionados