En Rust el JSON se maneja con serde, que es con diferencia la biblioteca más usada del ecosistema. Lo interesante es que no se escribe código de conversión: se marca el tipo con un atributo y el compilador genera el código de serializar y deserializar. Si los datos no encajan con lo declarado, falla al convertir y no a mitad del programa.
También te puede interesar
Las dependencias
Dos crates, y el primero necesita una característica concreta. Lo más seguro es dejar que Cargo escriba las líneas:
cargo add serde --features derive cargo add serde_json
El resultado en Cargo.toml:
[dependencies]
serde = { version = "1.0.229", features = ["derive"] }
serde_json = "1.0.151"
De un struct a JSON y de vuelta
El atributo #[derive(Serialize, Deserialize)] es todo lo que hay que escribir. A partir de ahí el tipo viaja en las dos direcciones:
use serde::{Deserialize, Serialize};
#[derive(Serialize, Deserialize, Debug)]
struct Articulo {
nombre: String,
precio: f64,
unidades: u32,
}
fn main() -> Result<(), Box<dyn std::error::Error>> {
let articulos = vec![
Articulo { nombre: "tornillos".into(), precio: 1.50, unidades: 120 },
Articulo { nombre: "tuercas".into(), precio: 0.80, unidades: 80 },
];
let json = serde_json::to_string_pretty(&articulos)?;
println!("{json}");
let recuperados: Vec<Articulo> = serde_json::from_str(&json)?;
println!("\nrecuperados {} artículos; el primero cuesta {:.2}",
recuperados.len(), recuperados[0].precio);
Ok(())
}
salida[
{
"nombre": "tornillos",
"precio": 1.5,
"unidades": 120
},
{
"nombre": "tuercas",
"precio": 0.8,
"unidades": 80
}
]
recuperados 2 artículos; el primero cuesta 1.50
Fíjate en que el JSON dice 1.5 y 0.8 aunque en el código se escribió 1.50 y 0.80. No es un fallo: JSON no guarda el número de decimales, guarda el número. Si necesitas dos decimales al mostrarlo, se piden al imprimir con {:.2}, como en la última línea. Y si los necesitas en el propio JSON, el precio no debería ser un f64 sino un entero de céntimos o una cadena.
El otro detalle es to_string_pretty, que genera JSON con sangría para que lo lea una persona. Para enviarlo por red se usa to_string, que lo deja en una línea.
Leer y escribir un archivo JSON
Combinándolo con lo del ejemplo de lectura, un archivo de configuración son cuatro líneas:
use serde::{Deserialize, Serialize};
use std::fs;
#[derive(Serialize, Deserialize, Debug)]
struct Config {
host: String,
puerto: u16,
depurar: bool,
}
fn main() -> Result<(), Box<dyn std::error::Error>> {
let config = Config { host: "localhost".into(), puerto: 8080, depurar: false };
fs::write("config.json", serde_json::to_string_pretty(&config)?)?;
let texto = fs::read_to_string("config.json")?;
let leida: Config = serde_json::from_str(&texto)?;
println!("{}:{} depurar={}", leida.host, leida.puerto, leida.depurar);
Ok(())
}
config.json y salida{
"host": "localhost",
"puerto": 8080,
"depurar": false
}
localhost:8080 depurar=false
JSON cuya forma no conoces
Cuando el JSON viene de fuera y no quieres declarar un struct para él, serde_json::Value permite recorrerlo como si fuera un diccionario:
fn main() -> Result<(), Box<dyn std::error::Error>> {
let texto = r#"{"ciudad":"Lima","habitantes":9700000,"etiquetas":["capital","costa"]}"#;
let v: serde_json::Value = serde_json::from_str(texto)?;
println!("ciudad : {}", v["ciudad"]);
println!("habitantes : {}", v["habitantes"].as_u64().unwrap());
println!("1ª etiqueta: {}", v["etiquetas"][0]);
println!("¿hay país? : {:?}", v.get("pais"));
Ok(())
}
salidaciudad : "Lima" habitantes : 9700000 1ª etiqueta: "capital" ¿hay país? : None
"Lima" con comillas, no Lima, porque v["ciudad"] no es una cadena sino un Value que contiene una cadena, y al mostrarlo se escribe como JSON. Para obtener el texto pelado hay que pedirlo: v["ciudad"].as_str().unwrap(). Lo mismo vale para los números, de ahí el as_u64() de la segunda línea.Y get("pais") devuelve None en lugar de fallar, porque la clave no existe. Es el Option de la lección de manejo de errores: la ausencia se representa en el tipo, no con una excepción.
Lo que se usa a diario
| Qué quieres | Cómo |
|---|---|
| Struct a texto JSON | serde_json::to_string(&x) |
| Lo mismo, legible para una persona | to_string_pretty(&x) |
| Texto JSON a struct | serde_json::from_str(&t) |
| JSON de forma desconocida | serde_json::Value |
| Que un campo pueda faltar | declararlo Option<T> |
| Que el campo se llame distinto en el JSON | #[serde(rename = "otro")] |
| Valor por defecto si falta | #[serde(default)] |
derive, el atributo no existe y el programa no compila: sale cannot find derive macro `Serialize` in this scope, con la nota de que «Serialize está importado, pero solo es un trait, sin macro derive». La causa no está en el código sino en el Cargo.toml: la línea de serde tiene que llevar features = ["derive"]. Por eso conviene añadirlo con cargo add serde --features derive en vez de escribir la línea a mano.Todo el código de esta página se compiló y ejecutó con rustc 1.96.1, serde 1.0.229 y serde_json 1.0.151 antes de publicarla; las salidas están copiadas de la ejecución real. Documentación oficial: serde.rs y serde_json en docs.rs.