[Rust] Serdeのシリアライズ/デシリアライズを試してみる

福岡オフィスのyoshihitohです。

もうすぐRustのstable版でasync-awaitが使えるようになりますね。11月8日(米国時間で11月7日)が待ち遠しいです。

async-awaitが入ってくると、APIサーバーやカスタムランタイムLambdaでの採用事例もどんどん増えていくんじゃないかなと思います。いざ使うとなった時に困らないようにするため、いろんな機能を試していきます。

今回はRust用のシリアライゼーションフレームワークである Serde を試してみます。

検証環境

  • Rust: 1.38.0 (625451e37 2019-09-23)
  • serde: 1.0.101
  • serde_json: 1.0.41

Serdeとは

Rustのデータをシリアライズ/デシリアライズするためのフレームワークです。

Overview · Serde

Serdeはtraitベースでシリアライズ/デシリアライズを実現しています。

これらのtraitを実装すると任意のデータ形式でシリアライズ/デシリアライズできるようになります。traitは自身で実装することもできれば、 #[derive(..)] を使ってコンパイル時に自動生成することもできます。

コンパイル時に自動生成というのが特徴です。実行時の動的リフレクションといったオーバーヘッドは発生しません。

対応してるデータ形式

Serde自体は汎用的な仕組みになっていて、JSONやYAMLといった具体的な形式は専用のライブラリを使って表現します。 MessagePackCBOR といったバイナリ形式の表現にも対応しています。他にも様々な形式に対応しているのでぜひ公式のドキュメントを確認してみてください。

Data formats · Serde

試してみる

CX事業本部で作るモバイルアプリとAPIサーバーはJSON形式でやりとりする事が多いです。SerdeはJSON用のライブラリ serde-json が用意されているのでこれを使ってシリアライズ/デシリアライズを試してみます。

まず、 Cargo.toml の依存関係にSerdeを追加します。

[dependencies]
serde = { version = "^1.0.101", features = ["derive"] }
serde_json = "^1.0.41"

features = ["derive"] は後述の自動生成を利用するための指定です。

serde-json のREADMEを参考にして、以下の構造体を使います。

struct Person {
    name: String,
    age: u8,
    phones: Vec<String>,
}

次のRustのデータとJSON文字列を相互に変換できるように実装していきます。

Rustのデータ

let p = Person {
    name: "John Doe".into(),
    age: 44,
    phones: vec![
        "+44 1234567".into(),
        "+44 2345678".into(),
    ],
};

JSON

{
    "name": "John Doe",
    "age": 44,
    "phones": [
        "+44 1234567",
        "+44 2345678"
    ]
}

自分で実装する

Serialize/Deserializeトレイトを自分で実装してみます。

Serialize

まず Serialize トレイトを実装します。実装すべきメソッドは serialize メソッドだけです。

Serialize トレイトは Serializerserialize_xxx メソッドを使って構造体などのデータを Serdeのデータモデル にマッピングします。

今回は先述の Person 構造体をシリアライズするので、 serialize_struct メソッドを使います。

serialize_struct の呼び出しに成功すると SerializeStruct を取得できるので、これを使って各フィールドをシリアライズします。

フィールド名とその値を指定するだけのシンプルな作りです。直感的でわかりやすいですね。

Deserialize

次に Deserialize トレイトを実装します。実装すべきは deserialize メソッドだけなんですが、Serializeより少し複雑です。

Deserialize トレイトはシリアライズされたデータを DeserializerVisitor を組み合わせて Serdeのデータモデル にマッピングします。

ドキュメントのサンプル実装 だと deserialize メソッドの中に関連する実装を定義していますが、今回は理解を深めるためにばらばらに実装してみます。また、サンプルだとBincodeとJSONの両方に対応させるための実装が入っていますが、今回はJSONに絞った部分のみ実装します。

まず、フィールドを識別するためのenumを追加し、このenum用のDeserializeとVisitorを実装します。String でフィールド名を保持すると無駄なメモリ割り当てが生じるので、これを回避するためにenumを利用します。

JSONの場合は文字列でフィールドを識別するので、visit_str を実装します。実装内容は単純で、フィールド名に合致するenumに置き換えるだけです。フィールドの識別なので deserialize_identifier を呼び出します。

仕上げにPerson構造体のDeserializeとVisitorを実装します。

Visitorは visit_map メソッドを実装します。引数で受けたmapのキーと値から構造体の各フィールドの値を取得して Person インスタンスを構築します。

Deserializeは deserialize メソッドを実装します。構造体のデシリアライズなので、 deserialize_struct を呼び出します。

動作確認

ここまでに実装した内容が動作するか試してみます。ビルトインのテスト機能を使います。

テスト実行します。

$ cargo test
...
running 2 tests
test manual::tests::test_person_deserialize ... ok
test manual::tests::test_person_serialize ... ok

test result: ok. 2 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out

期待通りの動きになりました!

deriveで自動生成する

理解を深めるためにSerializeとDeserializeを自分で実装しましたが、構造体を追加するたびに毎回実装するのは大変です。またフィールドが増えるたびに修正が必要となると不具合の温床になりそうです。

deriveマクロを利用すると同等のコードを自動生成してくれます。通常はこの機能の任せてしまうと良いでしょう。公式ドキュメントによると自前実装が必要になるケースは非常に稀とのことです。

対応は非常に簡単で、 Person 構造体の定義に #[derive(...)] の指定を追加するだけです。Serialize/Deserializeの両方を指定する場合、以下のように変更します。

これだけです。めっちゃ簡単ですね。

おわりに

今回はRustのシリアライゼーションフレームワークのSerdeの基本機能を試してみました。他にも None の扱いや enum の扱いなど気になることがたくさんあるので他の機能も試していきたいと思います。