Leer y escribir JSON en Rust con serde

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.

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
Mira las comillas de la primera línea. Imprime "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)]
El error que para a todo el mundo la primera vez. Si añades serde sin la característica 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.