1. Contrato: JSON como árbol de valores
Cuando ves por primera vez #include <nlohmann/json.hpp>, puede dar la sensación de que es «algo mágico». En realidad es solo una biblioteca que convierte texto JSON en un objeto conveniente json y viceversa. En el marco del curso damos por hecho que ya está disponible en el proyecto, así que no tratamos la instalación.
Nuestra tarea es aprender a usar las operaciones básicas parse() y dump(), y a distinguir un «JSON roto» de un «JSON correcto pero con forma equivocada».
Empecemos con un «esqueleto» mínimo para acordar el estilo:
#include <nlohmann/json.hpp>
#include <iostream>
int main() {
using nlohmann::json;
json j = json::object();
std::cout << j.dump() << '\n'; // {}
}
Fijaos en dos cosas.
Primero, json es el tipo «cualquier nodo JSON»: puede ser un objeto, un array, una cadena, un número, true/false o null.
Segundo, dump() es la manera de obtener una cadena que puedes imprimir o escribir en un archivo.
2. json::parse(): convertimos una cadena en un árbol JSON
Normalmente el JSON nos llega como texto: desde un archivo, por la red, desde configuración, en un test, del usuario (y eso siempre es un poco sospechoso). Así que el primer paso casi siempre es el mismo: tomamos un std::string, llamamos a json::parse() y obtenemos el árbol de valores.
Es importante entender: parse() no «adivina el significado», lee estrictamente la sintaxis. Una coma de más o comillas incorrectas producen un error de parseo.
La forma más cómoda de mantener texto JSON dentro del código es la raw-string R"( ... )", para no escapar las comillas.
#include <nlohmann/json.hpp>
#include <iostream>
#include <string>
int main() {
using nlohmann::json;
const std::string text = R"({"id": 7, "name": "Alice", "active": true})";
json j = json::parse(text);
std::cout << j.dump() << '\n'; // {"active":true,"id":7,"name":"Alice"}
}
Sí, el formato de salida «comprimido» es normal: dump() por defecto genera una cadena compacta. La salida bonita la veremos más adelante en esta lección.
Otro punto importante: un número JSON es solo un «number». En C++ tenemos int, long long, double… Por eso 42 y "42" son datos fundamentalmente distintos.
#include <nlohmann/json.hpp>
#include <iostream>
#include <string>
int main() {
using nlohmann::json;
json a = json::parse(R"({"age": 42})");
json b = json::parse(R"({"age": "42"})");
std::cout << a.at("age").dump() << '\n'; // 42
std::cout << b.at("age").dump() << '\n'; // "42"
}
Aquí usamos at("age") solo para mostrar la diferencia. Sobre «cómo leer campos correctamente» hablaremos luego, pero sin excesos: hoy el foco está en parse() y dump().
3. dump(): convertimos JSON en cadena y mostramos bonito
Tras parsear JSON en json j, a menudo necesitamos la operación inversa: guardar, mostrar, loguear, enviar a algún sitio. Para eso está dump().
Y tiene una supercapacidad que ahorra bastantes nervios: dump(2) (o dump(4)) imprime JSON con indentación. No cambia el significado, solo hace el texto legible para humanos.
Comparemos la salida compacta y la «bonita»:
#include <nlohmann/json.hpp>
#include <iostream>
int main() {
using nlohmann::json;
json j = {
{"id", 7},
{"name", "Alice"},
{"active", true}
};
std::cout << j.dump() << '\n'; // {"active":true,"id":7,"name":"Alice"}
std::cout << j.dump(2) << '\n'; // JSON bonito con indentación
}
Si quieres ver cómo luce dump(2), será algo así:
// {
// "active": true,
// "id": 7,
// "name": "Alice"
// }
Y aquí surge una reflexión práctica importante: cuando guardas datos en un archivo «para humanos» (configuraciones, settings, datos de ejemplo), dump(2) casi siempre es mejor que la salida compacta. Para almacenamiento «máquina a máquina» puede bastar la compacta, pero la legibilidad suele compensar.
4. Mini-práctica: objeto y array a mano
Antes de pasar a archivos, es útil «tocar» JSON como estructura en memoria. Cuando creas json programáticamente, construyes un árbol: un objeto con claves, dentro arrays y otros objetos.
Es similar a std::map<std::string, ...> y std::vector<...>, salvo que el tipo «valor» es uno solo — json.
Construyamos un documento tipo «lista de tareas», sin serializar struct por ahora (eso vendrá en la siguiente lección). Será simplemente JSON que sepamos imprimir.
#include <nlohmann/json.hpp>
#include <iostream>
int main() {
using nlohmann::json;
json doc;
doc["version"] = 1;
doc["tasks"] = json::array();
doc["tasks"].push_back({{"id", 1}, {"title", "Buy milk"}, {"done", false}});
std::cout << doc.dump(2) << '\n';
}
Aquí usamos por primera vez json::array() y push_back. Se parece mucho a trabajar con std::vector, y es intencionado: la biblioteca intenta ser amigable.
Fijaos en un sitio «peligrosamente cómodo»: doc["version"] y doc["tasks"]. El operador operator[] para objetos es muy conveniente al escribir. Pero al leer puede ser traicionero (a veces crea el campo si no existía).
Por eso en esta lección seguimos una regla simple: para escribir está bien usar [], para leer es mejor contains() + at().
5. Errores: parseo y estructura incorrecta
Cuando algo falla con JSON, es crucial entender en qué etapa.
Un error de parseo significa «el texto no es un documento JSON». Un error de acceso/tipo significa «el JSON se parseó, pero la estructura no cumple las expectativas del programa». Estos dos casos se arreglan de manera distinta: en el primero corriges el texto de entrada; en el segundo, el contrato del formato o la validación.
Capturando error de parseo: json::parse_error
Rompamos el JSON a propósito: pongamos una coma extra. Es la clásica «coma vengadora».
#include <nlohmann/json.hpp>
#include <iostream>
#include <string>
int main() {
using nlohmann::json;
const std::string bad = R"({"x": 1, })"; // coma extra
try {
json j = json::parse(bad);
std::cout << j.dump() << '\n';
} catch (const json::parse_error& e) {
std::cerr << "Parse error: " << e.what() << '\n';
}
}
Es importante que capturamos exactamente json::parse_error. Esto ayuda en programas grandes a separar «problema de formato de texto» de «problema de estructura de datos».
Si capturas solo std::exception también funcionará, pero el diagnóstico será menos específico.
Errores de estructura: clave ausente o tipo distinto
Ahora imaginemos que el JSON se parseó, pero falta un campo o tiene otro tipo. Por ejemplo, esperamos un número y hay una cadena. Esto ya no es parse_error.
#include <nlohmann/json.hpp>
#include <iostream>
#include <string>
int main() {
using nlohmann::json;
json j = json::parse(R"({"age": "42"})"); // age es cadena
try {
int age = j.at("age").get<int>(); // esperamos int
std::cout << age << '\n';
} catch (const json::type_error& e) {
std::cerr << "Type error: " << e.what() << '\n';
}
}
Y si la clave no existe, at("...") lanzará una excepción sobre la ausencia de la clave (a menudo es out_of_range dentro de nlohmann::json). Es una distinción útil: «campo ausente» no es lo mismo que «campo presente pero tipo incorrecto».
6. Leer JSON desde un archivo: ifstream + in >> j
Hasta ahora leímos JSON desde cadenas porque es más sencillo para mostrar la idea. Pero en la vida real los datos suelen estar en archivos: config.json, tasks.json, save.json — ponles el nombre que quieras.
Aquí surgen dos clases de problemas: el archivo puede no abrirse, y el JSON dentro puede estar roto. Hay que distinguir estos casos, si no acabarás arreglando el «parser» cuando el problema es la ruta del archivo.
El patrón mínimo para leer JSON desde archivo es:
#include <nlohmann/json.hpp>
#include <fstream>
#include <iostream>
int main() {
std::ifstream in("tasks.json");
if (!in) {
std::cerr << "Cannot open tasks.json\n";
return 1;
}
nlohmann::json j;
in >> j; // parsea directamente desde el stream
std::cout << j.dump(2) << '\n';
}
Es una forma muy cómoda: operator>> en nlohmann::json hace el parseo «desde el stream». Pero recuerda: si dentro del archivo hay JSON inválido, puede lanzarse una excepción de parseo. Por eso en versiones más cuidadas normalmente envuelves en try/catch.
Aquí una versión «algo más madura», pero aún corta:
#include <nlohmann/json.hpp>
#include <fstream>
#include <iostream>
int main() {
using nlohmann::json;
std::ifstream in("tasks.json");
if (!in) return 1;
try {
json j;
in >> j;
std::cout << "Loaded OK\n"; // Loaded OK
} catch (const json::parse_error& e) {
std::cerr << "Bad JSON: " << e.what() << '\n';
return 2;
}
}
Y ahora sabes exactamente: return 1 — no se abrió el archivo, return 2 — se abrió pero el contenido está corrupto.
7. Escribir JSON en un archivo: ofstream + dump(2)
La escritura es la otra mitad del ciclo «datos ↔ archivo». Ya conocemos std::ofstream y sabemos imprimir JSON como cadena con dump().
Así que guardar es sorprendentemente sencillo: abres el archivo, compruebas y escribes la cadena.
Guardemos el documento en tasks.json con indentación:
#include <nlohmann/json.hpp>
#include <fstream>
int main() {
using nlohmann::json;
json doc = {{"version", 1}, {"app", "TaskTracker"}};
std::ofstream out("tasks.json");
if (!out) return 1;
out << doc.dump(2) << '\n';
}
Técnicamente puedes escribir sin '\n', pero normalmente una nueva línea al final del archivo hace la vida más agradable al verlo en un editor y al comparar diffs en git.
8. Mini-trozo de aplicación: cargar y guardar documento
Vamos a dar un paso importante, pequeño pero arquitectónicamente útil: sacaremos lectura/escritura JSON a funciones separadas. Esto ayuda a mantener main() «delgado» y a no dispersar try/catch por todo el código.
Por ahora no hacemos serialización completa de struct Task (vendrá después), pero aprenderemos a guardar todo el documento como json.
Función para cargar el documento
Crearemos la función load_json_document, que devuelve el documento o, en caso de problemas, un objeto vacío (simplificación para la versión didáctica). Sí, en rigor es mejor devolver optional o expected, pero eso ya lo veremos; hoy mantendremos el diseño lo más simple posible.
#include <nlohmann/json.hpp>
#include <fstream>
#include <iostream>
#include <string>
nlohmann::json load_json_document(const std::string& path) {
using nlohmann::json;
std::ifstream in(path);
if (!in) return json::object();
try {
json doc;
in >> doc;
return doc;
} catch (const json::parse_error&) {
return json::object();
}
}
No hay aquí «diagnóstico inteligente»: por ahora aprendemos la mecánica de parse()/dump(). Pero ya se aprecia la comodidad: en main() puedes llamar load_json_document("tasks.json") y seguir trabajando.
Función para guardar el documento
Ahora la operación simétrica: guardamos con formato bonito para que cualquiera pueda abrir el archivo y no arrepentirse.
#include <nlohmann/json.hpp>
#include <fstream>
#include <string>
bool save_json_document(const std::string& path, const nlohmann::json& doc) {
std::ofstream out(path);
if (!out) return false;
out << doc.dump(2) << '\n';
return true;
}
Y ahora tenemos una sensación agradable de «interfaz»: cargar documento y guardar documento son dos operaciones claras que no ensucian el resto del código.
Uso mínimo en main()
Construyamos un pequeño escenario: cargamos el documento, si está vacío creamos el por defecto, y lo guardamos de nuevo. No es la lógica «ideal», pero como demostración del ciclo file → json → file funciona muy bien.
#include <nlohmann/json.hpp>
#include <iostream>
#include <string>
nlohmann::json load_json_document(const std::string& path);
bool save_json_document(const std::string& path, const nlohmann::json& doc);
int main() {
nlohmann::json doc = load_json_document("tasks.json");
if (doc.empty()) {
doc["version"] = 1;
doc["tasks"] = nlohmann::json::array();
}
std::cout << doc.dump(2) << '\n';
save_json_document("tasks.json", doc);
}
Fijaos: no discutimos aquí la «corrección» de los campos version/tasks. Hoy solo aprendimos a leer y escribir JSON desde el punto de vista técnico.
9. Errores típicos al trabajar con parse() y dump()
Error nº1: intentar «leer JSON» sin manejar errores de parseo.
Los novatos suelen escribir json j = json::parse(text); y asumir que siempre funcionará. Pero el JSON puede estar roto: coma de más, comillas mal, archivo truncado. Si no capturas json::parse_error, la app será «a veces funciona». La protección mínima es un try/catch alrededor del parseo.
Error nº2: no distinguir «archivo no abierto» de «JSON dentro del archivo roto».
Si haces std::ifstream in(path); in >> j; sin comprobar if (!in), ante un problema de archivo empezarás a sospechar de todo: permisos, parser, rayos cósmicos. El orden correcto es simple: primero comprueba el stream, luego parsea y captura parse_error por separado.
Error nº3: usar operator[] para leer y crear campos accidentalmente.
j["x"] parece cómodo, pero en ciertos escenarios puede mutar el objeto y añadir una clave inexistente. Es especialmente molesto en el flujo «leer → guardar», porque de pronto empiezas a escribir campos fantasma. Para lectura es más seguro usar contains() + at().
Error nº4: pensar que dump() es validación.
Si parseaste JSON y luego hiciste dump(), no significa que los datos tengan sentido semántico. Solo significa que hay algún árbol JSON en memoria. Un campo puede ser una cadena en vez de número, una clave puede faltar, un array puede estar vacío. La corrección semántica es otro tema que tratamos más adelante.
Error nº5: guardar JSON «como venga» y luego sufrir al inspeccionar el archivo.
Técnicamente dump() sin indentación es válido, pero hace daño a los humanos. Cuando abres tasks.json y ves una línea de 5000 caracteres, dan ganas de cerrar el archivo y retirarse. dump(2) cuesta casi nada y mejora enormemente la mantenibilidad.
GO TO FULL VERSION