JSON Toolkit

JSON a CSV: de dónde sale el encabezado y por qué cambian los valores

En resumen: Nueve experimentos en un conversor real: la estrategia "claves de la primera fila" pierde campos en silencio, null, ausencia y cadena vacía se convierten todos en celdas vacías, un entero de 20 dígitos pierde precisión ya en JSON.parse, y un punto en una clave hace que dos rutas diferentes colisionen en la misma columna. Cómo se construye el encabezado, qué valores se deforman y cómo elegir una estrategia según el uso posterior.

Avanzado~6 min de lecturaPublicado el 2026-11-04Actualizado el 2026-11-04
Estrategias de encabezado y fidelidad de valores en la conversión de JSON a CSV

Cuando una conversión de JSON a CSV pierde datos, casi nunca sucede durante el paso de "conversión". La pérdida ocurre en dos preguntas que nadie hizo: quién decidió el encabezado, y ¿cada celda sigue siendo reconocible como el valor que era?

Un campo desaparece en treinta segundos

Muchos conversores — incluida la pipeline idiomática de jq — toman el encabezado solo del primer registro. Reproduce las consecuencias en Python:

data = [{"id": 1, "name": "A"}, {"id": 2, "email": "b@x.com"}]
first_keys = list(data[0].keys())          # ['id', 'name']
rows = [[r.get(k, "") for k in first_keys] for r in data]
# El email del segundo registro desaparece sin error ni aviso

La clave email no está en el encabezado: ni siquiera tiene derecho a convertirse en una celda vacía — la columna nunca se creó, y el valor se descarta en silencio. La corrección es la unión de claves: recorrer todos los registros y crear una columna para cada clave que aparezca en cualquier parte. Los mismos datos pasados por la unión producen el encabezado id,name,email, con una celda vacía donde falta name — y no se pierde nada.

El conversor directo de nuestra página /convert/csv aplica exactamente esta estrategia. Con la entrada [{"id":1,"name":"A"},{"id":2,"email":"b@x.com"}], produce:

id,name,email
1,A,
2,,b@x.com

Las claves se ordenan por primera aparición y los campos ausentes se vuelven celdas vacías. jsonoperations y jsonbeam llaman ambas a esto "la corrección del error clásico" — pero "usa la unión" es solo media historia. La unión tiene su propio precio; esa es la siguiente sección.

Tres formas de construir un encabezado — y a quién conviene cada una

Estrategia de encabezado Cómo se construye Fuerza Costo
Claves de la primera fila Solo se lee el primer registro Pocas columnas, estable, una línea de jq Los campos nuevos de registros posteriores se pierden en silencio
Unión de claves Se leen todos los registros Ningún campo se pierde Columnas dispersas y muchos vacíos cuando las formas varían
Lista explícita de columnas Definida a mano o por esquema Totalmente controlable, reproducible Requiere mantenimiento; los campos nuevos exigen actualización manual

El criterio de elección no es "cuál es más sofisticado" sino qué hace el destinatario con este CSV:

  • Un colega que le echa un vistazo: las claves de la primera fila bastan, las formas de registro suelen ser uniformes.
  • Un informe o una pipeline de datos: la unión de claves es el suelo — una celda vacía es, en sí misma, información.
  • Un export automático recurrente: la lista explícita de columnas. La deriva de forma (campo añadido aguas arriba, clave renombrada) debe fallar con estruendo, no producir en silencio una tabla que nadie nota que está mal.

La tercera opción es la más descuidada. En automatización, "la conversión tuvo éxito" y "la conversión es correcta" son dos afirmaciones distintas — y la lista explícita de columnas es el único mecanismo que las separa.

Cuando una clave contiene un punto, las rutas de aplanado colisionan

El aplanado en notación de punto convierte {"a": {"b": 2}} en la columna a.b. Pero ¿y si la clave misma contiene un punto? Probado con pandas:

import pandas as pd
d = [{"a.b": 1, "a": {"b": 2}}]
pd.json_normalize(d, sep=".")
# Solo sobrevive una columna: ['a.b'] — dos claves completamente distintas chocan en la misma ruta

La clave literal a.b y la ruta anidada a → b producen el mismo nombre de columna, una sobrescribe a la otra, y los datos se corrompen en silencio. Es un ejemplo construido, pero ocurre con datos reales: los nombres de métricas de monitoreo (cpu.usage) y los campos de logs (error.type) adoran los puntos.

Dos remedios:

  1. Cambiar el separador. sep="_" o un separador personalizado, después de verificar que las claves originales nunca lo contengan.
  2. Cambiar la notación de rutas. JSON Pointer usa / como separador (/a/b); el conversor Excel de toolvore sigue ese camino. Tiene la misma clase de ambigüedad — una clave con / debe escribirse como ~1 — pero las reglas de escape están normalizadas en la RFC 6901 en vez de inventadas por cada herramienta.

Sea cual sea tu elección, escribe la regla en la documentación del export. El encabezado CSV es el único "esquema" que llevan estos datos; el lector no encuentra tus reglas de aplanado en ningún otro sitio.

Los arrays: tres estrategias, cada una pierde algo distinto

Los arrays son el verdadero obstáculo del JSON a CSV: una celda no puede contener varios valores. Tres estrategias, con pérdidas verificadas en la práctica:

Estrategia Forma de salida Qué se pierde Ideal para
JSON-en-celda El array serializado como cadena JSON en la celda Las herramientas de tabla no pueden filtrar ni agregar los elementos Fidelidad primero, inspección manual
Columnas indexadas tags.0, tags.1, una columna cada una El número de columnas explota con el array más largo; muchos vacíos Arrays pequeños de longitud fija
Filas explotadas Una fila por elemento, campos padre duplicados Inflación de filas; el comportamiento de arrays vacíos debe definirse aparte Análisis de detalle, agregación por elemento

Nuestros conversores /convert/csv y /convert/table usan JSON-en-celda. La entrada [{"orderId":"A001","items":[{"sku":"X1","qty":2},{"sku":"Y9","qty":1}]}] produce:

orderId,items
A001,"[{""sku"":""X1"",""qty"":2},{""sku"":""Y9"",""qty"":1}]"

Cuando necesitas filas explotadas, el record_path de pandas lo hace directamente:

orders = [{"orderId": "A001", "total": 99,
           "items": [{"sku": "X1", "qty": 2}, {"sku": "Y9", "qty": 1}]}]
items = pd.json_normalize(orders, record_path=["items"],
                          meta=["orderId", "total"], sep=".")
# Dos filas de detalle; orderId y total duplicados en cada una

Al explotar hay tres trampas que decidir antes: explotar varios arrays a la vez construye un producto cartesiano; los arrays vacíos no producen ninguna fila por defecto (tu lista de clientes pierde personas); y los campos padre duplicados exigen deduplicación cuando agregas por orderId aguas abajo. La guía de convertcsvonline lista "decidir primero la granularidad" como primer paso exactamente por eso — fijada la granularidad, las tres preguntas tienen cada una una única respuesta.

Probado: valores que salen cambiados de la conversión

Una prueba de fidelidad ejecutada en nuestro conversor real (Node v22.13.1 + papaparse, la misma implementación de /convert/csv):

1. null, ausencia y cadena vacía se convierten todos en celdas vacías. Entrada [{"a":null},{"b":""},{"c":1}] — las tres celdas salen vacías. El CSV no tiene tres estados, solo "hay caracteres" y "no hay caracteres". Tras un round-trip, los tres se leen como ""; la semántica original es irrecuperable.

2. Los objetos y arrays anidados se vuelven cadenas JSON. Las comillas se duplican según la RFC 4180 ({""name"":""张三""}), y al pegar la celda en cualquier parser JSON el valor vuelve — por eso JSON-en-celda preserva la fidelidad.

3. Un entero de 20 dígitos pierde precisión ya en el parse — y la culpa no es del CSV. Entrada "id": 9007199254740993; en el CSV ya está 9007199254740992. JSON.parse en JavaScript usa doubles IEEE 754: la pérdida ocurre durante el parsing, antes de cualquier conversor CSV. El json.load de Python la preserva íntegramente. Los campos tipo identificador (IDs snowflake, números de pedido) deberían llegar como cadenas desde el productor. Más sobre precisión numérica en por qué los números grandes pierden precisión.

4. Los ceros a la izquierda en cadenas están a salvo en la capa de texto del CSV. "phone": "00123" sigue siendo 00123 en el CSV, porque es una cadena de principio a fin y nada la roza. Lo que roza los ceros es el paso posterior en que Excel trata la celda como número — ve cuatro formas de convertir JSON en Excel.

5. Campos con comas, comillas o saltos de línea. papaparse sigue la RFC 4180: citar el campo, duplicar las comillas internas. "He said \"hi\", ok" sale como "He said ""hi"", ok" y sobrevive intacto al round-trip. Los serializadores escritos a mano que evitan una biblioteca fallan casi siempre exactamente ahí.

Round-trip: lo que cambia el interruptor de inferencia

CSV → JSON debe adivinar un tipo para cada celda. Nuestro conversor desactiva la inferencia de tipos por defecto y devuelve todo como cadena:

[{ "id": "9007199254740992", "phone": "00123" }]

Con typeInference activado, id se vuelve número (la precisión ya se perdió), mientras que phone sigue siendo cadena — las reglas de inferencia excluyen las formas con cero a la izquierda como 010, que no son literales numéricos legales, así que la cadena sobrevive de pura suerte.

Grábate esta asimetría: la inferencia apagada como mucho te cuesta tipos; la inferencia encendida puede costarte datos. Las columnas que necesitan números deben convertirse con definiciones explícitas de columna, no confiadas a una inferencia global. La implementación rowsToRecords de nuestro artículo anterior JSON, CSV y datos tabulares es exactamente ese principio en código.

Elegir la estrategia según el destinatario: una tabla de decisión

Quién recibe el CSV Estrategia de encabezado Manejo de arrays Tipos
Un colega que lo abre una vez Unión de claves JSON-en-celda Sin inferencia
Excel / reporting Unión de claves JSON-en-celda, o columnas indexadas si longitud fija Columnas de ID forzadas como texto
Importación a base de datos Lista explícita de columnas Tablas maestro/detalle separadas, o explosión Definiciones explícitas de columna
BI / agregación Lista explícita de columnas Filas explotadas, granularidad = unidad mínima de análisis Definiciones explícitas de columna
Pipeline automatizada Lista explícita de columnas JSON-en-celda, explosión aguas abajo Inferencia apagada, conversión columna a columna

La ley común: cuanto más cerca del humano, más puedes permitirte relajar; cuanto más cerca de la máquina, más todo debe ser explícito.

Una checklist para copiar

  • [ ] ¿El encabezado viene de una unión completa de claves o de una lista explícita? Si no, ¿puedes probar que ningún registro perdió un campo?
  • [ ] ¿Las claves pueden contener puntos o barras? ¿Se verificó la colisión con el separador de aplanado?
  • [ ] ¿La estrategia de arrays está documentada? ¿Está decidido el comportamiento de arrays vacíos?
  • [ ] Para null, ausencia y cadena vacía: ¿qué lee el destinatario de cada uno? Elige una regla y congélala.
  • [ ] ¿Las columnas tipo identificador (IDs, números de pedido, teléfonos) viajan como cadenas de punta a punta?
  • [ ] Tras la explosión, ¿el número de filas es igual a la suma de las longitudes de los arrays? ¿Verificado por muestreo?
  • [ ] ¿Los campos de ejemplo con comas, comillas y saltos de línea sobreviven intactos a un round-trip?
  • [ ] ¿La inferencia global de tipos está apagada en el reimport, con definiciones explícitas para las columnas numéricas?

Solo cuando las ocho tienen respuesta clara, este CSV está "correcto" — y no simplemente "convertido con éxito".

FAQ

¿Por qué el conversor no aplana los objetos anidados en columnas por defecto?

El aplanado descontrola el número de columnas: el anidamiento profundo produce nombres como user.address.geo.lat, y combinado con la unión de claves, un solo registro de otra forma añade una pila de columnas. Las cadenas JSON en las celdas preservan la fidelidad y un número estable de columnas; aplana explícitamente cuando lo necesites.

¿Qué hacer si el encabezado contiene nombres de columna duplicados?

La colisión de dos claves distintas tras el aplanado (como a.b contra el anidado a→b) debe resolverse aguas arriba: cambia el separador o pasa a JSON Pointer. Para duplicados dentro del mismo objeto, los conversores suelen quedarse con el último valor; los duplicados en el encabezado dependen de la implementación — el unparse de papaparse no deduplica, y Excel fusiona o descarta duplicados en silencio al abrir. Por eso está en la checklist.

¿Los campos padre duplicados tras la explosión son un bug?

No — es diseño. Una fila explotada es una fila de detalle, y una fila de detalle debe poder reengancharse al registro padre por clave. Agrega aguas abajo por la clave padre; si el tamaño preocupa, exporta el archivo de detalle y una tabla maestra separada.

¿Cómo verifico que mi conversión no perdió ningún campo?

Haz una auditoría de claves: calcula la unión de todas las claves del JSON de origen, luego el conjunto de encabezados de columna del CSV, y haz la diferencia. Una diferencia no vacía significa campos perdidos. En pipelines por lotes, codifícalo como aserción en tus pruebas: cualquier cambio de conversor que empiece a perder campos fallará automáticamente.