JSON Toolkit

JSON a YAML desde la línea de comandos: cuatro trampas detrás de un solo comando yq

En resumen: El comando más copiado en los tutoriales, yq -P input.json > output.yaml, sigue emitiendo JSON en yq v4. Medimos yq v4.44.6, PyYAML 6.0.3 y js-yaml con una muestra de 2.39 MB, explicamos por qué la extensión de entrada fija el formato de salida y damos las recetas correctas para conversión por lotes, comillas, indentación y precisión de enteros grandes.

Intermedio~9 min de lecturaPublicado el 2026-11-03Actualizado el 2026-11-03
Portada que compara un comando de conversión de JSON a YAML con su salida en la terminal

En resumen: yq -P input.json > output.yaml — el comando más copiado en todos los tutoriales — sigue produciendo JSON en yq v4. Para obtener YAML hay que pasar -o=yaml (o -oy) de forma explícita. -P es la abreviatura de --prettyPrint: controla la indentación y el estilo de bloque, no el formato de salida.

Convertir JSON a YAML nunca fue cuestión de cambiar la extensión de un archivo. El coste real se esconde en cuatro sitios: la extensión de entrada decide el formato de salida, las reglas de comillas reescriben tus cadenas en silencio, la indentación de las secuencias afecta a las herramientas posteriores y un entero grande que pasa por yq pierde precisión para siempre.

1. Ejecuta primero el comando más popular

En una máquina limpia con yq v4.44.6 instalado, crea una entrada mínima:

printf '{"a":1,"b":{"c":2}}' > s.json
yq -P '.' s.json

Lo que devuelve no es YAML, sino JSON con indentación:

{
  "a": 1,
  "b": {
    "c": 2
  }
}

Si sigues el tutorial y rediriges esa salida a output.yaml, obtienes un archivo con extensión .yaml cuyo contenido es JSON. Cualquier analizador YAML 1.2 lo lee sin problemas (JSON es un subconjunto de YAML 1.2; la demostración completa de esa relación de contención está en límites de la conversión entre JSON, YAML y XML), así que nada falla de inmediato. Falla el día en que alguien añade un comentario o un ancla, o cuando yamllint en CI empieza a escupir una pared de truthy value should be one of.

Este es un fallo especialmente sigiloso en equipos que trabajan en español: el fichero se genera en un job de GitHub Actions, se revisa durante meses sin que nadie abra el contenido y todo el mundo asume que es YAML «porque termina en .yaml». La alarma salta solo cuando se añade yamllint al pipeline o cuando un compañero intenta insertar un comentario con # y el analizador se queja de una sintaxis que, en JSON, ni siquiera existe. Para entonces ya hay decenas de artefactos generados con el formato equivocado y hay que regenerarlos todos.

La invocación que sí produce YAML nombra el formato de salida:

yq -o=yaml '.' s.json
# a: 1
# b:
#   c: 2

2. Por qué: la extensión decide el formato de salida

yq v4 sigue la regla de que «el formato de entrada decide el formato de salida». Infieren el formato a partir de la extensión del archivo de entrada y, sin -o, reutilizan esa inferencia. Entrada .json → salida JSON. Entrada .yaml → salida YAML. Al leer desde stdin sin extensión, el valor por defecto es YAML.

Cuatro invocaciones sobre la misma entrada, medidas:

Comando Origen de la entrada Formato de salida real Veredicto
yq -P '.' s.json archivo s.json JSON (con indentación) trampa al redirigir a .yaml
yq -o=yaml '.' s.json archivo s.json YAML (estilo de bloque) correcto
yq -P -o=yaml '.' s.json archivo s.json YAML (estilo de bloque) correcto, -P sobra aquí
cat s.json | yq -P '.' stdin, sin extensión YAML (estilo de bloque) correcto, YAML es el valor por defecto

La tercera fila dice algo importante: -P no siempre es necesario cuando ya está -o=yaml, pero tampoco es inofensivo: véanse los tiempos de la sección sexta. La cuarta fila explica por qué «lo copié y funcionó»: mucha gente alimenta yq por una tubería, y eso esquiva justamente la rama de la extensión.

Conviene subrayar el contraste con jq, que siempre devuelve JSON independientemente de la extensión del archivo. Quien viene de jq da por hecho que la herramienta «ya sabe» qué formato quiere, y por eso no sospecha de -P. Y quien alimenta datos con cat archivo | yq nunca ve el problema: en ese camino yq ya asume YAML, de modo que el tutorial parece correcto. La divergencia entre ambos caminos es la razón por la que esta trampa sobrevive en tantas guías: cada lector prueba una variante distinta y ambas «funcionan», pero solo una funciona por el motivo correcto.

3. Conversión por lotes: tres recetas de shell

Convertir un solo archivo es solo el punto de partida. Casi todos los motivos reales para hacerlo en la línea de comandos son por lotes: todos los archivos de configuración de un directorio, todas las capturas de una API, de una vez.

Bucle bash / zsh (el más legible, para un solo directorio):

for f in configs/*.json; do
  yq -o=yaml '.' "$f" > "${f%.json}.yaml"
done

find recursivo (cuando importan los subdirectorios):

find . -name '*.json' -exec sh -c 'yq -o=yaml "." "$1" > "${1%.json}.yaml"' _ {} \;

PowerShell (en CI de Windows):

Get-ChildItem *.json | ForEach-Object { yq -o=yaml '.' $_.FullName > ($_.BaseName + '.yaml') }

Las tres comparten un requisito duro: valida el JSON de origen primero. Un convertidor no es un validador: ante un error de sintaxis, muchos convertidores solo dan una posición vaga, en lugar del detalle por línea que ofrece un validador. Añadir una comprobación dentro del bucle cuesta casi nada:

for f in configs/*.json; do
  jq empty "$f" || { echo "invalid: $f"; continue; }
  yq -o=yaml '.' "$f" > "${f%.json}.yaml"
done

jq empty analiza sin imprimir y sale con un código distinto de cero si el JSON es inválido. Combinado con set -euo pipefail, la tubería se detiene en el primer archivo malo en vez de producir un montón de resultados a medias. Los validadores basados en esquemas suelen indicar la ruta exacta del campo con instancePath; un convertidor rara vez ofrece esa precisión, y por eso la verificación previa no es un lujo sino una red de seguridad.

Dos detalles que se reparten los sustos en entornos reales. Primero, las comillas alrededor de "$f": sin ellas, un nombre de archivo con espacios —habitual en carpetas compartidas de equipos de documentación— parte la ruta y el bucle escribe en el sitio equivocado. Segundo, ${f%.json}.yaml recorta el sufijo por la derecha: si tus archivos se llaman config.prod.json, el resultado será config.prod.yaml, que es justo lo que se quiere, pero conviene comprobarlo una vez antes de lanzar el bucle sobre cientos de ficheros.

4. Cuatro parámetros de salida, cuatro tareas distintas

La lista de parámetros de yq es larga, pero en el camino de JSON a YAML solo importan cuatro:

Parámetro Qué hace Cuándo debes pasarlo
-o=yaml / -oy Fija el formato de salida a YAML Siempre que la entrada sea un archivo .json
-P / --prettyPrint Fuerza el estilo de bloque y normaliza la indentación Al leer de stdin, o cuando importa la indentación de secuencias
-I=n Ancho de indentación, por defecto 2 Cuando el consumidor exige 4 espacios (algunas guías de estilo de Ansible)
--split-exp Divide los elementos de un array en documentos Al generar manifiestos Kubernetes multirrecurso

La de los documentos múltiples merece un aparte. Los manifiestos de Kubernetes separan los recursos con --- por convención. Para generarlos desde un array JSON:

yq -P -o=yaml '.[] | splitDoc' --split-exp 'true' kubernetes-list.json

Sin --split-exp obtienes una única secuencia YAML; con él, cada elemento del array se convierte en un documento independiente separado por ---. kubectl apply acepta ambos, pero la legibilidad del diff difiere en un orden de magnitud: en una revisión de código, un array gigante obliga a contar llaves, mientras que los documentos separados dejan ver cada recurso completo.

El parámetro -I=n es el que más se olvida. yamllint y buena parte de los playbooks de Ansible exigen una indentación concreta; si generas YAML con 2 espacios y el repositorio usa 4, el archivo es válido pero el lint lo rechaza en el primer commit. Fijar -I=4 en el script de conversión evita un diff ruidoso en el que cada línea aparece modificada solo por el sangrado.

5. Comillas: seis formas de cadena que hay que entrecomillar

La inferencia de tipos de YAML es implícita. Una cadena que parece un número o un booleano se analiza como tal si no va entre comillas. No es un riesgo teórico: medido en yq v4.44.6, los seis valores siguientes reciben comillas automáticamente, lo que demuestra que el analizador realmente los leería mal:

printf '{"version":"1.0","enabled":"true","port":"8080","empty":"","colon":"host: value","hash":"v # c","unicode":"端口 8080","ts":"2026-11-03"}' | yq -P -o=yaml '.'
version: "1.0"
enabled: "true"
port: "8080"
empty: ""
colon: 'host: value'
hash: 'v # c'
unicode: 端口 8080
ts: "2026-11-03"

Las consecuencias, una por una:

Valor original (cadena JSON) Se lee como, sin comillas Impacto real
"1.0" Float 1.0 La versión se vuelve decimal; 1.10 se colapsa a 1.1
"true" Booleano true La lógica condicional se invierte o los tipos no coinciden
"8080" Entero 8080 El puerto cambia de tipo y falla la comparación de cadenas
"" null La cadena vacía desaparece y el consumidor recibe None
"host: value" Error de análisis o mapa anidado Tras dos puntos debe ir un espacio
"2026-11-03" Objeto fecha Se deserializa a datetime.date, no a una cadena

La última es la más traicionera. En JSON una fecha es solo una cadena; tras la conversión se convierte en una fecha YAML, y muchas implementaciones la serializan de vuelta como objeto de fecha en lugar de la cadena original, rompiendo la consistencia de ida y vuelta. Tanto PyYAML como yq añaden comillas aquí, lo que equivale a admitir que el riesgo es real.

Nótese también que unicode: 端口 8080 sale sin comillas: el texto CJK es seguro en YAML. Lo que debe preocuparte es el orden de las claves y la precisión, que vienen a continuación.

En configuraciones escritas por equipos hispanohablantes hay un caso extra que no aparece en la tabla y que merece vigilancia: los valores sí y no, las fechas con formato 03/11/2026 y los decimales escritos con coma como 0,5. Con un analizador YAML 1.1 —todavía presente en bibliotecas antiguas— la cadena no se interpreta como el booleano false, de modo que un campo activo: no cambia de significado sin que nadie toque el código. La contramedida es la misma de siempre: entrecomilla cualquier valor que «parezca» otra cosa, y no confíes en que el convertidor adivine tu intención. Los códigos postales con ceros a la izquierda, como 0150, entran en la misma categoría: sin comillas se convierten en un número y el cero desaparece.

6. Equivalentes en Python y Node, con tiempos medidos

No puedes instalar yq en todas partes: máquinas aisladas de la red, imágenes de CI bloqueadas, Windows sin gestor de paquetes. Los equivalentes en Python y Node existen, cada uno con sus propios valores por defecto esperando morder. Para una comparación por escenarios de las tres rutas, consulta selección de herramientas CLI y editores JSON; este artículo se queda en la dirección JSON → YAML.

Python + PyYAML (tres argumentos, ninguno opcional):

import json, yaml

with open("config.json", encoding="utf-8") as f:
    data = json.load(f)

with open("config.yaml", "w", encoding="utf-8") as f:
    yaml.safe_dump(
        data, f,
        default_flow_style=False,  # estilo de bloque, no {a: 1, b: 2}
        allow_unicode=True,        # conservar CJK tal cual, no \u7AEF\u53E3
        sort_keys=False,           # conservar el orden original de las claves
    )

Esos tres valores por defecto son False / False / True, lo que significa que PyYAML recién instalado ordena las claves alfabéticamente y escapa el CJK a \uXXXX. Medido sobre la misma entrada con los valores por defecto:

apiVersion: v1
data:
  LOG_LEVEL: info
  colon: 'host: value'
  unicode: "\u7AEF\u53E3 8080"
kind: ConfigMap
metadata:
  labels:
    app: web
  name: app-config

Fíjate en que kind queda ahora detrás de apiVersion y delante de metadata. La convención de Kubernetes pone apiVersion antes de kind; reordenar no rompe el análisis, pero hace miserable la revisión de código: quien revisa un manifiesto espera un orden conocido, y un diff que permuta todas las claves parece mucho más grande de lo que es.

Node + js-yaml:

const fs = require("fs");
const yaml = require("js-yaml");

const data = JSON.parse(fs.readFileSync("config.json", "utf8"));
fs.writeFileSync(
  "config.yaml",
  yaml.dump(data, { lineWidth: -1, noRefs: true, sortKeys: false })
);

lineWidth: -1 desactiva el plegado de líneas (si no, las URL largas se parten por la mitad) y noRefs: true suprime anclas y alias, obligatorio cuando el consumidor es kubectl u otra herramienta que no los resuelve.

Tiempos medidos (muestra: JSON de 2.39 MB, 12000 registros, cada uno con un objeto anidado y dos arrays; la máquina es el sandbox usado para esta tarea):

Implementación Convertir Analizar de vuelta Ida y vuelta
yq -o=yaml v4.44.6 0.45 s 0.45 s idéntico
yq -P -o=yaml v4.44.6 4.16 s — idéntico
Node js-yaml 4.x 0.23 s 0.11 s idéntico
Python PyYAML 6.0.3 3.26 s 8.01 s idéntico

Dos resultados contraintuitivos. Primero, añadir -P hace yq unas 9 veces más lento (0.45 s → 4.16 s) mientras produce una salida idéntica byte a byte en esta muestra (2,326,895 bytes en ambos casos): -P ejecuta una pasada completa de recomposición del documento, y ese coste se acumula al convertir cientos de archivos en un job de CI. Segundo, el análisis inverso de PyYAML (8.01 s) es más del doble que su volcado directo (3.26 s), así que Python es mala elección si tu tubería relee YAML una y otra vez.

Una más: 2.39 MB de JSON se convirtieron en 2.33 MB de YAML en estilo de bloque, aproximadamente un 3% más pequeño. El folklore de que «YAML siempre es más grande que JSON» no se sostiene: en estructuras muy anidadas, las comillas, las comas y las llaves de JSON pueden pesar más que el espacio en blanco de la indentación de YAML. En la práctica, la elección de la ruta suele venir impuesta por el entorno antes que por los tiempos: si el runner de CI ya tiene Node instalado, js-yaml es la opción más rápida y no exige descargar un binario; si el equipo trabaja offline con un Python preinstalado, PyYAML es la única vía disponible y conviene asumir su coste de relectura.

7. Tres cosas que no vuelven

Las secciones anteriores tratan de cómo convertir bien. Esta trata de lo que se pierde. Una entrada construida a propósito, medida:

printf '{"order_id":12345678901234567890,"zip":"0150","ts":"2026-11-03","f":1.0e6}' > t.json
yq -o=yaml '.' t.json

yq emite:

order_id: 1.2345678901234567e+19
zip: "0150"
ts: "2026-11-03"
f: 1000000

Y al convertir de vuelta: {"order_id":12345678901234567000,"zip":"0150","ts":"2026-11-03","f":1000000}.

El identificador de pedido de 20 dígitos 12345678901234567890 se convierte en 12345678901234567000: los tres últimos dígitos a cero, sin ningún aviso. yq analiza por la ruta float64 de Go; los enteros por encima de 2^53 no se pueden representar con exactitud, así que recurre a la notación científica y el valor original ya no está cuando vuelves a convertir.

La misma entrada pasada por PyYAML:

order_id: 12345678901234567890
zip: '0150'
ts: '2026-11-03'
f: 1000000.0

Los enteros de Python tienen precisión arbitraria, así que los 20 dígitos sobreviven y el viaje de ida y vuelta queda intacto. Si tus datos contienen identificadores de pedido, snowflake ids o números de documento, no uses yq: usa Python, o guarda el campo como cadena desde el principio. El mecanismo completo detrás de esta clase de fallos se explica en por qué los enteros grandes pierden precisión en JSON.

La segunda pérdida es la distinción entre float e int: 1.0e6 se convierte en 1000000 en la salida de yq mientras PyYAML conserva 1000000.0. La tercera es el orden de las claves: PyYAML reordena por defecto, yq y js-yaml lo conservan, una fuente oculta de desacuerdo en equipos que mezclan lenguajes.

El caso tiene una lectura especialmente incómoda para tiendas online y pasarelas de pago: un número de pedido o un identificador de transacción de 20 dígitos no salta a la vista en un diff porque la diferencia son tres ceros al final, y el archivo sigue siendo YAML válido. La única defensa fiable es un test de ida y vuelta que compare campo por campo el JSON original con el resultado de JSON → YAML → JSON, y que falle si cambia un solo dígito. Comparar solo el número de bytes o de campos no basta: aquí los dos archivos tienen exactamente el mismo tamaño.

8. En CI, no te limites a convertir: valida, convierte, lint

Tratar la conversión como único paso es el defecto de tubería más común. Un código de salida cero significa «el JSON de entrada se analizó», no «la salida es correcta» ni «la salida satisface al consumidor». La forma correcta, en tres etapas:

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

# 1. Validar el origen: capa sintáctica
jq empty deployment.json

# 2. Convertir: nombrar explícitamente el formato de salida
yq -o=yaml '.' deployment.json > deployment.yaml

# 3. Validar el artefacto: YAML analizable y limpio para lint
yq -o=json -I=0 '.' deployment.yaml > /dev/null
yamllint deployment.yaml

Ese yq -o=json ... > /dev/null del paso 3 parece redundante y es la línea más útil del bloque: obliga a analizar de nuevo el artefacto. Los errores de indentación, los fallos al resolver anclas y los tipos reescritos aparecen justo ahí. Si lo saltas, el problema espera hasta kubectl apply o hasta el arranque de la aplicación, cuando ya está desplegado.

Si el artefacto es una configuración mantenida por personas (valores de Helm, docker-compose, flujos de GitHub Actions), confirma además tres cosas: que el orden de las claves no se ha desordenado, que los números largos no han perdido dígitos y que los comentarios siguen teniendo que añadirse a mano. JSON no admite comentarios, así que toda anotación debe escribirse después de la conversión; ningún convertidor las inventará por ti.

En un workflow de GitHub Actions este patrón encaja sin fricción: el job de validación corre en un runner limpio, jq y yamllint se instalan en el mismo paso, y el script termina publicando el artefacto solo si las tres etapas pasan. Merece la pena fijar las versiones de las herramientas en el workflow —incluida la de yq— porque el comportamiento descrito en la sección segunda pertenece a yq v4 y no necesariamente a la versión que el runner instale por defecto el año que viene.

Preguntas frecuentes

¿Debo usar yq -P o -o=yaml?

-o=yaml decide si la salida es YAML; -P decide cómo se ve ese YAML. Con una entrada .json, -o=yaml es obligatorio y -P solo hace falta cuando quieres forzar el estilo de bloque o la indentación de secuencias. Al leer desde stdin sin extensión, yq ya produce YAML por defecto, así que -P por sí solo basta.

¿Puedo pasar el YAML convertido directamente a Kubernetes?

Sintácticamente sí, con tres cautelas: que las claves con convenciones de orden como apiVersion no se hayan reordenado (PyYAML lo hace por defecto; pon sort_keys=False), que no se hayan generado anclas ni alias (js-yaml: pon noRefs: true) y que los números largos conserven sus dígitos. Para manifiestos multirrecurso, separa en documentos con --- mediante --split-exp.

¿Por qué mi salida es una sola línea {a: 1, b: 2} en lugar de ir indentada?

Eso es el estilo flow. Es YAML válido, pero deja sin sentido la conversión. Usa -P para forzar el estilo de bloque; con PyYAML, confirma default_flow_style=False (desde PyYAML 5.1 ya vale False por defecto; las versiones antiguas lo necesitan explícito).

Mi identificador de pedido de 20 dígitos perdió las últimas cifras. ¿Puedo recuperarlas?

No: hay que prevenirlo. yq analiza por float64, así que los enteros por encima de 2^53 pierden precisión por construcción. O cambias a Python (enteros de precisión arbitraria; verificado sin pérdida) o guardas los números largos como cadenas en el JSON, lo que además resuelve el problema equivalente de JSON.parse en el lado JavaScript.

¿Herramienta online o línea de comandos?

Para configuraciones con secretos y capturas de API con tokens, usa la línea de comandos o una herramienta que corra en el cliente: nunca las subas. Para tareas puntuales y pequeñas en las que quieres confirmación visual, una herramienta online es más rápida; la página /convert/yaml de este sitio se ejecuta por completo en tu navegador y los datos no salen de tu máquina. Para todo lo que sea por lotes, reproducible o ligado a CI, usa la línea de comandos y escribe los pasos de validación en el mismo script.