Serializando ObjectId como String en Rust

Mongo DB es la base de datos que comúnmente utilizo para la mayoría de los proyectos en los que trabajo. En Mongo la información es guardada como documentos, en un formato especial denominado BSON (Binary JSON).

ObjectId es un tipo de datos definido en BSON, se utiliza para generar valores únicos dentro de la base de datos. Dado lo anterior es forzosamente el tipo que se utiliza para generar el valor que identifica a cada documento mediante el campo _id.

Un ejemplo es el siguiente documento que define un track. En él se observa el campo _id que corresponde a un valor que almacena un ObjectId. El campo user tiene el mismo tipo de datos.

Transformar una estructura de datos definida en Rust en un documento de Mongo DB requiere un proceso de serialización. Serde es una librería muy popular y que permite realizar serializaciones y deserializaciones de estructuras de datos en este lenguaje.

El documento visto anteriormente puede definirse con la siguiente estructura.

// Librería que permite el manejo de fechas y horas
use chrono::{DateTime, Utc};
// La librería de mongodb contiene un tipo de datos ObjectId
use mongodb::bson::oid::ObjectId;
// Para la serialización y deserialización
use serde::{Deserialize, Serialize};

#[derive(Deserialize, Serialize)]
struct Track {
    #[serde(
        rename = "_id",
        skip_serializing_if = "Option::is_none"
    )]
    id: Option<ObjectId>,
    user: ObjectId,
    name: String,
    description: String,
    created_at: DateTime<Utc>,
}

Es de notar que el campo id contiene un Option<ObjectId> lo cual se debe a que podemos dejar que Mongo DB se encargue de asignar el identificador de cada documento. En este caso cuando creamos una nueva instancia de Track no conocemos tal identificador, es sólo después de guardar el documento que lo obtenemos de Mongo.

Si queremos usar esta misma estructura para una REST API que transmite documentos en JSON podemos usar la librería serde_json que permite serializar y deserializar este tipo de datos.

Sin embargo si queremos inmediatamente hacer uso de la misma estructura y transformarla en JSON encontraremos que ObjectId se compone de un objeto con una propiedad $oid que almacena al identificador, tal y como se define en su especificación.

{
    "_id": {
        "$oid":"62ae1dfae4fc90d2b9ce3222"
    },
    "user": {
        "$oid":"62ae1dfae4fc90d2b9ce3223"
    },
    "name":"Track no str",
    "description":"Description",
    "created_at":"2022-06-18T18:48:26.443379Z"
}

Lamentablemente esta representación requiere que todos aquellos que quieren consumir estos documentos en JSON implementen cambios para poder tratar a los campos _id y user con esta representación. En general los clientes esperarían que los identificadores sean transferidos en una cadena de caracteres y no un objeto.

Por tanto tenemos que realizar un cambio en la forma en que serde convierte el documento a JSON, indicándole que para la serialización utilizará otro método.

Esto se logra mediante el atributo [serialize_with](https://serde.rs/field-attrs.html) del campo. Dicho atributo espera una función con la forma.

fn<S>(&T, S) -> Result<S::Ok, S::Error>

En este caso contamos con Option<ObjectId> y ObjectId como tipos de campos a los que queremos serializar como String. Por lo tanto implementaremos funciones para cada uno.

use serde::Serializer;
use mongodb::bson::oid::ObjectId;

pub fn serialize_option_oid_as_string<S>(oid: &Option<ObjectId>, serializer: S) -> Result<S::Ok, S::Error> 
    where S: Serializer
{
    match oid {
        Some(ref oid) => serializer.serialize_some(oid.to_string().as_str()),
        None => serializer.serialize_none()
    }
}

pub fn serialize_oid_as_string<S>(oid: &ObjectId, serializer: S) -> Result<S::Ok, S::Error>
    where S: Serializer
{
    serializer.serialize_str(oid.to_string().as_str())
}

ObjectId cuenta con un método to_string() que nos servirá para este caso. También serializer cuenta con el método serialize_str que aplica una transformación a dicha cadena y obtener el resultado.

Ahora sólo queda definir estas funciones en sus respectivos atributos

#[derive(Deserialize, Serialize)]
struct Track {
    #[serde(rename = "_id", skip_serializing_if="Option::is_none", serialize_with="serialize_option_oid_as_string")]
    id: Option<ObjectId>,
    name: String, #[serde(serialize_with="serialize_oid_as_string")]
    user: ObjectId,
    description: String,
    created_at: DateTime<Utc>,
}

Con lo cual obtenemos el resultado esperado.

{
    "_id":"62ae29aafbaae94b6dcf61d0",
    "name":"Track no str",
    "user":"62ae29aafbaae94b6dcf61d2",
    "description":"Description",
    "created_at":"2022-06-18T19:38:18.726771Z"
}

El código del ejemplo se encuentra aquí.

Para aprender más sobre serializadores pueden referirse a dicha sección en la documentación de serde.