Conversor de CSV a JSON con comillas y tipos
Convierte un fichero CSV real (con comas dentro de comillas, comillas escapadas, campos vacíos y códigos con ceros delante) en un JSON bien formado y con tipos, e informa de las líneas que no se han podido convertir. Análisis de texto con estados, escape de caracteres y formatos de intercambio.
- Recorrer una cadena con estados
- StringBuilder
- Escape de caracteres
- Expresiones regulares
- Listas
- Formatos CSV y JSON
Enunciado
Una academia exporta sus alumnos de una hoja de cálculo en CSV y quiere cargarlos en una aplicación web que solo entiende JSON. Parece trivial (separar por comas), pero los datos reales lo complican: hay apellidos como «López, Juan» que llevan una coma dentro y por eso van entre comillas, y textos que contienen las propias comillas, que en CSV se escriben dobles ("").
Además, en JSON los tipos importan: 8.5 es un número, true un booleano, un campo vacío es null y el resto son textos entre comillas. Hay una trampa clásica: un código postal como 08001 parece un número, pero si se convierte se pierde el cero. En JSON un número no puede empezar por cero, así que esos valores se quedan como texto.
Algunas líneas vienen mal (les faltan campos o tienen comillas sin cerrar). No se convierten, pero el programa dice cuáles eran para poder corregirlas.
Qué tiene que hacer el programa
- La primera línea no vacía es la cabecera, con los nombres de los campos. Cada línea siguiente es un registro. Las líneas en blanco se ignoran (pero cuentan para numerar las líneas).
- Los campos se separan por comas. Un campo puede ir entre comillas dobles, y entonces puede contener comas; dentro de las comillas,
""representa una comilla. A cada campo (y a cada nombre de la cabecera) se le quitan los espacios del principio y del final. - Un registro con un número de campos distinto del de la cabecera, o con comillas sin cerrar, no se convierte: se anota su número de línea.
- Tipos: un campo vacío es
null; un campo que cumple el formato de número JSON (signo menos opcional,0o una cifra del 1 al 9 seguida de más cifras, y opcionalmente un punto y decimales) se escribe tal cual;trueyfalseson booleanos; todo lo demás es texto entre comillas, con"escrito\",\escrito\\y el tabulador escrito\t. - Salida: un array JSON con un objeto por registro, en el orden del fichero, con dos espacios de sangría para cada objeto y cuatro para cada propiedad (
"campo": valor), comas entre elementos y nada al final de la última línea. Sin registros,[]. - Después del JSON,
Registros: N · Líneas ignoradas: M (l1, l2…), sin el paréntesis si M es 0. Si la entrada no tiene ni la cabecera, soloEl fichero está vacío.
Entrada
Un fichero CSV: cabecera y una línea por registro, campos separados por comas y entre comillas cuando hace falta.
Ejemplos de ejecución
Tu programa debe escribir exactamente esta salida para estas entradas. Las pruebas del editor incluyen estos ejemplos y otros casos ocultos.
Alumnos de una academia
Entrada
id,nombre,ciudad,cp,nota,beca 1,Ana Gil,Madrid,28001,8.5,true 2,"López, Juan",Barcelona,08001,6,false 3,"Eva ""la rápida"" Ruiz",Sevilla,41001,,true 4,Luis,Valencia,46001,7.25 5,Marta Díaz,Bilbao,48001,-0.5,false
Salida por consola
[
{
"id": 1,
"nombre": "Ana Gil",
"ciudad": "Madrid",
"cp": 28001,
"nota": 8.5,
"beca": true
},
{
"id": 2,
"nombre": "López, Juan",
"ciudad": "Barcelona",
"cp": "08001",
"nota": 6,
"beca": false
},
{
"id": 3,
"nombre": "Eva \"la rápida\" Ruiz",
"ciudad": "Sevilla",
"cp": 41001,
"nota": null,
"beca": true
},
{
"id": 5,
"nombre": "Marta Díaz",
"ciudad": "Bilbao",
"cp": 48001,
"nota": -0.5,
"beca": false
}
]
Registros: 4 · Líneas ignoradas: 1 (5)Espacios, barras y comillas sin cerrar
Entrada
codigo , descripcion , precio A1 , "Cable HDMI 2m" , 7.95 A2,"Ruta C:\temp",0 A3,"Sin cerrar,1 A4,,
Salida por consola
[
{
"codigo": "A1",
"descripcion": "Cable HDMI 2m",
"precio": 7.95
},
{
"codigo": "A2",
"descripcion": "Ruta C:\\temp",
"precio": 0
},
{
"codigo": "A4",
"descripcion": null,
"precio": null
}
]
Registros: 3 · Líneas ignoradas: 1 (4)Guía paso a paso
Intenta resolverlo por tu cuenta y abre un paso solo cuando te atasques: cada uno te acerca a la solución sin dártela entera.
1. Por qué no sirve split(",")
"López, Juan",Barcelona tiene dos campos, pero split(",") daría tres. Hay que recorrer la línea carácter a carácter recordando si estás dentro de unas comillas: una coma solo separa campos fuera de ellas.
2. Una máquina de dos estados
Con un boolean entreComillas basta. Fuera de comillas: " abre comillas, , cierra el campo y cualquier otro carácter se añade. Dentro: " seguida de otra " es una comilla literal (añádela y salta la segunda); una " sola cierra las comillas.
if (entreComillas) {
if (c != '"') actual.append(c);
else if (i + 1 < linea.length() && linea.charAt(i + 1) == '"') { actual.append('"'); i++; }
else entreComillas = false;
}3. Decidir el tipo
Comprueba los casos en orden: vacío → null; número JSON → tal cual (la expresión regular -?(0|[1-9]\\d*)(\\.\\d+)? descarta 08001, 1e5, +3, .5 y 3.); true o false → booleano; si no, texto.
4. Escapar los textos
Un texto con una comilla dentro rompería el JSON. Recorre sus caracteres y sustituye " por \\", la barra \\ por \\\\ y el tabulador por \\t. Úsalo también con los nombres de la cabecera.
5. Montar la salida
Construye cada objeto como texto en un StringBuilder y guárdalo en una lista; al final únelos con String.join(",\n", objetos). Así la coma entre objetos sale sola, sin tener que saber cuál es el último mientras lees.
Resuélvelo aquí
El editor trae el esqueleto del programa. Pulsa «Ejecutar» para comprobarlo con los ejemplos y con 3 casos ocultos que buscan los errores típicos.
Ejemplo
id,nombre,ciudad,cp,nota,beca 1,Ana Gil,Madrid,28001,8.5,true 2,"López, Juan",Barcelona,08001,6,false 3,"Eva ""la rápida"" Ruiz",Sevilla,41001,,true 4,Luis,Valencia,46001,7.25 5,Marta Díaz,Bilbao,48001,-0.5,false
[
{
"id": 1,
"nombre": "Ana Gil",
"ciudad": "Madrid",
"cp": 28001,
"nota": 8.5,
"beca": true
},
{
"id": 2,
"nombre": "López, Juan",
"ciudad": "Barcelona",
"cp": "08001",
"nota": 6,
"beca": false
},
{
"id": 3,
"nombre": "Eva \"la rápida\" Ruiz",
"ciudad": "Sevilla",
"cp": 41001,
"nota": null,
"beca": true
},
{
"id": 5,
"nombre": "Marta Díaz",
"ciudad": "Bilbao",
"cp": 48001,
"nota": -0.5,
"beca": false
}
]
Registros: 4 · Líneas ignoradas: 1 (5)Solución explicada
Ver la solución completa
1import java.util.ArrayList;
2import java.util.List;
3import java.util.Scanner;
4
5public class Main {
6 /** Campos de una línea CSV separados por comas, con comillas dobles opcionales ("" es una comilla). null si hay comillas sin cerrar. */
7 static List<String> campos(String linea) {
8 List<String> lista = new ArrayList<>();
9 StringBuilder actual = new StringBuilder();
10 boolean entreComillas = false;
11 for (int i = 0; i < linea.length(); i++) {
12 char c = linea.charAt(i);
13 if (entreComillas) {
14 if (c != '"') {
15 actual.append(c);
16 } else if (i + 1 < linea.length() && linea.charAt(i + 1) == '"') {
17 actual.append('"');
18 i++;
19 } else {
20 entreComillas = false;
21 }
22 } else if (c == '"') {
23 entreComillas = true;
24 } else if (c == ',') {
25 lista.add(actual.toString().trim());
26 actual.setLength(0);
27 } else {
28 actual.append(c);
29 }
30 }
31 if (entreComillas) return null;
32 lista.add(actual.toString().trim());
33 return lista;
34 }
35
36 /** Texto JSON entre comillas, con las comillas, barras y tabuladores escapados. */
37 static String cadena(String s) {
38 StringBuilder sb = new StringBuilder("\"");
39 for (char c : s.toCharArray()) {
40 switch (c) {
41 case '"' -> sb.append("\\\"");
42 case '\\' -> sb.append("\\\\");
43 case '\t' -> sb.append("\\t");
44 default -> sb.append(c);
45 }
46 }
47 return sb.append('"').toString();
48 }
49
50 /** El valor JSON de un campo: null, número, booleano o texto. */
51 static String valor(String campo) {
52 if (campo.isEmpty()) return "null";
53 // Un número JSON no admite ceros a la izquierda: «08001» es un código postal, no el 8001
54 if (campo.matches("-?(0|[1-9]\\d*)(\\.\\d+)?")) return campo;
55 if (campo.equals("true") || campo.equals("false")) return campo;
56 return cadena(campo);
57 }
58
59 public static void main(String[] args) {
60 Scanner sc = new Scanner(System.in);
61 List<String> cabecera = null;
62 List<String> objetos = new ArrayList<>();
63 List<Integer> ignoradas = new ArrayList<>();
64 int numero = 0;
65 while (sc.hasNextLine()) {
66 String linea = sc.nextLine();
67 numero++;
68 if (linea.isBlank()) continue;
69 List<String> c = campos(linea);
70 if (cabecera == null) {
71 cabecera = c;
72 continue;
73 }
74 if (c == null || c.size() != cabecera.size()) {
75 ignoradas.add(numero);
76 continue;
77 }
78 StringBuilder obj = new StringBuilder(" {\n");
79 for (int i = 0; i < c.size(); i++) {
80 obj.append(" ").append(cadena(cabecera.get(i))).append(": ").append(valor(c.get(i)));
81 obj.append(i < c.size() - 1 ? ",\n" : "\n");
82 }
83 objetos.add(obj.append(" }").toString());
84 }
85
86 if (cabecera == null) {
87 System.out.println("El fichero está vacío");
88 return;
89 }
90 System.out.println(objetos.isEmpty() ? "[]" : "[\n" + String.join(",\n", objetos) + "\n]");
91 String lista = ignoradas.isEmpty() ? "" : " (" + String.join(", ", ignoradas.stream().map(String::valueOf).toList()) + ")";
92 System.out.println("Registros: " + objetos.size() + " · Líneas ignoradas: " + ignoradas.size() + lista);
93 }
94}El analizador de líneas es una máquina de estados con dos estados (dentro o fuera de comillas). Es la técnica general para leer formatos con delimitadores y escapes, y explica por qué en producción se usan librerías (OpenCSV, Apache Commons CSV) en lugar de split.
Separar el análisis (campos), la decisión de tipo (valor) y el escape (cadena) en métodos pequeños permite probar cada uno por separado y reutilizar el escape para las claves.
La regla del número sigue la gramática de JSON: por eso 08001 y 007 siguen siendo texto. Es un error real y frecuente al importar códigos postales, teléfonos o DNI desde hojas de cálculo, que se comen los ceros de la izquierda.
La salida se monta en memoria y se escribe al final: así el JSON es válido aunque haya líneas ignoradas por el medio, y el resumen va después sin mezclarse con él.
Para ir más allá
- Admite el punto y coma como separador (el que usa Excel en español) detectándolo en la cabecera.
- Admite campos entre comillas que contengan saltos de línea (un registro que ocupa varias líneas).
- Haz la conversión inversa, de JSON a CSV, poniendo comillas solo a los campos que lo necesitan.