Serde es la biblioteca de serialización y deserialización estándar en el ecosistema Rust. Más allá del derive básico, ofrece una colección de atributos para renombrar campos, omitir valores, aplanar structs anidadas, manejar enums polimórficos y aplicar transformaciones personalizadas. Dominar estos atributos es imprescindible para integrarse con APIs externas que no siguen las convenciones de Rust.
rename_all: adaptar convenciones de nombres
Las APIs REST suelen usar camelCase o snake_case en JSON, mientras que Rust usa snake_case en structs. #[serde(rename_all = "camelCase")] convierte automáticamente todos los nombres de campos al formato indicado.
use serde::{Deserialize, Serialize};
#[derive(Serialize, Deserialize, Debug)]
#[serde(rename_all = "camelCase")]
struct PerfilUsuario {
primer_nombre: String,
apellido: String,
fecha_nacimiento: String,
codigo_postal: String,
}
fn main() {
let perfil = PerfilUsuario {
primer_nombre: "Alice".into(),
apellido: "García".into(),
fecha_nacimiento: "1990-05-15".into(),
codigo_postal: "28001".into(),
};
let json = serde_json::to_string_pretty(&perfil).unwrap();
println!("{json}");
// { "primerNombre": "Alice", "apellido": "García", ... }
// rename_all acepta: "camelCase", "PascalCase", "SCREAMING_SNAKE_CASE",
// "kebab-case", "lowercase", "UPPERCASE", "snake_case"
}
skip_serializing_if: campos opcionales
#[serde(skip_serializing_if = "Option::is_none")] omite el campo en el JSON cuando es None. Esto evita enviar "campo": null a APIs que no esperan claves nulas.
use serde::{Deserialize, Serialize};
fn es_cero(n: &u32) -> bool { *n == 0 }
#[derive(Serialize, Deserialize, Debug)]
struct Producto {
nombre: String,
precio: f64,
#[serde(skip_serializing_if = "Option::is_none")]
descripcion: Option<String>,
#[serde(skip_serializing_if = "es_cero")]
stock_reservado: u32,
#[serde(skip_serializing_if = "Vec::is_empty")]
etiquetas: Vec<String>,
}
fn main() {
let p = Producto {
nombre: "Libro Rust".into(),
precio: 39.99,
descripcion: None,
stock_reservado: 0,
etiquetas: vec![],
};
println!("{}", serde_json::to_string(&p).unwrap());
// {"nombre":"Libro Rust","precio":39.99} campos opcionales omitidos
}
flatten: aplanar structs anidadas
#[serde(flatten)] incorpora los campos de un struct anidado directamente en el JSON padre, sin crear un objeto anidado. Es útil para descomponer structs grandes en partes reutilizables o para mapear APIs con campos mixtos.
use serde::{Deserialize, Serialize};
#[derive(Serialize, Deserialize, Debug)]
struct Metadatos {
creado_en: String,
actualizado_en: String,
version: u32,
}
#[derive(Serialize, Deserialize, Debug)]
struct Articulo {
id: u64,
titulo: String,
#[serde(flatten)]
meta: Metadatos, // los campos de Metadatos aparecen al mismo nivel
}
fn main() {
let json = r#"{
"id": 1,
"titulo": "Serde avanzado",
"creado_en": "2024-01-01",
"actualizado_en": "2024-06-15",
"version": 3
}"#;
let articulo: Articulo = serde_json::from_str(json).unwrap();
println!("{articulo:?}");
// Los campos de Metadatos se leen del nivel raíz del JSON
println!("{}", serde_json::to_string(&articulo).unwrap());
// Se serializa con todos los campos al mismo nivel
}
tag y untagged: enums polimórficos
Los enums representados en JSON pueden usar distintas estrategias. #[serde(tag = "tipo")] añade un campo discriminante; #[serde(untagged)] intenta deserializar cada variante por su forma.
use serde::{Deserialize, Serialize};
// Con tag: el campo "tipo" discrimina la variante
#[derive(Serialize, Deserialize, Debug)]
#[serde(tag = "tipo", rename_all = "snake_case")]
enum Evento {
UsuarioCreado { id: u64, nombre: String },
PedidoCompletado { pedido_id: u64, total: f64 },
ErrorOcurrido { codigo: u32, mensaje: String },
}
// Sin tag (untagged): intenta cada variante por su estructura
#[derive(Serialize, Deserialize, Debug)]
#[serde(untagged)]
enum Respuesta {
Ok { dato: String },
Error { error: String, codigo: u32 },
}
fn main() {
let evento = Evento::PedidoCompletado { pedido_id: 42, total: 129.99 };
let json = serde_json::to_string(&evento).unwrap();
println!("{json}");
// {"tipo":"pedido_completado","pedido_id":42,"total":129.99}
let r: Respuesta = serde_json::from_str(r#"{"error":"no encontrado","codigo":404}"#).unwrap();
println!("{r:?}");
}
serde(with): formatos personalizados
#[serde(with = "módulo")] redirige la serialización y deserialización de un campo a funciones propias. Es la solución cuando el tipo no implementa Serialize/Deserialize o cuando el formato externo difiere del interno.
use serde::{Deserialize, Serialize};
mod serde_timestamp {
use serde::{Deserializer, Serializer};
pub fn serialize<S: Serializer>(ts: &u64, ser: S) -> Result<S::Ok, S::Error> {
ser.serialize_str(&format!("{ts}s"))
}
pub fn deserialize<'de, D: Deserializer<'de>>(de: D) -> Result<u64, D::Error> {
use serde::Deserialize;
let s = String::deserialize(de)?;
s.trim_end_matches('s').parse().map_err(serde::de::Error::custom)
}
}
#[derive(Serialize, Deserialize, Debug)]
struct Evento {
nombre: String,
#[serde(with = "serde_timestamp")]
duracion_segundos: u64,
#[serde(default, skip_serializing_if = "Option::is_none")]
descripcion: Option<String>,
}
fn main() {
let e = Evento { nombre: "Procesamiento".into(), duracion_segundos: 42, descripcion: None };
let j = serde_json::to_string(&e).unwrap();
println!("{j}"); // {"nombre":"Procesamiento","duracion_segundos":"42s"}
let e2: Evento = serde_json::from_str(&j).unwrap();
println!("{e2:?}");
}
La combinación de estos atributos permite adaptar cualquier formato de API sin código de transformación manual. El patrón habitual es definir un struct de "DTO" (Data Transfer Object) con todos los atributos serde necesarios para el formato externo, y un struct interno limpio para la lógica de negocio, con una conversión explícita entre ambos.
