serde en Rust: serializar y deserializar JSON, TOML y YAML con derives

serde es el framework de serialización más usado en Rust: convierte estructuras a JSON, TOML, YAML, MessagePack y otros formatos, y al revés. La clave es que todo el trabajo pesado ocurre en tiempo de compilación mediante macros derive. En runtime no hay reflexión ni overhead de inspección de tipos.

Instalación

// Cargo.toml
[dependencies]
serde      = { version = "1", features = ["derive"] }
serde_json = "1"

Derive básico en structs

use serde::{Serialize, Deserialize};

#[derive(Serialize, Deserialize, Debug)]
struct Usuario {
    id: u32,
    nombre: String,
    email: String,
    activo: bool,
}

fn main() -> serde_json::Result<()> {
    let usuario = Usuario {
        id: 1,
        nombre: "Ana García".into(),
        email: "[email protected]".into(),
        activo: true,
    };

    // Struct ? JSON
    let json = serde_json::to_string_pretty(&usuario)?;
    println!("{json}");

    // JSON ? Struct
    let json_entrada = r#"{"id":2,"nombre":"Luis","email":"[email protected]","activo":false}"#;
    let u2: Usuario = serde_json::from_str(json_entrada)?;
    println!("{u2:?}");

    Ok(())
}

Atributos de campo

use serde::{Serialize, Deserialize};

#[derive(Serialize, Deserialize, Debug)]
struct Producto {
    #[serde(rename = "product_id")]  // nombre distinto en JSON
    id: u32,

    nombre: String,

    #[serde(skip_serializing_if = "Option::is_none")]  // omitir si None
    descripcion: Option<String>,

    #[serde(default)]  // usar Default si falta en el JSON
    en_stock: bool,

    #[serde(skip)]     // ignorar completamente en ambas direcciones
    cache_interno: String,
}

fn main() -> serde_json::Result<()> {
    let p = Producto {
        id: 42,
        nombre: "Teclado mecánico".into(),
        descripcion: None,
        en_stock: true,
        cache_interno: "ignorado".into(),
    };

    let json = serde_json::to_string(&p)?;
    // {"product_id":42,"nombre":"Teclado mecánico","en_stock":true}
    // descripcion y cache_interno no aparecen
    println!("{json}");
    Ok(())
}

Enums con serde

use serde::{Serialize, Deserialize};

#[derive(Serialize, Deserialize, Debug)]
#[serde(tag = "tipo", content = "datos")]  // representación adyacente
enum Evento {
    Click { x: i32, y: i32 },
    Teclado { tecla: String, modificadores: Vec<String> },
    Scroll { delta: f64 },
}

fn main() -> serde_json::Result<()> {
    let evento = Evento::Click { x: 100, y: 200 };
    let json = serde_json::to_string_pretty(&evento)?;
    // {"tipo":"Click","datos":{"x":100,"y":200}}
    println!("{json}");

    let e2: Evento = serde_json::from_str(
        r#"{"tipo":"Teclado","datos":{"tecla":"Enter","modificadores":["Ctrl"]}}"#
    )?;
    println!("{e2:?}");
    Ok(())
}

serde_json::Value para JSON dinámico

use serde_json::{Value, json};

fn main() -> serde_json::Result<()> {
    // Construir JSON sin struct
    let payload = json!({
        "nombre": "API Rust",
        "version": 2,
        "activo": true,
        "etiquetas": ["rust", "api", "json"]
    });

    println!("{payload}");

    // Navegar el JSON
    if let Some(nombre) = payload["nombre"].as_str() {
        println!("Nombre: {nombre}");
    }

    // Parsear JSON arbitrario
    let texto = r#"{"desconocido": [1, 2, {"clave": "valor"}]}"#;
    let v: Value = serde_json::from_str(texto)?;
    println!("{}", v["desconocido"][2]["clave"]); // "valor"

    Ok(())
}

Reutilizar los derives con TOML y YAML

// Cargo.toml
// toml = "0.8"
// serde_yaml = "0.9"

use serde::{Serialize, Deserialize};

#[derive(Serialize, Deserialize, Debug)]
struct Config {
    host: String,
    puerto: u16,
    debug: bool,
}

fn main() -> anyhow::Result<()> {
    let config_toml = r#"
host = "localhost"
puerto = 8080
debug = true
"#;

    // El mismo struct funciona con TOML
    let config: Config = toml::from_str(config_toml)?;
    println!("{config:?}");

    // Y con YAML
    let config_yaml = "host: localhostnpuerto: 8080ndebug: truen";
    let config2: Config = serde_yaml::from_str(config_yaml)?;
    println!("{config2:?}");

    Ok(())
}

Resumen

  • #[derive(Serialize, Deserialize)] genera el código de serialización en compilación: cero overhead en runtime.
  • serde_json::to_string y from_str son los puntos de entrada más comunes.
  • Los atributos de campo (rename, skip, default, skip_serializing_if) controlan la representación JSON sin tocar la lógica.
  • serde_json::Value y el macro json! permiten trabajar con JSON sin structs tipadas.
  • El mismo #[derive(Serialize, Deserialize)] funciona con cualquier formato que tenga un crate serde-compatible: JSON, TOML, YAML, CBOR, MessagePack, etc.

COMPARTE ESTE ARTÍCULO

COMPARTIR EN FACEBOOK
COMPARTIR EN TWITTER
COMPARTIR EN LINKEDIN
COMPARTIR EN WHATSAPP