À medida que trabalhamos com solicitações HTTP, sempre precisamos converter entre uma estrutura de dados, que pode ser enum
, struct
, etc. em um formato que pode ser armazenado ou transmitido e posteriormente reconstruído, por exemplo, JSON.
Serde é uma biblioteca (crate) para serializar e desserializar estruturas de dados Rust de forma eficiente e genérica. Neste artigo, mostrarei como usar Atributos para personalizar as implementações Serialize
e Deserialize
produzidas pela derivação do Serde.
Iniciando
Vamos começar com uma simples struct Student
, definida como abaixo, e inicializar nosso primeiro aluno chamado 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() };
Convenção de nomenclatura
No exemplo acima, se convertermos para uma string JSON usando serde_json::to_string(&student)
como está neste momento, o resultado será parecido com o abaixo.
{
"name": "tom",
"student_id": "J19990"
}
Até aqui tudo bem! No entanto, dependendo de onde você está enviando sua solicitação HTTP, pode ser aplicada uma convenção de nomenclatura diferente daquela em Rust. Existem basicamente duas abordagens. Você pode renomear o campo ou pode aplicar a convenção de nomenclatura a toda a struct.
Por exemplo, na verdade, queremos que studentId
seja o nome do campo em vez de student_id
.
Abordagem 1: renomear um único campo com #[serde(rename="")]
.
struct Student {
pub name: String,
#[serde(rename = "studentId")]
pub student_id: String,
}
Abordagem 2: aplicar a convenção de nomenclatura camelCase
a toda a struct usando #[serde(rename_all="camelCase")].
#[serde(rename_all = "camelCase")]
struct Student {
pub name: String,
pub student_id: String,
}
Ambos os métodos produzirão a seguinte saída:
{
"name": "tom",
"studentId": "J19990"
}
Além do camelCase
, também existem outras convenções de nomenclatura que você pode aplicar, como lowercase
, UPPERCASE
, PascalCase
, camelCase
, snake_case
, SCREAMING_SNAKE_CASE
, kebab
-case
, SCREAMING
-KEBAB
-CASE
.
Outra coisa que você pode estar se perguntando é por que você gostaria de renomear um campo? Bem, é super útil se o nome do campo necessário for uma palavra-chave reservada do Rust, como type
. Outro recurso útil é quando você está trabalhando com enums
e deseja que ele seja etiquetado externamente com um nome específico. Vamos abordar isso em breve.
Skip
Skip pode ser usado nos campos que você não deseja serializar ou desserializar. Um exemplo simples poderia ser o seguinte. Vamos adicionar birth_year
e age
ao nosso Student
.
struct Student {
pub name: String,
pub student_id: String,
pub birth_year: u32,
pub age: u32,
}
Podemos querer atualizar age
dinamicamente e, portanto, precisamos de uma referência ao birth_year
do aluno. No entanto, ao enviarmos a solicitação, apenas o campo age
deve estar presente. Isso pode ser resolvido usando #[serde(skip)].
struct Student {
pub name: String,
pub student_id: String,
#[serde(skip)]
pub birth_year: u32,
pub age: u32,
}
Ao fazer isso, nosso objeto JSON se tornará:
{
"name": "tom",
"studentId": "J19990",
"age": 123
}
com birth_year
ignorado.
Skip If
Duas das maneiras mais comuns (pelo menos para mim) de usar isso são para campos Option
e Vectors
vazios.
Option
Digamos que temos um campo middle_name: Option<String>
para a struct Student
. Se quisermos pular este campo no caso em que o student
não tiver um nome do meio, podemos fazer o seguinte.
#[serde(skip_serializing_if = "Option::is_none")]
pub middle_name: Option<String>
Isso produzirá um JSON de saída como o seguinte para um aluno com e sem nome do meio.
// sem nome do meio
{
"name": "tom",
"studentId": "J19990"
}
// com nome do meio
{
"name": "tom",
"studentId": "J19990",
"middleName": "middle"
}
Campo Vector
Por exemplo, temos um campo pets: Vec<String>
para a struct student
. Como o aluno não precisa necessariamente possuir um animal de estimação, pode ser um vetor vazio.
Para pular a serialização em um vetor vazio, você pode adicionar o seguinte atributo ao campo.
#[serde(skip_serializing_if = "Vec::is_empty")]
pub pets: Vec<String>,
As diferenças na saída entre ter o atributo e não ter são mostradas a seguir.
// sem o atributo
{
"name": "tom",
"studentId": "J19990",
"pets": []
}
// com o atributo
{
"name": "tom",
"studentId": "J19990"
}
Dependendo dos requisitos do corpo da solicitação, você pode escolher entre as duas opções.
Flatten
Isso é especialmente útil quando você tem uma struct na qual deseja tornar alguns campos públicos e/ou atribuir a eles valores padrão, mas não a outros, fatorar chaves frequentemente agrupadas e assim por diante.
Para mostrar o que quero dizer, vamos criar uma nova struct chamada SideInfo
e alterar a struct Student
da seguinte forma.
#[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>,
}
Fazendo isso, podemos atribuir valores Default
aos campos dentro de SideInfo
enquanto solicitamos a entrada do usuário para Student
. Isso é especialmente útil quando você tem muitos campos para os quais deseja atribuir valores padrão.
Vamos criar um novo 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 imprimir a string JSON dele:
{
"name": "dan",
"studentId": "1",
"sideInfo": {
"address": "47 street"
}
}
Como você pode ver, o campo de address
está aninhado dentro de sideInfo
. No entanto, ao adicionar o atributo flatten
ao campo side_info
dentro da struct Student
:
#[serde(skip_serializing_if="Option::is_none", flatten)]
pub side_info: Option<SideInfo>
Agora teremos:
{
"name": "dan",
"studentId": "1",
"address": "47 street"
}
Tag x Untag em enum
Digamos que temos um enum StudentList
como o seguinte:
enum StudentList {
Student1(Student),
Student2(Student)
}
Neste exemplo, na verdade, você não precisa ter o enum
, mas só para mostrar como usar marcação e renomeação, siga comigo.
Declare uma lista de estudantes:
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)];
Se imprimirmos o JSON como está agora, será assim. Está externamente marcado e é o comportamento padrão do serde
:
[
{
"Student1": {
"name": "tom",
"studentId": "J19990",
"pets": [],
"middleName": "middle"
}
},
{
"Student2": {
"name": "dan",
"studentId": "J19990",
"pets": [],
"middleName": "middle"
}
}
]
Mas e se você quiser que todas as tags tenham o mesmo nome, por exemplo, Student
? Você pode pensar que pode usar rename_all
para conseguir isso, mas, na verdade, não pode. Você terá que renomear manualmente cada variante dentro do enum
.
#[derive(Debug, Clone, Serialize, Deserialize)]
enum StudentList {
#[serde(rename="Student")]
Student1(Student),
#[serde(rename="Student")]
Student2(Student)
}
Isso nos dará a seguinte saída:
[
{
"Student": {
"name": "tom",
"studentId": "J19990",
"pets": [],
"middleName": "middle"
}
},
{
"Student": {
"name": "dan",
"studentId": "J19990",
"pets": [],
"middleName": "middle"
}
}
]
Untagged
E se quisermos apenas um array simples de estudantes sem mostrar o nome da variante do enum
? Podemos conseguir isso adicionando o atributo #[serde(untagged)]
ao enum. Ao fazer isso, nossa saída se tornaria:
[
{
"name": "tom",
"studentId": "J19990",
"pets": [],
"middleName": "middle"
},
{
"name": "dan",
"studentId": "J19990",
"pets": [],
"middleName": "middle"
}
]
Marcado internamente
Outra possível representação de um enum
seria marcado internamente.
Vamos criar um novo enum
que mantém diferentes tipos de estudantes. Teremos alunos Leader
, Subleader
e 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á ter a tag identificando qual variante estamos tratando dentro do conteúdo, ao lado de quaisquer outros campos da variante, como mostrado abaixo.
[
{
"type": "leader",
"name": "tom",
"studentId": "J19990",
"pets": [],
"middleName": "middle"
},
{
"type": "regular",
"name": "dan",
"studentId": "J19990",
"pets": [],
"middleName": "middle"
}
]
Marcado de forma adjacente
Esta é a sintaxe desejada/comum no mundo Haskell, onde a tag e o conteúdo são adjacentes um ao outro como dois campos dentro do mesmo objeto.
Mudar os atributos do enum
para o seguinte:
#[serde(tag = "type", content = "student", rename_all = "camelCase")]
nos fornecerá:
[
{
"type": "leader",
"student": {
"name": "tom",
"studentId": "J19990",
"pets": [],
"middleName": "middle"
}
},
{
"type": "regular",
"student": {
"name": "dan",
"studentId": "J19990",
"pets": [],
"middleName": "middle"
}
}
]
Há muito mais que você pode fazer com o Serde. Se estiver interessado, vá verificar os sites oficiais deles para mais informações!
Obrigado por ler! Boa programação!
Artigo escrito por Itsuki. Traduzido por Marcelo Panegali.
Latest comments (0)