paild tech blog

法人カードのクラウド型発行・管理サービスpaild

serdeを理解する –– 基本編

お手伝いの yuki です。

今日は serde の話です。最近仕事で serde を使っていて「これはバグでは??」と思った挙動がありました。具体的には untagged というものなのですが、同僚に「これはバグではなく仕様」というコメントをもらいました。たしかにドキュメントを見てみると注意書き等書いてあり、そういえば serde をなんとなく使っていたなと思いました。というわけで今日は記事を書くことで serde の理解を深めるということをしたいと思います。

なお、この記事はドキュメント以上のことはあまり書かれないと思います。実務的にどう利用しているかという観点から、このドキュメントをもとに記事として再編集することを目指しているためです。serde のドキュメントは下記にあります。

serde.rs

目次

まず、読み方

よく聞くのが serde をそもそもどう読むかという問題です。日本語圏特有の問題かと思いきや、実は英語圏でもたびたび問題になるようです。私は普段は日中英語で仕事をして暮らしているのですが、同僚たちの発音の仕方から大きく2つに分かれるなと思っています。

  1. rを伸ばす人: サーディ、サーデ、サード
  2. rを発音する人: セルディ、セルデ

アメリカやイギリスなど英語圏の人は前者で発音してそうです。「サード」は違和感あるかも、と言っている人はいましたが、一応聞くみたいです。日本の人や日本で働いている経験が長い海外出身の同僚は「セルデ」と読んでそうです。私個人はというと、英語で話すときは「サーディ」と読むようにして、日本語で話すときは「セルデ」と読んでるかなと思います。正直どちらでもいいと思います。が、英語圏向けには「サーディ」と読んでおくのが無難そうです。

serde とは何か?

Rust で JSON を扱う際、あまりにも空気や水のように当たり前に使っているので忘れがちなのですが、そもそも serde というのはデータのシリアライゼーションとデシリアライゼーションを扱うクレートです。この「データの」というのがポイントで、対象は JSON には限らないということです。たとえば YAML や TOML も対応したクレートがありますし、 MessagePack や Avro といったデータフォーマットにも対応できます。最近見つけた変わり種は AWS の DynamoDB の Item に対応したものや S 式もあるという点でしょうか。

serde を作ったのは David Tolnay 氏(通称 dtolnay)という方です。この方は Rust を使ったソフトウェアの開発において欠かせないクレートをいくつも作っており、この人がモチベーションを失うと Rust のエコシステムが大変なことになるレベルで重要な方です。非常に Rust への理解が深いことでも有名で、Rust Quiz というサイトを作っています。

dtolnay.github.io

基本

基本的な使い方

ペイルド含め HTTP をしゃべるアプリケーションを実装する際によく利用するであろう、JSON のパースを例に serde を読み解いていきます。クレートの追加の仕方、初歩的な利用方法について解説します。構造体から JSON への変換を説明したあと、実は serde は抽象実装の提供にとどまっており、データフォーマットごとに「使う側」による制御がなされるのみである、という点を確認します。

たとえば下記のような JSONシリアライズ、デシリアライズ[*1]したいものとします。serde のよさを説明するために、あえてネストしたオブジェクトをもつ JSON を扱っています。

{
  "field_a": "Hello, world!",
  "nested": {
    "field_b": 42,
    "field_c": true
  }
}

これを Rust の構造体に変換します。serde を自身のワークスペースに追加しましょう。

$ cargo add serde -F derive

適当なファイルを作って構造体を用意します。lib.rs などなんでもよいです。

//! lib.rs
use serde::{Deserialize, Serialize};

#[derive(Debug, Serialize, Deserialize)]
pub struct BasicParsing {
    field_a: String,
    nested: NestedStruct,
}

#[derive(Debug, Serialize, Deserialize)]
pub struct NestedStruct {
    field_b: i32,
    field_c: bool,
}

ここまでで、一通り「シリアライズ」と「デシリアライズ」を行える構造体が用意できました。では、JSON に変換させてみましょう。JSON とのやりとりをさせるためには、「serde_json」というクレートを利用します。

$ cargo add serde_json

下記のようにテストを書いてみます。「serialize_json」でシリアライズ(Rust 構造体→JSON)を、「deserialize_json」でデシリアライズJSON → Rust 構造体)をテストしています。

//! lib.rs
// 先ほどのコードの続き

#[test]
fn serialize_json() {
    let basic_struct = BasicStruct {
        field_a: "Hello, world!".to_string(),
        nested: NestedStruct {
            field_b: 42,
            field_c: true,
        },
    };
    let json = serde_json::to_string(&basic_struct).unwrap();
    assert_eq!(
        json,
        r#"{"field_a":"Hello, world!","nested":{"field_b":42,"field_c":true}}"#
    );
}

#[test]
fn deserialize_json() {
    let json =
        serde_json::json!({"field_a":"Hello, world!","nested":{"field_b":42,"field_c":true}});
    let basic_struct: BasicStruct = serde_json::from_value(json).unwrap();
    let expect = BasicStruct {
        field_a: "Hello, world!".to_string(),
        nested: NestedStruct {
            field_b: 42,
            field_c: true,
        },
    };

    assert_eq!(basic_struct, expect);
}

まず、構造体は設計通りきちんと変換されているようです。ネストした構造体も期待通りJSONシリアライズ、そしてJSONからデシリアライズできていることがわかります。ネストされた構造体にもきちんと「Serialize」と「Deserialize」を derive させておく必要がある点に注意ですが、ついていない場合はコンパイルエラーとして検出されます。ちなみにですが何重にもネストできます。

余談ですが serde_json::json! マクロは非常に便利で、ここにそのまま JSON をぺたっと貼り付けると、なんとバリデーションチェックまで込みで JSON オブジェクトをチェックしてくれます。もちろん素の文字列から JSON に起こす機構も用意されているのですが(serde_json::from_str)、こちらのマクロは JSON の形式が正しいかまでチェックしてくれるため大変便利です。

serde のおもしろいところは、「derive」を通じて「Serialize」と「Deserialize」のトレイトを与えておくと、あとはどんな形式に変換するかは構造体側で管理する必要がないという点です。要するに「この構造体は別のデータフォーマットに変換される」ということをトレイトで示してはおくものの、その示す先は構造体それ自体の実装からは完全に切り離されているということです。これにより、簡単に出力対象のデータフォーマットのスイッチングを行えます。

たとえば YAML として出力するためには、「serde_yaml」というクレートを使って同じように書くだけです。試してみましょう。

serde_yaml を依存に追加します。

$ cargo add serde_yaml

テストコードに serde_yaml を用いたものを書いてみましょう。

//! lib.rs
// JSON のテストコードの続き

#[test]
fn serialize_yaml() {
    let basic_struct = BasicStruct {
        field_a: "Hello, world!".to_string(),
        nested: NestedStruct {
            field_b: 42,
            field_c: true,
        },
    };
    let yaml = serde_yaml::to_string(&basic_struct).unwrap();
    assert_eq!(
        yaml,
        r#"field_a: Hello, world!
nested:
  field_b: 42
  field_c: true
"#
    );
}

#[test]
fn deserialize_yaml() {
    let basic_struct: BasicStruct = serde_yaml::from_str(
        r#"field_a: Hello, world!
nested:
  field_b: 42
  field_c: true
"#,
    )
    .unwrap();
    let expect = BasicStruct {
        field_a: "Hello, world!".to_string(),
        nested: NestedStruct {
            field_b: 42,
            field_c: true,
        },
    };

    assert_eq!(basic_struct, expect);
}

テストを動かしてみると、実際にきちんと動いていることを確かめられます。他にも toml などでぜひ試してみてください。

まとめ

ここまでをまとめておきましょう。

  • serde はシリアライズ・デシリアライズに関する抽象実装を提供するのみである。
  • どういうデータフォーマットに変換するかは、使う側で専用のクレートを使って切り替える。JSON を利用する際には serde_jsonYAML を利用する際には serde_yaml などの専用のクレートを追加で利用する。

次回は serde の応用的な利用方法を見ていきます。とくに enum の解釈は現実のアプリケーションでの利用時に重要になってくるので、重点的に見ていく予定です。

*1:なおシリアライズという場合は、Rust の構造体の表現から JSON の表現に変換することを意味します。また、デシリアライズという場合は、JSON の表現を Rust の構造体の表現へ変換することを意味します。