Rustで学術論文からテキストを抽出する #24 応用編 - arXiv論文収集システム構築 Appendix (1) serde

:::note
## Summary
- Rustの`serde`クレートは推せる
:::

今回のシステム実装で一番お世話になっているクレートがおそらく`serde`です．
あれこれ実装する中で，いくつかの`serde`の機能にも使い慣れてきたので，Appendixその1では`serde`の概要や使い方を改めて紹介しようと思います．

## `serde`クレート

改めて，[`serde`](https://serde.rs/)は以下のようなクレートです．

```text: Serde Overview
Serde is a framework for serializing and deserializing Rust data structures
efficiently and generically.

The Serde ecosystem consists of data structures that know how to serialize and
deserialize themselves along with data formats that know how to serialize and
deserialize other things. Serde provides the layer by which these two groups
interact with each other, allowing any supported data structure to be serialized
and deserialized using any supported data format.

---
Serdeは、Rustのデータ構造を効率的かつ汎用的にシリアライズおよびデシリアライズするための
フレームワークです。

Serdeエコシステムは、自身をシリアライズおよびデシリアライズする方法を知っているデータ構造と、
他のものをシリアライズおよびデシリアライズする方法を知っているデータフォーマットで構成されて
います。Serdeはこれらの2つのグループが相互にやり取りするためのレイヤーを提供し、サポート
されている任意のデータ構造を、サポートされている任意のデータフォーマットを使用してシリアライズ
およびデシリアライズできるようにします。
```

今回は`serde_json`しか使っていませんが，`serde`自体はJSONに限らず様々なフォーマットのSerialize/Deserializeに適用できます．
以下はOverviewで紹介されているリストの一部．

|フォーマット|crate|
|:-:|:-:|
|JSON|https://github.com/serde-rs/json|
|YAML|https://github.com/dtolnay/serde-yaml|
|TOML|https://docs.rs/toml|
|URL|https://docs.rs/serde_qs|
|CSV|https://docs.rs/csv|
|Pickle|https://github.com/birkenfeld/serde-pickle|

`Pickle`まであるのは驚きでした．
RustとPythonは使い所が違い，データ連携できると助かるケースもあるので，いずれ試してみたいです．

## 基本的な使い方 (`serde_json`)
ここからはJSON形式に絞って，まずは基本的な使い方を確認します．

### 構造体 ↔︎ JSON
`serde_json`のGitHubリポジトリに載っているコードほとんどそのままですが，構造体を定義してJSONテキストをDeserialzie→Serializeする処理は以下のようになります．

```rust
use serde::{Deserialize, Serialize};

// 構造体定義
#[derive(Serialize, Deserialize)]
struct Person {
    name: String,
    age: u8,
    phones: Vec<String>,
}

// JSONテキスト
let data = r#"
    {
        "name": "John Doe",
        "age": 43,
        "phones": [
            "+44 1234567",
            "+44 2345678"
        ]
    }"#;

// JSONテキスト→Rust構造体にDeserialize
let p: Person = serde_json::from_str(data)?;

// Rust構造体→JSONテキストにSerialize
let data = serde_json::to_string_pretty(&p);
println!("{}", data);
// {
//    "name": "John Doe",
//    "age": 43,
//    "phones": [
//        "+44 1234567",
//        "+44 2345678"
//    ]
// }

```

### Enum ↔︎ JSON
構造体だけでなく，`Enum`も使えます．
以下は[serdeのドキュメント](https://serde.rs/json.html)からのサンプル．

```rust
#[derive(serde::Serialize)]
enum E {
    W { a: i32, b: i32 },
    X(i32, i32),
    Y(i32),
    Z,
}

let w = E::W { a: 0, b: 0 };
println!("{}", serde_json::to_string(&w).unwrap());
// {"W":{"a":0,"b":0}}

let x = E::X(0, 0);
println!("{}", serde_json::to_string(&x).unwrap());
// {"X":[0,0]}

let y = E::Y(0);
println!("{}", serde_json::to_string(&y).unwrap());
// {"Y":0}

let z = E::Z;
println!("{}", serde_json::to_string(&z).unwrap());
// "Z"
```

## Tips

### Deserialize時にデフォルト値を与えたい

APIを操作していると，必ずしも完全な形のJSONが返ってこないことがよくあります．そんなときに，構造体としてフィールドだけは保持しつつ，Deserializeできなかった場合にはデフォルトで設定した値を格納できる機能があります．

以下のように実装します．

```rust
#[derive(Deserialize, Default)]
struct Page {
    #[serde(default = "String::new")]
    pub id: String,
    #[serde(default = "u32::default"]
    pub page_num: u32
}
```

`#[serde(default = "")]`にデフォルト値を返す関数を記載しておくことで，Deserializeできなかった場合に指定された関数を実行します．
この機能を知ってから，構造体に`#[derive(Default)]`を指定しておくことのありがたみに気がつきました．

`#[derive(Default)]`は，`Default`トレイトを実装してくれるもので，手動で実装する場合は以下のようになります．

```rust
impl Default for Page {
    fn default() -> Self {
        Page {
            ...
        }
    }
}
```

今回のシステム実装では，特殊なデフォルト値を返すケース以外は，`#[derive(Default)]`を実装しておけば大丈夫でした．

### JSONの`null`を扱いたい

JSONではよく返ってくる`null`ですが，Rustはそのままでは`null`を扱えません．
Rustでは`null`や`None`は`Option<>`を使って対処しますが，`serde_json`では`Option<>`を指定することで`null`のDeserializeに対応してくれます．

```rust
#[derive(Deserialize, Default)]
struct Page {
    pub id: String,
    pub page_num: u32,
    pub description: Option<String>,
}
```

上記のように`Option<>`を使ったフィールドを定義しておくと，`description: null`だった場合には`None`が設定されるようになっています．

難点としては，構造体のフィールドにアクセスするときに毎回`unwrap()`をかまさなくてはいけない点と，`Option<>`を`unwrap()`するため，所有権の移動が発生してしまい少々扱いが複雑になってしまう点でしょうか．

APIの実装で構造体のネストが深くなると`unwrap()`と`clone()`だけでコードが冗長に見えてしまうため，ここだけは改善したい点でした．

### Serialize時に特定のフィールドだけ除外したい

これは，APIでJSONを受け取るときにはフィールドとして持っておきたいが，JSONをPOSTするときには設定したくないフィールドがある場合に便利でした．
`created_time`のようなフィールドが該当します．
また，`Option<>`付きのフィールドで，`None`の場合にはSerializeしたくないというケースも結構ありました．

以下のように対応します．

```rust
#[derive(Deserialize, Default)]
struct Page {
    pub id: String,
    pub page_num: u32,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub description: Option<String>,
    #[serde(skip_serializing)]
    pub created_time: String
}
```

`skip_serializing_if`は特定の条件を満たした場合に当該フィールドをSerialize対象から除外する設定で，`Option<>`付きフィールドには基本的に設定するようにしていました．

`skip_serializing`は無条件にSerialize対象からフィールドを除外する設定です．

## その他

[serde.rsのExamplesページ](https://serde.rs/examples.html)には，上記以外にも様々な場合に対応できるSerialize/DeserializeのTipsが載っています．

## まとめ

`serde`の概要と今回のシステム実装で使用した機能を紹介しました．
Rustはサードパーティのクレートを前提としたコアライブラリの薄い言語ですが，`serde`の立ち位置は絶妙だと思います．
Rustの言語と同じ発想なのか，Serialize/Deserializeに関するコアな機能は提供しつつ，各フォーマットに対応する個別の処理は他のサードパーティ製クレートに処理を委譲しています．

Rustのサードパーティライブラリはまだまだ発展途上ですが，`serde`周辺のエコシステムは非常にバランスがよく使いやすいと思っています．いずれRustのエコシステムもこのように成長していってほしいです．

`serde`はリポジトリも積極的にメンテナンスされているので，安心して利用することができる点もポイント高いです．

まとめると，`serde`は推せる，と思いました．