¿Por qué se rompe mi YAML? Los errores ruidosos y los silenciosos
Dos tickets llegan la misma mañana y ambos dicen «se rompió mi YAML». Uno es un error de análisis rojo en la línea 12, resuelto en un minuto. El otro es un servicio que arrancó bien pero se comporta mal, porque un valor sin comillas se convirtió en silencio en otra cosa. Son problemas opuestos, y el peligroso es el callado.
Dos tickets llegan la misma mañana, y ambos dicen lo mismo: se rompió mi YAML.
El primero es un error de análisis rojo: el log de CI apunta a la línea 12, «no se permiten tabulaciones como indentación», y se arregla en menos de un minuto. El segundo es más raro: un servicio arrancó limpiamente, sin errores en ningún lado, pero está sirviendo la región equivocada, y tras una hora mirando lo encuentras — alguien escribió region: NO para Noruega, y la configuración lo cargó como el booleano false. La misma queja, dos problemas completamente distintos. Y el segundo es el que llega a producción, precisamente porque nada se rompió lo bastante fuerte como para detenerlo.
Eso es lo que hay que entender de YAML: «se rompió» son dos fallos opuestos vestidos con una misma frase. Uno es ruidoso — el analizador rechaza el archivo y te dice exactamente dónde. El otro es silencioso — el archivo se analiza bien y le entrega a tu programa un valor que nunca escribiste. El ruidoso te cuesta diez minutos; el silencioso llega a producción. En cuanto sabes cuál tienes delante, YAML deja de parecer embrujado, porque cada uno pide un movimiento de depuración opuesto. Este es el hermano del artículo pilar sobre formatos de configuración, que sostenía que la costumbre de YAML de adivinar es el precio de su comodidad — aquí liquidamos cómo muerde exactamente ese adivinar, y cómo distinguirlo de un error de sintaxis corriente.
Los fallos ruidosos: YAML no analiza (y esa es la buena noticia)
Cuando YAML rechaza un archivo, date por afortunado. El analizador ha atrapado el problema por ti y te ha entregado una línea y una columna. Son molestos, no peligrosos — sigues el puntero y arreglas la sintaxis. Estos son los que de verdad te encontrarás.
Tabulaciones usadas para indentar. El más común de todos. YAML prohíbe el carácter de tabulación para indentar — la estructura es solo espacios.
server:
host: localhost
Esa tabulación delante de host produce, al pie de la letra:
YAMLParseError: Tabs are not allowed as indentation at line 2, column 1
El arreglo es lo que dice el mensaje: reemplaza la tabulación por espacios. Este muerde más fuerte cuando un editor inserta tabulaciones sin que se vean, o un copiar-pegar las arrastra — el archivo parece bien indentado y solo el analizador ve la tabulación.
Indentación inconsistente. YAML deduce tu estructura de cuánto se indenta cada línea, así que un espacio de más o de menos cambia qué anida bajo qué — y a menudo se vuelve un error que el analizador no puede resolver.
database:
host: localhost
port: 5432
port está indentado un espacio más que host, y el analizador rechaza el anidamiento imposible. El arreglo es alinear las claves hermanas a la misma columna, exactamente.
Una comilla o un corchete sin cerrar. Las colecciones de flujo ([...], {...}) y las cadenas entre comillas deben cerrarse.
ports: [80, 443
te da Flow sequence … must be sufficiently indented (o un error de corchete faltante, según lo que siga) — y name: "prod da Missing closing "quote. Ambos nombran la línea; ambos se arreglan cerrando lo que abriste.
Claves duplicadas. Dos claves con el mismo nombre en un mismo mapa son ambiguas, y un analizador estricto las rechaza de plano:
region: us-east-1
region: eu-west-1
→ Map keys must be unique at line 2, column 1. (No todos los analizadores son así de estrictos — algunos se quedan en silencio con el último valor, lo que desliza esto sin ruido hacia la otra categoría. Cuando el tuyo sí lo rechaza, te está haciendo un favor.)
Para cada fallo de esta sección el movimiento es idéntico: lee el error, ve a la línea y columna que nombra, arregla la sintaxis ahí. El analizador está de tu lado. Ahora, la mitad en la que no lo está.
Los fallos silenciosos: YAML analiza bien y te entrega el valor equivocado
Aquí no hay error rojo, ni número de línea, nada que seguir. El archivo se analizó «con éxito». El problema es que con éxito significó algo distinto de lo que escribiste — una cadena se volvió booleano, una clave vacía se volvió null, una versión se volvió número. Tu programa arranca con datos malos y falla en algún punto lejano de la causa real. Este es el caro, y viene con varias caras.
El espacio que falta tras los dos puntos. Una clave necesita dos puntos seguidos de un espacio (o un salto de línea). Sáltate el espacio y no obtienes un error — obtienes una cadena simple:
timeout:30
se analiza como la cadena "timeout:30", no como una clave timeout con valor 30. Tu config.timeout es undefined, y nada en ninguna parte lo dijo.
Null implícito. Una clave sin nada detrás no es una cadena vacía — es null. También lo son ~, null, Null y NULL:
retries:
fallback: ~
cache: null
Los tres valores son null. Si esperabas que retries fuera una cadena vacía, un 0, o «sin definir usa el valor por defecto», puede que en cambio estés entregando un null literal a código que no lo comprueba. El valor parece ausente; en realidad está presente y es null.
El problema de Noruega y su parentela. Este es el del artículo pilar, y es el fallo silencioso estrella. Bajo YAML 1.1 — todavía el valor por defecto en analizadores como PyYAML — un montón de palabras sueltas se vuelven booleanos:
region: NO # el booleano false, no el código de país "NO"
enabled: yes # el booleano true
debug: off # el booleano false
yes, no, on, off, y, n y sus mayúsculas se convierten todas en booleanos. El core schema de YAML 1.2 eliminó esto — ahí NO sigue siendo una cadena — pero muchos runtimes aún usan 1.1 por defecto, así que no des por hecho que estás a salvo. (La historia completa está en el pilar.)
Números que se comen tus cadenas. Cualquier cosa con forma de número se vuelve uno, tirando lo que la hacía significativa como texto:
version: 1.10 # el número 1.1 — el cero final desaparece
build: 1e5 # el número 100000 — leído como notación científica
zip: 01234 # 1234 (1.2 core) o 668 (octal, 1.1) — el cero inicial se esfuma
port: 0700 # 700 (1.2 core) o 448 (octal, 1.1)
time: 22:22 # 1342 bajo 1.1 (base 60: 22×60 + 22); una cadena bajo 1.2 core
Si cada uno se convierte —y en qué— depende del analizador y del esquema que aplique; bajo uno que use estas reglas implícitas, cambian de tipo sin error alguno. (22:22, por ejemplo, sigue siendo una cadena bajo el core schema de YAML 1.2, pero pasa a 1342 bajo 1.1.) Una comprobación de versión que esperaba "1.10" se compara contra 1.1 y no coincide; un ID con ceros a la izquierda pierde su cero sin ruido.
Bajo un analizador así, ninguno suele dar error, de modo que no hay línea a la que saltar — para el analizador no ha pasado nada. El movimiento de depuración no puede ser «sigue el error», porque no hay error. Tiene que ser un hábito en su lugar.
Cómo depurar cada uno
Todo el juego está en distinguir los dos, y la prueba es una sola pregunta: ¿obtuviste un error de análisis o no?
- Obtuviste un error. Es un fallo ruidoso. El mensaje lleva línea y columna — ve ahí y arregla la sintaxis. Tabulaciones a espacios, hermanos alineados, comillas y corchetes cerrados, claves duplicadas renombradas. Diez minutos, como mucho.
- Analizó, pero algo más adelante está mal. Es un fallo silencioso, y no hay línea que seguir. No releas el archivo entero buscando una errata — mira el valor que tu programa recibió de verdad y trabaja hacia atrás. Un
falsedonde esperabas texto, unnulldonde esperabas una clave, un número donde esperabas una cadena de versión: cada uno apunta directo a un escalar sin comillas que fue coercionado. El arreglo casi siempre es el mismo hábito — pon entre comillas todo lo que una persona lee como texto: códigos de país, versiones, IDs con ceros a la izquierda, horas, y todo lo de la familiayes/no/on/off.
region: "NO"
version: "1.10"
zip: "01234"
Las comillas no cuestan casi nada y apagan el adivinar sobre ese valor.
Hay una manera más rápida de ver los silenciosos antes de que salgan a producción. Pega el archivo en el conversor de YAML y lee el panel de comprobaciones: analiza como YAML 1.2 por defecto, pero señala específicamente varias de las trampas de arriba — la trampa booleana de YAML 1.1 (ese no sin comillas), los números de versión a punto de colapsar en flotantes, los enteros grandes más allá de 2^53 y las tabulaciones usadas para indentar — con los valores que van a cambiar. Convertir el mismo archivo a JSON atrapa el resto: los tipos que ves en la salida JSON son los que tu programa recibirá de verdad, así que un null implícito o un timeout:30 vuelto cadena aparecen tal cual son.
La lista de comprobación
La próxima vez que YAML «se rompa», empieza preguntando de qué tipo es:
- ¿Hay un error de análisis? Si lo hay, es ruidoso y seguro — ve a la línea y columna que nombra. Tabulaciones a espacios, arregla la indentación, cierra la comilla o el corchete, quita las claves duplicadas.
- ¿Analizó pero se comporta mal? Es silencioso. No hay línea que seguir — inspecciona el valor que recibió tu programa y remóntalo hasta el escalar que fue coercionado.
- Pon entre comillas los escalares ambiguos. Códigos de país, versiones, IDs con ceros a la izquierda, horas, y la familia
yes/no/on/off. Las comillas apagan el adivinar de YAML sobre ese valor — el hábito más eficaz contra los fallos silenciosos. - Pásalo por el panel de comprobaciones antes de que salga. El conversor de YAML saca a la luz las coerciones silenciosas por adelantado, y un vistazo a la conversión a JSON te muestra los tipos reales.
La línea para quedarse: los errores de YAML que detienen tu build son los que tienes suerte de recibir. Te cuestan minutos y se señalan a sí mismos. Es el YAML que analiza sin decir palabra — y le entrega en silencio a tu programa un false, un null o un número redondeado — el que te cuesta una tarde. Aprende a sospechar del éxito callado, no solo del fallo ruidoso, y la mayor parte del embrujo de YAML se desvanece.