Convertir JSON a y desde CSV y datos tabulares
En resumen: CSV es bidimensional, JSON tiene forma de árbol. La parte difícil de la conversión no es el código, sino decidir de antemano cómo se aplanan los arrays y cómo se preservan los tipos.

En resumen: aplana con claves punteadas y arrays serializados como JSON cuando las personas vayan a leer la exportación, divide en una tabla principal más tablas relacionadas cuando cargues una base de datos, y expande los arrays en filas adicionales solo para pivotes; al restaurar, convierte mediante definiciones explícitas de columna en vez de inferencia global.
Aplanar un árbol en una tabla siempre pierde jerarquía; restaurar una tabla en un árbol siempre adivina tipos. Ambas direcciones necesitan sus reglas declaradas de antemano.
1. Qué especifica realmente CSV
RFC 4180 ocupa solo unas pocas páginas y especifica menos de lo que mucha gente supone:
- Cada registro ocupa una línea, con los campos separados por comas;
- El terminador de línea es CRLF, aunque en la práctica también se acepta
\n; - Un campo que contenga una coma, una comilla doble o un salto de línea debe ir entre comillas dobles;
- Las comillas dobles dentro de un campo se escriben como dos comillas dobles
""; - La primera línea puede ser una cabecera;
- No se especifica ninguna codificación de caracteres, ni tampoco los tipos.
id,name,remark
1,张三,"喜欢 JSON,讨厌尾逗号"
2,李四,"他说:""这行有引号"""
3,王五,"多行
备注"
Fíjate en la regla 3: los campos que contienen saltos de línea son legales, así que un analizador ingenuo que divida por \n y luego por , fallará con toda seguridad.
2. JSON → CSV: decide primero la estructura
Solo hay tres estrategias para aplanar una estructura de árbol en una tabla bidimensional:
Estrategia 1: aplanar campos (la más común)
{ "id": 1, "user": { "name": "张三", "city": "杭州" }, "tags": ["a", "b"] }
id,user.name,user.city,tags
1,张三,杭州,"[""a"",""b""]"
- Une las claves de objetos anidados con
.; - Los arrays no pueden expresarse como columnas, así que pásales
JSON.stringifydirectamente dentro de la celda (que sigue siendo un campo CSV legal); - Inconveniente: las herramientas de hoja de cálculo no pueden filtrar el array, así que hace falta un segundo análisis.
Estrategia 2: expandir el array en varias filas
id,user.name,tag
1,张三,a
1,张三,b
Esto encaja en relaciones uno a muchos en las que los consumidores aguas abajo agregan por elemento. El inconveniente son los campos del padre duplicados y un número de filas que se dispara.
Estrategia 3: dividir en varias tablas
Una tabla principal más tablas relacionadas unidas por un id: el enfoque más limpio, pero va más allá de "una única exportación" y se parece más al modelado de datos.
¿Cuál? Usa la estrategia 1 para exportaciones destinadas a personas, la estrategia 3 para cargar en una base de datos y la estrategia 2 para hacer pivotes de datos.
3. CSV → JSON: cómo se adivinan los tipos
CSV no tiene tipos, así que cada paso de restauración es una suposición:
| Contenido de la celda | Significado posible | Riesgo de convertir directamente a número |
|---|---|---|
00123 |
ID de empleado, código postal | Se pierden los ceros iniciales → 123 |
1.10 |
Número de versión, importe | Se convierte en 1.1 |
1e5 |
Identificador o notación científica | Se convierte en 100000 |
2026-10-30 |
Fecha o cadena | Ambigüedad en zona horaria y formato |
13800138000 |
Número de teléfono | Sigue siendo seguro con 11 dígitos, pero algunos ID sí pierden precisión |
"" |
Cadena vacía o null | Ambigüedad semántica |
Valor por defecto sugerido: trata todo como cadena, y deja que un schema o una definición explícita de columna decida qué columnas se convierten en números o booleanos. La "inferencia inteligente" es encantadora al exportar y una fuente de incidentes al importar.
type ColumnType = 'string' | 'number' | 'boolean' | 'json';
/**
* Convierte las filas CSV en objetos según las definiciones de columna.
* @param rows El array de filas en bruto; la primera fila es la cabecera
* @param columnTypes Mapeo de nombre de columna a tipo
* @returns El array de objetos convertido
*/
function rowsToRecords(
rows: string[][],
columnTypes: Record<string, ColumnType>,
): Array<Record<string, unknown>> {
const [headerRow, ...dataRows] = rows;
return dataRows.map((dataRow) => {
const record: Record<string, unknown> = {};
headerRow.forEach((columnName, columnIndex) => {
const rawValue = dataRow[columnIndex] ?? '';
const columnType = columnTypes[columnName] ?? 'string';
if (columnType === 'number') {
const numericValue = Number(rawValue);
record[columnName] = Number.isFinite(numericValue) ? numericValue : null;
return;
}
if (columnType === 'boolean') {
record[columnName] = rawValue === 'true' || rawValue === '1';
return;
}
if (columnType === 'json') {
try {
record[columnName] = JSON.parse(rawValue);
} catch {
record[columnName] = rawValue;
}
return;
}
record[columnName] = rawValue;
});
return record;
});
}
4. Tres trampas en torno a Excel
4.1 Codificación
Cuando Excel abre un CSV UTF-8 sin BOM, el texto chino se convierte en mojibake. Añadir un BOM (\uFEFF) al exportar es la corrección de compatibilidad más barata:
const bom = '\uFEFF';
const csvContent = `${bom}${headerLine}\n${bodyLines.join('\n')}`;
4.2 Números largos que se convierten en notación científica
Un documento nacional de identidad de 18 dígitos o un número de pedido de 19 dígitos aparece en Excel como 1.23457E+17, y el valor en sí ya se ha truncado.
Anteponer un tabulador al campo o envolverlo como fórmula (="1234...") preserva el formato de texto, pero convierte la celda en una fórmula.
El enfoque más seguro es indicar a los usuarios que "usen la función de importación y especifiquen la columna como texto", o ofrecer un .xlsx junto con la exportación.
4.3 Delimitadores afectados por la configuración regional
Algunas configuraciones regionales europeas usan por defecto el punto y coma ; como separador. Al distribuir entre regiones, o fija CSV a las comas y dilo, o ofrece una versión TSV (separada por tabuladores): TSV tolera mejor las configuraciones regionales.
5. Tratamiento de archivos grandes
No cargues de una vez en memoria CSVs con más de cien mil filas:
import { createReadStream } from 'node:fs';
import { parse } from 'csv-parse';
const parser = createReadStream('orders.csv').pipe(
parse({ columns: true, skipEmptyLines: true, bom: true }),
);
for await (const record of parser) {
// Procesa un registro a la vez; el uso de memoria es independiente del tamaño del archivo
}
La dirección de exportación funciona igual: genera y escribe en un flujo sobre la marcha, en lugar de concatenar una única cadena gigante.
6. Autocomprobación de consistencia de ida y vuelta
Un recorrido completo de CSV (JSON → CSV → JSON) casi nunca es exactamente idéntico, pero puedes definir qué pérdidas son aceptables:
| Comprobación | Expectativa |
|---|---|
| Número de campos | Idéntico |
| Valores de cadena | Idénticos (incluidos los ceros iniciales) |
| Tipos numéricos | Determinados por la definición de columna, no por inferencia |
| Objetos anidados | Recuperables mediante rutas punteadas |
| Arrays | Visibles tras la serialización a una cadena JSON |
| Valores vacíos | Normalizados a null o a la cadena vacía: elige exactamente uno |
Poner esta tabla en los tests es mucho más eficaz que un acuerdo verbal.
7. Lista de comprobación
- [ ] La semántica de "una fila es un registro" está declarada explícitamente y se refleja en el nombre del archivo exportado;
- [ ] Toda columna que pueda perder precisión se exporta como cadena;
- [ ] Los CSV en chino incluyen un BOM, o la codificación se declara explícitamente;
- [ ] Los campos que contienen comas, comillas o saltos de línea se escapan correctamente (usa una biblioteca, no concatenes cadenas tú mismo);
- [ ] Los archivos grandes pasan por lecturas y escrituras en streaming;
- [ ] Las importaciones convierten los tipos según la definición de columna, nunca mediante inferencia inteligente global.
Preguntas frecuentes
¿Cómo elijo entre aplanar, expandir filas y dividir tablas?
Aplana para exportaciones destinadas a personas: une las claves anidadas con . y serializa los arrays con JSON.stringify dentro de la celda, a costa de no poder filtrar esos arrays en una hoja de cálculo. Divide en una tabla principal más tablas relacionadas cuando el destino sea una base de datos. Expande los arrays en filas adicionales cuando los consumidores aguas abajo agregan por elemento, aceptando campos del padre duplicados.
¿Por qué una importación CSV no debería inferir tipos automáticamente?
RFC 4180 especifica delimitadores y escapado pero no tipos, así que cada paso de restauración es una suposición: 00123 pierde sus ceros iniciales, 1.10 se convierte en 1.1 y 1e5 se convierte en 100000. Trata todo como cadena por defecto y deja que un schema o una definición explícita de columna decida qué columnas se convierten en números o booleanos.
¿Debo esperar que un recorrido de ida y vuelta de CSV reproduzca exactamente el JSON original?
Casi nunca. Define en su lugar las pérdidas aceptables: número de campos idéntico, valores de cadena idénticos incluidos los ceros iniciales, tipos numéricos decididos por la definición de columna, objetos anidados recuperables mediante rutas punteadas, arrays visibles tras la serialización y valores vacíos normalizados a null o a la cadena vacía: elige exactamente uno.