JSON con Serde en Rust
El JSON está en todas partes: archivos de configuración, APIs web, partidas guardadas. En Rust, casi todo el mundo usa dos crates para trabajar con él. serde (abreviatura de serialize / deserialize, serializar / deserializar) sabe convertir valores de Rust en datos y al revés. serde_json le enseña el formato JSON.
Piensa en serde como un traductor. Describes tus datos una sola vez, como un struct normal de Rust, y serde los traduce a JSON y de vuelta por ti. Nunca escribes el texto JSON a mano ni lo desmontas carácter a carácter.
Preparar el proyecto
Sección titulada «Preparar el proyecto»Dentro de tu proyecto, añade los dos crates. La feature derive hace que serde escriba por ti el código de traducción:
cargo add serde --features derivecargo add serde_jsonAhora tu Cargo.toml tiene:
[dependencies]serde = { version = "1.0", features = ["derive"] }serde_json = "1.0"Si Cargo.toml y cargo add son nuevos para ti, lee primero Cargo y crates en Rust. El Playground de Rust ya tiene los dos crates, así que todos los botones para probar el código de esta página funcionan sin preparar nada.
De struct a JSON
Sección titulada «De struct a JSON»Añade #[derive(Serialize)] a un struct y serde_json::to_string podrá convertirlo en texto JSON:
use serde::Serialize;
#[derive(Serialize)]struct User { name: String, age: u32, admin: bool,}
fn main() { let user = User { name: String::from("Ana"), age: 28, admin: false };
let compact = serde_json::to_string(&user).unwrap(); println!("{}", compact);
let pretty = serde_json::to_string_pretty(&user).unwrap(); println!("{}", pretty);}Salida:
{"name":"Ana","age":28,"admin":false}{ "name": "Ana", "age": 28, "admin": false}to_string te da un JSON compacto, ideal para enviarlo por la red. to_string_pretty añade saltos de línea y espacios, que queda mejor en archivos que va a leer una persona. Las claves salen en el mismo orden que los campos de tu struct.
De JSON a struct
Sección titulada «De JSON a struct»El camino contrario se llama deserializar. Añade Deserialize a la lista del derive y llama a serde_json::from_str. Le dices qué tipo quieres con una anotación de tipo:
use serde::{Deserialize, Serialize};
#[derive(Serialize, Deserialize, Debug)]struct User { name: String, age: u32, admin: bool,}
fn main() -> Result<(), serde_json::Error> { let text = r#"{"name": "Ana", "age": 28, "admin": false}"#;
let user: User = serde_json::from_str(text)?; println!("{:?}", user); println!("{} tiene {} años", user.name, user.age); Ok(())}Salida:
User { name: "Ana", age: 28, admin: false }Ana tiene 28 añosHay dos cosas en las que fijarse:
r#"..."#es un raw string (cadena literal sin escapes). Dentro puedes escribir"libremente, algo muy práctico para JSON.from_strdevuelve unResult, porque puede que el texto no sea JSON válido. AquímaindevuelveResult, así que podemos usar?para pasar cualquier error hacia arriba.
Cuando el JSON está mal
Sección titulada «Cuando el JSON está mal»El JSON real a menudo no es lo que esperas. Falta un campo, o un número llega como texto. serde lo comprueba todo y te da un error claro en lugar de fallar sin más. Usa match para tratarlo:
use serde::Deserialize;
#[derive(Deserialize, Debug)]struct User { name: String, age: u32,}
fn main() { let inputs = [ r#"{"name": "Ana", "age": 28}"#, r#"{"name": "Ben"}"#, r#"{"name": "Cy", "age": "old"}"#, ];
for text in inputs { match serde_json::from_str::<User>(text) { Ok(user) => println!("OK: {:?}", user), Err(e) => println!("Error: {}", e), } }}Salida:
OK: User { name: "Ana", age: 28 }Error: missing field `age` at line 1 column 15Error: invalid type: string "old", expected u32 at line 1 column 27Los mensajes de error los escribe serde_json en inglés. El primero dice que falta el campo age; el segundo, que llegó el texto "old" (“viejo”) cuando se esperaba un número u32. Los dos indican la línea y la columna donde está el problema, y por eso dejamos el JSON de entrada en inglés: si cambiáramos el texto, cambiaría también la columna.
La parte ::<User> se llama “turbofish”. Es otra forma de indicar qué tipo quieres, útil cuando no hay un let user: User en el que poner la anotación.
Claves que faltan: Option y #[serde(default)]
Sección titulada «Claves que faltan: Option y #[serde(default)]»¿Y si un campo es de verdad opcional? Tienes dos buenas opciones:
- Hacer que el campo sea un
Option. Una clave que falta (onull) se convierte enNone. - Añadir
#[serde(default)]. Una clave que falta recibe el valor por defecto del tipo:0,false, unStringvacío o unVecvacío.
use serde::Deserialize;
#[derive(Deserialize, Debug)]struct Profile { name: String, bio: Option<String>, #[serde(default)] followers: u32, #[serde(default)] tags: Vec<String>,}
fn main() { let full = r#"{"name": "Ana", "bio": "Le encanta Rust", "followers": 120, "tags": ["rust"]}"#; let short = r#"{"name": "Ben"}"#;
let a: Profile = serde_json::from_str(full).unwrap(); let b: Profile = serde_json::from_str(short).unwrap();
println!("{:?}", a); println!("{:?}", b);}Salida:
Profile { name: "Ana", bio: Some("Le encanta Rust"), followers: 120, tags: ["rust"] }Profile { name: "Ben", bio: None, followers: 0, tags: [] }Usa Option cuando “sin valor” signifique algo distinto de “cero” o “vacío”. Usa default cuando el valor por defecto te sirva.
Renombrar campos
Sección titulada «Renombrar campos»Rust usa snake_case para los nombres de los campos, pero muchas APIs web usan camelCase, y algunas usan nombres que ni siquiera son válidos en Rust (como type). No tienes que renunciar a tus nombres de Rust. Usa rename_all para todo el struct y rename para un solo campo:
use serde::{Deserialize, Serialize};
#[derive(Serialize, Deserialize, Debug)]#[serde(rename_all = "camelCase")]struct UserProfile { user_id: u64, display_name: String, is_verified: bool, #[serde(rename = "type")] account_type: String,}
fn main() { // el tipo de respuesta que podría devolver una API web let response = r#"{ "userId": 42, "displayName": "Ana Silva", "isVerified": true, "type": "premium" }"#;
let profile: UserProfile = serde_json::from_str(response).unwrap(); println!("{} (id {}) tiene una cuenta {}", profile.display_name, profile.user_id, profile.account_type);
// al serializar se usan los mismos nombres, así que el JSON va y vuelve igual println!("{}", serde_json::to_string(&profile).unwrap());}Salida:
Ana Silva (id 42) tiene una cuenta premium{"userId":42,"displayName":"Ana Silva","isVerified":true,"type":"premium"}Por defecto, serde simplemente ignora las claves del JSON que tu struct no tiene. Así que solo necesitas describir los campos que te interesan, aunque la API envíe cincuenta.
Listas de cosas: un Vec de structs
Sección titulada «Listas de cosas: un Vec de structs»Un array de JSON se convierte en un Vec. Los structs también pueden contener otros structs y vectores, así que puedes describir un JSON con muchos niveles simplemente anidando tipos:
use serde::Deserialize;
#[derive(Deserialize, Debug)]struct Task { title: String, done: bool,}
fn main() { let text = r#"[ {"title": "Aprender serde", "done": true}, {"title": "Escribir un archivo de configuración", "done": false}, {"title": "Llamar a una API", "done": false} ]"#;
let tasks: Vec<Task> = serde_json::from_str(text).unwrap();
for task in &tasks { let mark = if task.done { "x" } else { " " }; println!("[{}] {}", mark, task.title); } let left = tasks.iter().filter(|t| !t.done).count(); println!("Quedan {} tareas", left);}Salida:
[x] Aprender serde[ ] Escribir un archivo de configuración[ ] Llamar a una APIQuedan 2 tareasJSON sin tipos: Value y json!
Sección titulada «JSON sin tipos: Value y json!»A veces no conoces de antemano la forma del JSON, o solo necesitas un trocito. Para eso está serde_json::Value. Puede guardar cualquier JSON: un objeto, un array, un texto, un número, un booleano o null. La macro json! construye un Value con una sintaxis parecida a JSON:
use serde_json::{json, Value};
fn main() { let data: Value = json!({ "city": "Lisboa", "temps": [18, 21, 19], "sunny": true });
println!("Ciudad: {}", data["city"]); println!("Primera temperatura: {}", data["temps"][0]);
// as_str() te da un &str normal en lugar de un texto JSON if let Some(city) = data["city"].as_str() { println!("Texto normal: {}", city); }
// una clave que no existe da Null en lugar de fallar println!("Viento: {}", data["wind"]);}Salida:
Ciudad: "Lisboa"Primera temperatura: 18Texto normal: LisboaViento: nullFíjate en que al imprimir data["city"] salen comillas: sigue siendo un texto JSON. Usa .as_str(), .as_i64() o .as_bool() para obtener valores normales de Rust. Cada uno devuelve un Option, porque el valor podría ser de otro tipo.
Value es flexible, pero pierdes la ayuda del compilador. Cuando conozcas la forma de los datos, usa mejor un struct.
Leer y escribir un archivo JSON
Sección titulada «Leer y escribir un archivo JSON»Aquí tienes un ejemplo del mundo real: guardar la configuración de una app en config.json y volver a cargarla. Combina esta página con Leer y escribir archivos en Rust. std::fs::write guarda el texto y std::fs::read_to_string lo vuelve a cargar:
use serde::{Deserialize, Serialize};use std::error::Error;use std::fs;
#[derive(Serialize, Deserialize, Debug)]struct Config { username: String, theme: String, font_size: u32,}
fn main() -> Result<(), Box<dyn Error>> { let config = Config { username: String::from("ana"), theme: String::from("oscuro"), font_size: 14, };
// guardar let text = serde_json::to_string_pretty(&config)?; fs::write("config.json", text)?;
// cargar let loaded_text = fs::read_to_string("config.json")?; let loaded: Config = serde_json::from_str(&loaded_text)?; println!("Cargado: {:?}", loaded); Ok(())}Salida:
Cargado: Config { username: "ana", theme: "oscuro", font_size: 14 }¿Por qué Box<dyn Error>? Este main puede fallar de dos formas distintas: con un error de archivo (std::io::Error) o con un error de JSON (serde_json::Error). Box<dyn Error> significa “cualquier tipo de error”, así que ? funciona con los dos. La página de errores personalizados lo explica con más detalle.
Errores comunes
Sección titulada «Errores comunes»Olvidar derivar Deserialize. Si tu struct solo deriva Serialize, puedes escribir JSON pero no leerlo. El error es largo, pero las líneas importantes son estas:
error[E0277]: the trait bound `User: serde::Deserialize<'de>` is not satisfied |10 | let user: User = serde_json::from_str(r#"{"name":"Ana","age":28}"#).unwrap(); | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ unsatisfied trait bound | = note: for local types consider adding `#[derive(serde::Deserialize)]` to your `User` typeEl compilador te dice que User no implementa el trait Deserialize y te sugiere añadir #[derive(serde::Deserialize)]. La solución: #[derive(Serialize, Deserialize)]. Si en cambio ves “cannot find derive macro Serialize” (no se encuentra la macro derive Serialize), es que añadiste serde sin la feature derive. Ejecuta cargo add serde --features derive.
Usar .unwrap() con datos que no controlas. En ejemplos pequeños no pasa nada, pero el JSON que viene de un usuario o de una web puede estar roto. Un archivo mal formado hará que tu programa termine con un panic. Usa ? o match, como viste arriba.
Esperar que data["name"] se imprima sin comillas. Un Value se imprime como JSON. Llama a .as_str() para obtener el texto normal.
Práctica
Sección titulada «Práctica»1. Guardar una lista de la compra
Sección titulada «1. Guardar una lista de la compra»Crea un struct Item con un name (String) y una qty (u32). Mete dos elementos en un Vec, convierte el vector entero en JSON con una sola llamada e imprímelo.
Pista: solo el struct necesita #[derive(Serialize)]. Un Vec de cosas serializables también es serializable, y se convierte en un array JSON.
Ver solución
use serde::Serialize;
#[derive(Serialize)]struct Item { name: String, qty: u32,}
fn main() { let list = vec![ Item { name: String::from("manzanas"), qty: 3 }, Item { name: String::from("pan"), qty: 1 }, ];
let json = serde_json::to_string(&list).unwrap(); println!("{}", json);}Salida:
[{"name":"manzanas","qty":3},{"name":"pan","qty":1}]serde ya sabe manejar Vec, String y los números, así que basta con derivar Serialize en Item. Prueba to_string_pretty para ver la misma lista repartida en varias líneas.
2. Configuración con valores por defecto
Sección titulada «2. Configuración con valores por defecto»Lee este JSON en un struct Settings: {"siteTitle": "Mi blog", "postsPerPage": 5}. El struct debe tener site_title, posts_per_page y un theme que puede faltar. Luego lee también {"siteTitle": "Notas"}, donde posts_per_page debe tomar el valor 0 por defecto. Imprime los dos con {:?}.
Pista: necesitas #[serde(rename_all = "camelCase")] en el struct, Option<String> para theme y #[serde(default)] en posts_per_page.
Ver solución
use serde::Deserialize;
#[derive(Deserialize, Debug)]#[serde(rename_all = "camelCase")]struct Settings { site_title: String, #[serde(default)] posts_per_page: u32, theme: Option<String>,}
fn main() { let full = r#"{"siteTitle": "Mi blog", "postsPerPage": 5}"#; let short = r#"{"siteTitle": "Notas"}"#;
let a: Settings = serde_json::from_str(full).unwrap(); let b: Settings = serde_json::from_str(short).unwrap();
println!("{:?}", a); println!("{:?}", b);}Salida:
Settings { site_title: "Mi blog", posts_per_page: 5, theme: None }Settings { site_title: "Notas", posts_per_page: 0, theme: None }rename_all hace corresponder siteTitle con site_title y postsPerPage con posts_per_page. Ninguno de los dos JSON tiene theme, así que vale None las dos veces, y el postsPerPage que falta en el segundo se convierte en 0. Quita #[serde(default)] y el segundo unwrap() provoca un panic con “missing field postsPerPage” (falta el campo postsPerPage). Aquí .unwrap() está bien porque el JSON lo escribimos nosotros; con datos reales, usa ? o match.
Resumen
Sección titulada «Resumen»- Añade
serde(con--features derive) yserde_json, y luego#[derive(Serialize, Deserialize)]en tus structs. serde_json::to_string/to_string_prettyconvierten un valor en JSON;serde_json::from_strconvierte JSON en un valor.from_strdevuelve unResult. Con datos reales, trátalo con?omatch, no con.unwrap().- Usa
Optiono#[serde(default)]para claves que pueden faltar, yrename/rename_allpara que coincidan con los nombres del JSON. - Usa
serde_json::Valueyjson!cuando no conozcas la forma de los datos. Si la conoces, usa structs.
Comprueba lo que aprendiste
Sección titulada «Comprueba lo que aprendiste»Comprueba lo aprendido
1. ¿Qué derive te permite convertir texto JSON en tu struct?
2. Puede que un objeto JSON no tenga la clave bio. ¿Qué tipo es buena idea para ese campo?
3. Tu struct tiene un campo display_name, pero el JSON usa displayName. ¿Cuál es la solución más sencilla para todo el struct?
4. Por defecto, ¿qué hace serde con las claves del JSON que tu struct no tiene?
Puntuación: 0 / 4
Para leer más
Sección titulada «Para leer más»- serde.rs — la guía oficial de Serde (en inglés): una visión general de cómo funciona serde, con ejemplos para muchos formatos de datos.
- Container attributes — serde.rs (en inglés): opciones para un struct o enum completo, como
rename_allydeny_unknown_fields. - Field attributes — serde.rs (en inglés): opciones para campos sueltos, como
rename,default,skipyskip_serializing_if. serde_json— docs.rs (en inglés): la portada del crate, con ejemplos de JSON con tipos y sin tipos y de la macrojson!.serde_json::Value— docs.rs (en inglés): todos los métodos para leer JSON sin tipos, comoas_str,as_i64yget.