WEB3DEV Español

Cover image for Guía Detallada para la Serialización y Deserialización con Serde en Rust
Hector
Hector

Posted on

Guía Detallada para la Serialización y Deserialización con Serde en Rust

Mientras trabajamos con los Request HTTP, siempre necesitamos convertir de ida y vuelta entre la estructura de datos, puede ser enum, struct, etc y un formato que puede ser almacenado o transmitido y reconstruído luego, por ejemplo, JSON.

Serde es una biblioteca (caja) para _ser_ializar y _de_serializar (serializing / deserializing) la estructura de datos de Rust eficiente y generalmente. En este artículo, te mostraré cómo usar los Atributos para persononalizar las implementaciones Serialize y Deserialize producidas por la derivada de Serde.

Comenzando

Empecemos con un simple struct Student que define como abajo e inicializa nuestro primer estudiante llamado Tom.

use serde::{Serialize, Deserialize};
#[derive(Debug, Clone, Serialize, Deserialize)]
struct Student {
pub name: String,
pub student_id: String,
}


let student = Student{name:"tom".to_owned(), student_id:"J19990".to_owned()};
Enter fullscreen mode Exit fullscreen mode

Convención del Caso

Para el ejemplo de arriba, si convertimos la cadena JSON usando serde_json::to_string(&student) como ahora, el output se verá así:

{
"name": "tom",
"student_id": "J19990"
}
Enter fullscreen mode Exit fullscreen mode

¡Se ve bien! Sin embargo, dependiendo de dónde envíes tu Pedido HTTP, una convención de caso diferente que en Rust pueda aplicarse. Básicamente, hay dos formas de acercarse: puedes renombrar el campo rename o puedes aplicar el caso de convención para todo el struct.

Por ejemplo, queremos studentId en vez de student_id como el nombre del campo.

Acercamiento 1: renombra un solo campo con #[serde(rename="").

struct Student {
pub name: String,
#[serde(rename="studentId")
pub student_id: String,
}
Enter fullscreen mode Exit fullscreen mode

Acercamiento 2: aplica un caso de convención camelCase a toda la estructura usando #[serde(rename_all="camelCase").

#[serde(rename_all = "camelCase")]
struct Student {
pub name: String,
pub student_id: String,
}
Enter fullscreen mode Exit fullscreen mode

Cualquiera de los métodos te dará el siguiente output:

{
"name": "tom",
"studentId": "J19990"
}
Enter fullscreen mode Exit fullscreen mode

Además de camelCase, también hay otra convención de caso que puedes aplicar. Los valores posibles son lowercase, UPPERCASE, PascalCase, camelCase, snake_case, SCREAMING_SNAKE_CASE, kebab-case, SCREAMING-KEBAB-CASE.

Otra cosa que puede que te estés preguntando, ¿por qué querrías renombrar el campo? Bueno, es muy útil si el campo del nombre requerido es un keyword reservado de Rust como type. Otro lugar útil es cuando trabajas con enum y quieres que sea externamente tageado con un nombre específico. Pronto iremos a través de esto.

Skip

Skip puede usarse en estos campos que no quieras serializar o deserializar. Un ejemplo sencillo puede ser así: vamos a añadir birth_year y age a nuestro Student.

struct Student {
pub name: String,
pub student_id: String,
pub birth_year: u32,
pub age: u32,
}
Enter fullscreen mode Exit fullscreen mode

Quizá queramos actualizar age dinámicamente y, por lo tanto, necesitamos una referencia del birth_year del estudiante. Sin embargo cuando enviamos el pedido, sólo el campo age debería estar presente. Esto se puede resolver usando #[serde(skip)].

struct Student {
pub name: String,
pub student_id: String,
#[serde(skip)]
pub birth_year: u32,
pub age: u32,
}
Enter fullscreen mode Exit fullscreen mode

Hacer esto, es lo que serán nuestros objetos JSON

{
"name": "tom",
"studentId": "J19990",
"age": 123
}
Enter fullscreen mode Exit fullscreen mode

Salteándose birth_year

Skip If

Dos de las formas más comunes (al menos para mi) para usar estos campos Option y Vectors vacíos.

Opción

Digamos que tenemos un campo middle_name: Option<String> por la estructura student. Si queremos saltarnos este campo en el caso donde student no tiene uno, podemos hacer lo siguiente:

#[serde(skip_serializing_if="Option::is_none")]
pub middle_name: Option<String>
Enter fullscreen mode Exit fullscreen mode

Esto producirá un output JSON como seguir al estudiante con o sin el segundo nombre.

// sin el nombre medio
{
"name": "tom",
"studentId": "J19990",
}

// con el nombre medio
{
"name": "tom",
"studentId": "J19990",
"middleName": "middle"
}
Enter fullscreen mode Exit fullscreen mode

Campo del vector

Por ejemplo, tenemos el campo pets: Vec<String> para la estructura student. Ya que el estudiante no necesariamente tiene que tener una mascota, puede ser un vector vacío.

Para saltarnos el serialización en un vector vacío, puedes añadir el siguiente atributo al campo:

#[serde(skip_serializing_if="Vec::is_empty")]
pub pets: Vec<String>,
Enter fullscreen mode Exit fullscreen mode

Las diferencias en el output entre tener el atributo y no, se muestra abajo:

// no tiene atributo
{
"name": "tom",
"studentId": "J19990",
"pets": []
}

// tiene atributo
{
"name": "tom",
"studentId": "J19990"
}
Enter fullscreen mode Exit fullscreen mode

Dependiendo del requisito del cuerpo de tu pedido, puedes elegir entre los dos.

Flatten

Esto es especialmente útil cuando tienes una estructura que quiere hacer algunos campos públicos y/o darles valores predeterminados, pero no otros, considerar las claves agrupadas con frecuencia, etc.

Para mostrarte lo que quiero decir, vamos a crear una nueva estructura llamada SideInfo y cambiar la estructura Student a lo siguiente.

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
struct Student {
pub name: String,
pub student_id: String,
#[serde(skip_serializing_if="Option::is_none")]
pub side_info: Option<SideInfo>
}

#[derive(Debug, Clone, Serialize, Deserialize, Default)]
#[serde(rename_all = "camelCase")]
struct SideInfo {
#[serde(skip_serializing_if="Option::is_none")]
pub pets: Option<Vec<String>>,
#[serde(skip_serializing_if="Option::is_none")]
pub address: Option<String>,
}
Enter fullscreen mode Exit fullscreen mode

Haciendo esto, podemos darle valores Default a los campos dentro de SideInfo mientras requiere el Input del usuario por Student. Esto es especialmente útil cuando tienes muchos campos que quieras darle valores por defecto.

Vamos a crear un nuevo Student:

let student = Student{name:"dan".to_owned(), student_id: "1".to_owned(), side_info:Some(SideInfo{address:Some("47 street".to_owned()), ..Default::default()})};
Enter fullscreen mode Exit fullscreen mode

E imprimamos la cadena JSON en el:

{
"name": "dan",
"studentId": "1",
"sideInfo": {
"address": "47 street"
}
}
Enter fullscreen mode Exit fullscreen mode

Como puedes ver, el campo address está anidado dentro de sideInfo. Sin embargo, añadiendo el atributo flatten al campo sideInfo dentro de la estructura Student:

#[serde(skip_serializing_if="Option::is_none", flatten)]
pub side_info: Option<SideInfo>
Enter fullscreen mode Exit fullscreen mode

Ahora tendremos:

{
"name": "dan",
"studentId": "1",
"address": "47 street"
}
Enter fullscreen mode Exit fullscreen mode

Tag vs Untag en enum

Digamos que tenemos un enum StudentList como el siguiente:

enum StudentList {
Student1(Student),
Student2(Student)
}
Enter fullscreen mode Exit fullscreen mode

En este ejemplo, realmente no tienes que tener enum, pero sólo para mostrarte cómo usar el tageo y renombre, aguanta un momento.

Declara una lista de estudiantes:

let student1 = Student{name:"tom".to_owned(), student_id:"J19990".to_owned(), pets: vec![], middle_name:Some("middle".to_owned())};
let student2 = Student{name:"dan".to_owned(), student_id:"J19990".to_owned(), pets: vec![], middle_name:Some("middle".to_owned())};

let student_list = vec![StudentList::Student1(student1), StudentList::Student2(student2)];
Enter fullscreen mode Exit fullscreen mode

Si imprimimos el JSON como está ahora mismo, será como a continuación. Está externamente tageado y es el comportamiento por defecto de serde.

[
{
"Student1": {
"name": "tom",
"studentId": "J19990",
"pets": [],
"middleName": "middle"
}
},
{
"Student2": {
"name": "dan",
"studentId": "J19990",
"pets": [],
"middleName": "middle"
}
}
]
Enter fullscreen mode Exit fullscreen mode

Pero, ¿qué pasa si quieres que todos los tags tengan el mismo nombre, como por ejemplo Student? Puede que pienses que puedas usar rename_all para lograr esto, pero no es así. Tendrás que rename cada una de las variables dentro de enum.

#[derive(Debug, Clone, Serialize, Deserialize)]

enum StudentList {
#[serde(rename="Student")]
Student1(Student),
#[serde(rename="Student")]
Student2(Student)
}
Enter fullscreen mode Exit fullscreen mode

El output será:

[
{
"Student": {
"name": "tom",
"studentId": "J19990",
"pets": [],
"middleName": "middle"
}
},
{
"Student": {
"name": "dan",
"studentId": "J19990",
"pets": [],
"middleName": "middle"
}
}
]
Enter fullscreen mode Exit fullscreen mode

Untagged

¿Qué pasa si sólo queremos un simple array de estudiante sin mostrarte la variante del nombre enum? Podemos lograr esto añadiendo el atributo #[serde(untagged)] al enum. Haciéndolo, nuestro output sería:

[
{
"name": "tom",
"studentId": "J19990",
"pets": [],
"middleName": "middle"
},
{
"name": "dan",
"studentId": "J19990",
"pets": [],
"middleName": "middle"
}
]
Enter fullscreen mode Exit fullscreen mode

Tageado Internamente

Otra representación posible de enum sería internamente tageada.

Vamos a crear un nuevo enum que contiene otros tipos de estudiantes distintos. Tendremos estudiantes Leader, Subleader y Regular:

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(tag = "type", rename_all="camelCase")]
enum StudentType {
Regular(Student),
Leader(Student),
SubLeader(Student)
}
Enter fullscreen mode Exit fullscreen mode

Especificar serde(tag = “type”) nos permitirá tener el tag que identifica con cuál variable estamos trabajando dentro del contexto, luego de cualquier otros campos de la variante como abajo:

[
{
"type": "leader",
"name": "tom",
"studentId": "J19990",
"pets": [],
"middleName": "middle"
},
{
"type": "regular",
"name": "dan",
"studentId": "J19990",
"pets": [],
"middleName": "middle"
}
]
Enter fullscreen mode Exit fullscreen mode

Taggeado Adyacentemente

Esta es la sintaxis deseada/común en el mundo de Haskell donde el tag y el contenido están adyacentes el uno al otro como dos campos dentro del mismo objeto.

Cambia los atributos de enum a lo siguiente:

#[serde(tag = "type", content = "student", rename_all="camelCase")]
Enter fullscreen mode Exit fullscreen mode

Nos proveerán:

[
{
"type": "leader",
"student": {
"name": "tom",
"studentId": "J19990",
"pets": [],
"middleName": "middle"
}
},
{
"type": "regular",
"student": {
"name": "dan",
"studentId": "J19990",
"pets": [],
"middleName": "middle"
}
}
]
Enter fullscreen mode Exit fullscreen mode

Hay muchas, muchas otras cosas que puedes hacer con Serde. Si estás interesado, ¡ve y revisa su página oficial para más información!

¡Gracias por leer! ¡Feliz codeo!

Este artículo es una traducción de Itsuki, hecha por Héctor Botero. Puedes encontrar el artículo original aquí.
Sería genial escucharte en nuestro Discord, puedes contarnos tus ideas, comentarios, sugerencias y dejarnos saber lo que necesitas.
Si prefieres puedes escribirnos a @web3dev_es en Twitter.

Discussion (0)