Saltar al contenido

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:

TXT
{ "nombre": "nuevo" }                 → cambia el nombre, no toques la descripción{ "nombre": "nuevo", "desc": null }   → cambia el nombre Y borra la descripción

Con un Option normal no puedes distinguirlos: los dos llegan con desc: None. Para distinguirlos hace falta un nivel más:

Rust
#[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:

Rust
#[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 null es 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.