WEB3DEV

Cover image for Guia Detalhado para Serialização e Desserialização com o Serde em Rust
Panegali
Panegali

Posted on

Guia Detalhado para Serialização e Desserialização com o Serde em Rust

À 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() };
Enter fullscreen mode Exit fullscreen mode

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"

}
Enter fullscreen mode Exit fullscreen mode

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,

}
Enter fullscreen mode Exit fullscreen mode

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,

}
Enter fullscreen mode Exit fullscreen mode

Ambos os métodos produzirão a seguinte saída:

{

  "name": "tom",

  "studentId": "J19990"

}
Enter fullscreen mode Exit fullscreen mode

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,

}
Enter fullscreen mode Exit fullscreen mode

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,

}
Enter fullscreen mode Exit fullscreen mode

Ao fazer isso, nosso objeto JSON se tornará:

{

  "name": "tom",

  "studentId": "J19990",

  "age": 123

}
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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"

}
Enter fullscreen mode Exit fullscreen mode

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>,
Enter fullscreen mode Exit fullscreen mode

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"

}
Enter fullscreen mode Exit fullscreen mode

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>,

}
Enter fullscreen mode Exit fullscreen mode

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()})};
Enter fullscreen mode Exit fullscreen mode

e imprimir a string JSON dele:

{

  "name": "dan",

  "studentId": "1",

  "sideInfo": {

    "address": "47 street"

  }

}
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

Agora teremos:

{

  "name": "dan",

  "studentId": "1",

  "address": "47 street"

}
Enter fullscreen mode Exit fullscreen mode

Tag x Untag em enum

Digamos que temos um enum StudentList como o seguinte:

enum StudentList {

    Student1(Student),

    Student2(Student)

}
Enter fullscreen mode Exit fullscreen mode

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)];
Enter fullscreen mode Exit fullscreen mode

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"
    }
  }
]
Enter fullscreen mode Exit fullscreen mode

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)

}
Enter fullscreen mode Exit fullscreen mode

Isso nos dará a seguinte saída:

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

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"
  }
]
Enter fullscreen mode Exit fullscreen mode

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),

}
Enter fullscreen mode Exit fullscreen mode

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"

  }

]
Enter fullscreen mode Exit fullscreen mode

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")]
Enter fullscreen mode Exit fullscreen mode

nos fornecerá:

[
  {
    "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

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.

Top comments (0)