Option, null y el undefined que no existe
JSON tiene null y no tiene undefined, y JavaScript tiene los dos. De ese desajuste salen tres decisiones: si un campo ausente es lo mismo que un campo nulo, qué hacer al recibir, y por qué un campo opcional casi nunca debería serlo.
Las reglas
La última fila es la que muerde: { id: 42, nombre: undefined } llega a Rust como {"id":42}. Si nombre es un String, error de deserialización; si es Option<String>, llega como None en silencio.
Ausente frente a nulo
Hay dominios donde la diferencia importa: «no me has dicho nada de este campo» no es lo mismo que «este campo está vacío». El caso clásico es una actualización parcial:
{ "nombre": "nuevo" } → cambia el nombre, no toques la descripción{ "nombre": "nuevo", "desc": null } → cambia el nombre Y borra la descripciónCon un Option normal no puedes distinguirlos: los dos llegan con desc: None. Para distinguirlos hace falta un nivel más:
#[derive(serde::Deserialize)]pub struct ActualizarProyecto { pub nombre: Option<String>, // Option de Option: fuera dice si vino la clave, dentro si traía valor #[serde(default, deserialize_with = "doble_option")] pub descripcion: Option<Option<String>>,}Es feo, funciona, y casi siempre la respuesta correcta es no necesitarlo: si mandas la operación en vez del estado —nivel 17.1— entonces «borrar la descripción» es una operación con su nombre y no un null en un objeto. La ambigüedad desaparece porque nunca existió.
Controlar cómo sale
Dos atributos que conviene conocer:
#[derive(serde::Serialize)]#[serde(rename_all = "camelCase")]pub struct Proyecto { pub id: ProyectoId, // por defecto: sale siempre, con null cuando no hay pub descripcion: Option<String>, // con esto: si no hay, la clave no aparece #[serde(skip_serializing_if = "Option::is_none")] pub ultima_apertura: Option<String>,}Cuál elegir:
- Que salga siempre con
nulles lo más predecible para el frontend: el objeto tiene siempre la misma forma, y TypeScript la describe con un campo que puede ser nulo. Es el valor por defecto y suele ser lo correcto. - Omitir la clave ahorra bytes y complica el otro lado, porque el campo pasa a ser opcional en el tipo. Merece la pena solo si son muchos campos ausentes en muchos elementos.
El coste de un campo opcional
Y aquí está la parte que importa a medio plazo. Cada Option en un tipo que cruza es una rama que el frontend tiene que tratar, para siempre. Un tipo con seis campos opcionales tiene sesenta y cuatro formas posibles, y la interfaz tiene que estar bien en todas o fallar en alguna.
La tentación de poner Option es fuerte porque resuelve problemas inmediatos: un campo que todavía no calculas, uno que solo existe en algunos casos, uno que a veces falla. Tres correcciones que casi siempre son mejores:
| en vez de | mejor |
|---|---|
| un campo opcional que a veces no se ha calculado | un enum de estado: cargando, listo con el valor, fallo. Nivel 17.2 |
| un campo opcional que solo existe en algunos casos | dos tipos distintos, o un enum con las variantes reales |
| un campo opcional con un valor por defecto obvio | el valor por defecto, y sin Option |
La regla: un campo es Option cuando la ausencia es parte del dominio —un proyecto puede no tener descripción— y no cuando la ausencia es un accidente de la implementación.