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()};
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"
}
¡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,
}
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,
}
Cualquiera de los métodos te dará el siguiente output:
{
"name": "tom",
"studentId": "J19990"
}
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,
}
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,
}
Hacer esto, es lo que serán nuestros objetos JSON
{
"name": "tom",
"studentId": "J19990",
"age": 123
}
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>
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"
}
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>,
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"
}
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>,
}
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()})};
E imprimamos la cadena JSON en el:
{
"name": "dan",
"studentId": "1",
"sideInfo": {
"address": "47 street"
}
}
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>
Ahora tendremos:
{
"name": "dan",
"studentId": "1",
"address": "47 street"
}
Tag vs Untag en enum
Digamos que tenemos un enum StudentList
como el siguiente:
enum StudentList {
Student1(Student),
Student2(Student)
}
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)];
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"
}
}
]
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)
}
El output será:
[
{
"Student": {
"name": "tom",
"studentId": "J19990",
"pets": [],
"middleName": "middle"
}
},
{
"Student": {
"name": "dan",
"studentId": "J19990",
"pets": [],
"middleName": "middle"
}
}
]
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"
}
]
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)
}
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"
}
]
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")]
Nos proveerán:
[
{
"type": "leader",
"student": {
"name": "tom",
"studentId": "J19990",
"pets": [],
"middleName": "middle"
}
},
{
"type": "regular",
"student": {
"name": "dan",
"studentId": "J19990",
"pets": [],
"middleName": "middle"
}
}
]
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)