DevKitLab Logo DevKitLab
JSON / YAML / TOML / Configuración

JSON vs YAML vs TOML: qué formato de configuración elegir y por qué

Un pull request añade un archivo de configuración pequeño y la revisión descarrila en cuarenta comentarios —¿YAML? ¿TOML? ¿mejor JSON?— y no cambia nada. El hilo se atasca en la pregunta equivocada: ¿cuál es el mejor formato? No existe un formato universalmente mejor. La mayor virtud de cada uno viene con un defecto a juego, y en cuanto lo ves, la elección se reduce a dos preguntas mucho más simples.

Un pull request añade un solo archivo pequeño —la configuración de un servicio nuevo— y la revisión descarrila. «¿Por qué YAML? Nadie recuerda las reglas de indentación.» «TOML es más limpio, usa ese.» «¿Hace falta otro formato más? Si ya tenemos JSON por todas partes.» Cuarenta comentarios después, el archivo no ha cambiado y nadie ha quedado convencido. El hilo se atascó en la pregunta en la que siempre se atasca: ¿cuál es el mejor formato?

Y aquí está el punto: no existe un formato universalmente mejor, y preguntar por él es justo lo que dejó el hilo sin avanzar. JSON, YAML y TOML no son tres candidatos que compiten por un mismo título: están afinados para trabajos distintos, y la mayor virtud de cada uno viene con un defecto a juego. Que JSON no admita comentarios es el mismo rasgo que lo hace un formato de transporte limpio. Que YAML adivine lo que quisiste decir es el mismo rasgo detrás de sus tristemente célebres trampas. Que TOML te obligue a escribirlo todo explícito es el mismo rasgo que lo vuelve verboso en cuanto los datos se hacen profundos. En cuanto ves que ninguno sale gratis, «¿cuál es el mejor?» se deshace en dos preguntas que sí tienen respuesta —y el formato sale de ellas:

  1. ¿Es un archivo de datos que intercambian las máquinas, o una configuración que mantiene una persona?
  2. Si la mantiene una persona, ¿cuánto quieres que el formato adivine por ti?

Primero, lo mismo escrito de tres maneras

Antes de las preguntas, mira una misma configuración pequeña en los tres. Los mismos datos: un nombre de servicio, un puerto, un flag, una lista:

{
  "name": "billing-api",
  "port": 8080,
  "debug": false,
  "allowedHosts": ["localhost", "127.0.0.1"]
}
name = "billing-api"
port = 8080
debug = false
allowedHosts = ["localhost", "127.0.0.1"]
name: billing-api
port: 8080
debug: false
allowedHosts:
  - localhost
  - 127.0.0.1

Puestos uno junto a otro, las diferencias parecen cosméticas: llaves frente a clave = valor frente a indentación. No son cosméticas. Cada disposición apuesta algo distinto sobre quién teclea en este archivo y quién lo lee de vuelta. Para hacer esa apuesta necesitas las dos preguntas.

Pregunta 1: ¿datos para máquinas o configuración para personas? El comentario es la pista

Una configuración también la lee una máquina, claro —no es ahí donde está la línea. La línea es quién lo escribe y mantiene: un archivo de datos lo producen y lo consumen programas que mueven información entre sistemas, mientras que una configuración la escribe y la cuida una persona para gobernar un programa. La forma más rápida de saber a qué lado pertenece un archivo es preguntar una sola cosa: ¿se le permiten comentarios?

JSON no los tiene. Eso no es un descuido: los comentarios se dejaron fuera a propósito, para que JSON pudiera ser un formato de intercambio de datos puro, con una sintaxis y un modelo de datos pequeños y muy interoperables, y sin nada dentro que un lector pudiera confundir con una instrucción. Una carga útil que vuela entre dos servidores no necesita un # TODO. Ese rigor es justo el punto: sin comentarios, sin comas finales, cada clave entre comillas, una sola manera de escribir cada cosa. Esa rigidez es precisamente lo que quieres cuando la máquina del otro lado tiene que analizarlo igual cada vez, y es también por lo que una coma o un comentario de más hacen que JSON se niegue a analizar.

Por eso JSON es un excelente formato de datos y un formato de configuración incómodo. En el momento en que estás editando a mano un archivo JSON y alargas la mano hacia un comentario para explicar por qué retries es 3, has descubierto que el archivo no son datos en tránsito: es configuración disfrazada de formato de datos. El ecosistema lo ha admitido en silencio por todos lados: tsconfig.json no es JSON estricto, es JSONC (JSON con comentarios); una larga lista de herramientas acepta JSON5. Cada una de esas variantes existe para devolver lo que JSON quitó a propósito, porque esos archivos eran configuración desde el principio.

Así que la pregunta 1 separa con nitidez:

  • Son datos que se mueven entre programas —una respuesta de API, un mensaje en una cola, una entrada de caché, datos que guardas o envías— → JSON. Aquí, que no tenga comentarios y que sea quisquilloso son virtudes. Echar mano de YAML o TOML sería cambiar el rigor que quieres por una flexibilidad que ninguna máquina pidió.
  • Es configuración que mantiene una persona —la configuración de un servicio, una tubería de CI, los ajustes de una herramienta— → es un archivo de configuración. Pasa a la pregunta 2.

Una salvedad honesta: mucha configuración sí vive en JSON —package.json, .eslintrc.json— y eso está bien cuando el archivo lo gestiona sobre todo la máquina y las personas apenas lo tocan. La fricción crece en proporción a cuánto lo edita a mano una persona. package.json lo escribe casi siempre tu gestor de paquetes; tsconfig.json lo escribes tú, y por eso justamente la herramienta tuvo que devolverle los comentarios.

Pregunta 2: ¿cuánto debe adivinar el formato? YAML vs TOML

Una vez que es un archivo de configuración, la verdadera disyuntiva está entre YAML y TOML, y se reduce a un rasgo: cuánto adivina el formato por ti.

YAML es el que más adivina. Escribes port: 8080 y decide que es un número; debug: false y es un booleano; host: localhost y es una cadena. Rara vez tecleas una comilla o un corchete. Esa inferencia es lo que hace a YAML tan ligero de escribir y, como muestra la sección siguiente, es la fuente de sus trampas más famosas.

TOML es el que menos adivina de los formatos de configuración, a propósito. Su postura es explícito, pero cómodo: las cadenas van entre comillas como en JSON, los números, los booleanos y las fechas tienen formas inequívocas y —esto es lo clave— conserva lo que JSON rechazó: los comentarios y la agrupación amable en [section]. Una palabra suelta que no puede clasificar no se convierte silenciosamente en otro tipo: es un error de análisis.

Así que, dentro de los formatos de configuración, la línea divisoria es la forma y la profundidad:

  • Configuración mayormente plana —una lista de ajustes, unas cuantas secciones agrupadas como [database], [server], [logging]— → TOML. Se lee como un archivo INI que creció: evidente, fácil de recorrer con grep, difícil de malinterpretar. Por eso el Cargo.toml de Rust, el pyproject.toml de Python e incontables CLI se decantaron por él.
  • Configuración profundamente anidada —árboles de mapas, listas de objetos, valores que quieres reutilizar (manifiestos de Kubernetes, playbooks de Ansible, flujos de GitHub Actions, Docker Compose)— → YAML. La indentación despliega un árbol profundo de forma mucho más legible que los encabezados repetidos de TOML, y YAML tiene maquinaria de verdad —anclas y alias— para reutilizar.

La trampa que le dio a YAML su fama: la coerción de tipos

Aquí es donde el «adivinar» de YAML pasa de comodidad a incidente. El caso célebre es el problema de Noruega:

countries:
  - NO   # ¿Noruega... o el booleano false?

En YAML 1.1 —la versión que muchos analizadores de uso común, como PyYAML, todavía aplican por defecto— NO no es la cadena "NO". Es el booleano false. La misma coerción atrapa a yes, no, on, off, y y n: todos se leen como booleanos, no como texto. Una configuración que lista códigos de país deja caer Noruega sin ruido y le entrega a tu programa un false. (El core schema de YAML 1.2 eliminó esta regla —ahí NO sigue siendo una cadena—, pero muchos runtimes aún usan por defecto el comportamiento de 1.1, así que no des por hecho que estás a salvo.)

No se queda en los booleanos, ni es solo cosa de la 1.1. Algunas trampas también se disparan bajo el core schema de YAML 1.2, porque el valor realmente tiene forma de número:

version: 1.10    # en 1.1 y en 1.2 core: el número 1.1 — el cero final desaparece
build:   1e5     # en 1.1 y en 1.2 core: 100000 — leído como notación científica
time:    22:22   # solo en YAML 1.1: 1342 (base 60: 22×60 + 22); una cadena en 1.2 core

Normalmente ninguna de estas da error. Qué valor se convierte, y a qué tipo, depende del analizador y del esquema que aplique —22:22 es sexagesimal solo bajo YAML 1.1, mientras que 1.10 y 1e5 también son números bajo el core schema de 1.2. Cuando la conversión implícita ocurre, el tipo equivocado entra en silencio, y te enteras en producción, cuando una comprobación de versión coteja el número 1.1 con la cadena "1.10" y dictamina que no coinciden.

Basta con adoptar un hábito: poner entre comillas todo lo que una persona lee como texto pero un analizador podría leer como otra cosa —códigos de país, cadenas de versión, SHA de git, puertos con cero inicial, horas. version: "1.10", - "NO". YAML es seguro en cuanto tratas su «adivinar» como algo de lo que protegerte y no en lo que apoyarte. Solo tienes que saber que hay que protegerse. (La guía completa de cómo se rompe YAML —los errores ruidosos y estas coerciones silenciosas— está en otro artículo: ¿por qué se rompe mi YAML?.)

Y ahora el contraste que explica por qué TOML se siente más seguro:

country = NO       # error: TOML no adivina; escribe "NO" o se niega a analizar
port    = 0700     # error: no se permiten ceros iniciales

TOML convierte la respuesta equivocada y silenciosa de YAML en un error de análisis explícito. Una palabra suelta que no puede clasificar no se vuelve false: produce un error de análisis claro al leer la configuración. Ese es el atractivo en una frase: en TOML no puedes «sin querer» llevar el problema de Noruega a producción, porque la forma ambigua sencillamente no es válida.

Pero TOML no es magia, y aquí está el matiz honesto: lo que tiene exactamente forma de número se sigue analizando como número.

version = 1.10     # sigue volviéndose el flotante 1.1; para la cadena, escribe "1.10"

La diferencia es la previsibilidad, no la inmunidad. La regla de TOML es corta —si tiene exactamente forma de número, es un número, y todo lo demás va entre comillas o es un error—, mientras que YAML carga una lista larga y sorprendente de palabras sueltas (NO, on, 22:22) que significan algo distinto de sí mismas. Una regla corta que cabe en la cabeza gana a una larga que te da sustos.

La factura de la virtud de TOML: la profundidad

La explicitud de TOML tiene su propio coste, y esa factura vence cuando los datos se hacen profundos. Mira la misma lista de objetos anidados en ambos:

[[server]]
name = "alpha"
[server.limits]
maxConns = 100

[[server]]
name = "beta"
[server.limits]
maxConns = 200
server:
  - name: alpha
    limits:
      maxConns: 100
  - name: beta
    limits:
      maxConns: 200

Los encabezados [[server]] y [server.limits] de TOML son inequívocos, pero se repiten, y quien lee tiene que rearmar el árbol en su cabeza. En YAML, la indentación es el árbol. Por eso, con datos muy anidados, la gente termina yendo más allá de TOML hacia JSON o YAML: con estructura plana o poco profunda, TOML es un placer; varios niveles adentro, se te resiste, y YAML (con comillas defensivas) se lee mejor.

El único principio bajo todo esto

Da un paso atrás y los tres se ordenan sobre un mismo eje —cuánto dejan a la inferencia— y cada propiedad, buena o mala, se deriva de la posición que ocupa el formato en él:

Adivina…ObtienesPagas
JSONnada; todo explícito, sin comentariosun formato de transporte estricto y ampliamente interoperablepenoso de editar a mano: sin comentarios, sin comas finales
TOMLcasi nada; la ambigüedad es un errorconfiguración segura, evidente, fácil de recorrer con grepverboso en cuanto los datos se anidan hondo
YAMLlo máximo; infiere tipos de palabras sueltaslo más ligero de escribir, escala a árboles profundosel problema de Noruega y la coerción de tipos silenciosa

No hay fila sin coste. Eso es lo que se le escapó a la discusión original: no buscas el formato sin defectos, eliges con qué defecto puedes vivir para este archivo. Un formato de transporte molesto de editar a mano está bien: rara vez lo editas a mano. Un formato de configuración verboso al anidar está bien, si tu configuración es plana. Un formato de configuración que adivina tipos está bien, si pones comillas a la defensiva y quien revisa sabe dónde mirar.

Una tabla de decisión

El archivo…UsaPorque
Datos que intercambian las máquinas (API, cola, almacenamiento)JSONel rigor y la amplia interoperabilidad son el trabajo; no hacen falta comentarios
Configuración humana, secciones mayormente planasTOMLexplícito e inequívoco: no puede caer en el problema de Noruega
Configuración humana, muy anidada o con mucha reutilizaciónYAMLla indentación escala a árboles profundos; anclas para reutilizar (comillas a la defensiva)
Configuración que editas a mano pero atrapada en JSONJSONC / JSON5ya admitiste que es configuración; devuélvele los comentarios

Los conversores de aquí están hechos para sacar a la luz justo estas trampas, no solo para ir y volver con el texto. Pega YAML en el conversor de YAML y su panel de comprobaciones señala la trampa booleana de YAML 1.1 (ese no sin comillas) y los números de versión a punto de colapsar en flotantes; pega TOML en el conversor de TOML y señala las fechas-hora degradadas a cadenas ISO y los enteros que superan 2^53 y que el conversor conserva como cadenas para evitar pérdida de precisión —justo los puntos donde el formato que dejas y el que adoptas no coinciden. Ambos usan por defecto YAML 1.2 core / TOML estándar (una directiva %YAML 1.1 explícita en el documento devuelve a YAML al comportamiento de 1.1) y solo señalan los riesgos en vez de bloquear, así que lo que haga otro runtime sigue dependiendo de su analizador y su esquema; aun así, ver la diferencia por adelantado es mejor que encontrártela en producción. Y si solo necesitas limpiar o validar un bloque JSON, el formateador de JSON también lo hace.

La lista de comprobación

La próxima vez que un hilo se atasque en «qué formato», no discutas cuál es el mejor. Pregunta, en orden:

  1. ¿Datos o configuración? Datos que intercambian las máquinas → JSON, y para. Su rigor es la virtud, no un defecto que arreglar.
  2. (Persona) ¿plano o profundo? Secciones mayormente planas → TOML. Muy anidado o con mucha reutilización → YAML.
  3. ¿Pusiste comillas a los escalares ambiguos? Sobre todo en YAML: códigos de país, versiones, SHA, horas, números con cero inicial. Ponles comillas, o el analizador adivina por ti.
  4. ¿Estás peleando con el formato? Querer comentarios en JSON, o ahogarte en encabezados [[section]] en TOML, significa que el archivo se le quedó grande a su formato. Esa es la señal para cambiar, no para aguantar.

Bajo los cuatro pasos hay una línea que vale la pena guardar: no existe un formato de configuración mejor que todos; existe el que, para este archivo, puede permitirse su coste inherente. Responde «datos o configuración», luego «plano o profundo», pon comillas a lo que YAML adivinaría de otro modo, y la elección deja de ser cuestión de gustos y pasa a ser algo que puedes defender en una sola frase.