serdeにまつわる3つの小話

serdeにまつわる3つの小話

こんにちは、OPTiM TECH BLOG Advent Calendar 2020 12/23の記事をR&Dチームの齋藤(@aznhe21)からお送りします。

Rustでプログラムを書いたことのある人なら、serdeクレートは誰もが使ったことがあるのではないかと思います。 実際crates.ioの統計情報を見ると3000万回以上ダウンロード(2020/12/23現在)されており、その人気さが分かります。

今回はそのserdeについてのちょっとしたTips的な小話をしていきます。

f:id:optim-tech:20201223131241p:plain

空のデータを表現するたったひとつの冴えないやり方

Rustにはデータを持たない「空」の構造体を定義する構文がいくつかあります。

// ユニット構造体
struct Hoge;
// タプル構造体
struct Fuga();
// 通常の構造体
struct Piyo {}

どれもが同じ「空」の構造体なので、シリアライズ結果も同じになるでしょうか? 実際に見てみましょう。

use serde::Serialize;

#[derive(Serialize)]
struct Hoge;

#[derive(Serialize)]
struct Fuga();

#[derive(Serialize)]
struct Piyo {}

fn main() {
    println!("{}", serde_json::to_string(&Hoge).unwrap());
    // null

    println!("{}", serde_json::to_string(&Fuga()).unwrap());
    // []

    println!("{}", serde_json::to_string(&Piyo {}).unwrap());
    // {}
}

どう見ても別の型です。本当にありがとうございました。
と言うわけで「空のデータ」を表現する場合は波括弧を使う「通常の構造体」を使いましょう。

ただ、struct Fuga()が配列スタイルでシリアライズされるのが納得いかないのでもう少し検証してみます。

Fugaのような記法の構造体は、内部に値を1つ、あるいは複数持つことも出来ます。 見てみましょう。

use serde::Serialize;

#[derive(Serialize)]
struct S0();

#[derive(Serialize)]
struct S1(u8);

#[derive(Serialize)]
struct S2(u8, u8);

#[derive(Serialize)]
struct S3(u8, u8, u8);

fn main() {
    println!("{}", serde_json::to_string(&S0()).unwrap());
    // []

    println!("{}", serde_json::to_string(&S1(0)).unwrap());
    // 0

    println!("{}", serde_json::to_string(&S2(0, 1)).unwrap());
    // [0,1]

    println!("{}", serde_json::to_string(&S3(0, 1, 2)).unwrap());
    // [0,1,2]
}

おや、S1だけ様子がおかしいですね。 実は、内部に1つだけ値を持つ「タプル構造体」はタプル構造体ではなく「newtype構造体」です。 タプルの記法である丸括弧でも、値が1つの場合はタプルではなく内部の値そのものとなるのと似ています。

つまり、newtype構造体は内部の値がそのまま、タプル構造体は配列として値が出力されると言うことです。

バイナリデータにはserde_bytesを使う

serdeでは「数値の配列」と「バイト列」を区別するため、データをバイト列として出力するにはserde_bytesというクレートを使い、 そのデータがバイト列であることを明示する必要があります。

use serde::Serialize;

#[derive(Serialize)]
struct ByteArray {
    // これだと「数値の配列」
    data: Vec<u8>,
}

#[derive(Serialize)]
struct Binary {
    // これだと「バイト列」
    #[serde(with = "serde_bytes")]
    data: Vec<u8>,
}

fn main() {
    let array = ByteArray {
        data: vec![0, 1, 2],
    };
    println!("{}", serde_qs::to_string(&array).unwrap());
    // シリアライズ結果は配列
    // data[0]=0&data[1]=1&data[2]=2

    let bin = Binary {
        data: vec![0, 1, 2],
    };
    println!("{}", serde_qs::to_string(&bin).unwrap());
    // シリアライズ結果はパーセントエンコードされたバイナリデータ
    // data=%00%01%02
}

JSONとMessagePackでバイト列の表現を変える方法

JSONでは画像データなどのバイナリデータは表現できません。 これをやるには多くの場合Base64によってデータを表現すると思います。 ただ、これではBase64の変換処理が発生して面倒な上、サイズが大きく膨らんでしまってメモリや帯域を多く消費してしまいます。

{
  // imageは160バイトのJPEG画像
  // Base64でエンコードした結果216バイトに膨れてしまった
  "image": "/9j/4AAQSkZJRgABAQAAAQABAAD/2wBDAAMCAgICAgMCAgIDAwMDBAYEBAQEBAgGBgUGCQgKCgkICQkKDA8MCgsOCwkJDRENDg8QEBEQCgwSExIQEw8QEBD/wAALCAABAAEBAREA/8QAFAABAAAAAAAAAAAAAAAAAAAACf/EABQQAQAAAAAAAAAAAAAAAAAAAAD/2gAIAQEAAD8AVN//2Q=="
}

これを防ぐため、JSONと併せてMessagePackというバイナリ形式のデータを用いることがあります。 MessagePackであれば画像のようなバイナリデータを表現でき、サイズが大きく膨らむことはありません (もちろんMessagePackでなくともCBORやFlexBuffersなど他のバイナリなデータ形式でも構いません)。

この実装を考えたとき、JSONとMessagePackで異なるデータ型を定義する方法が考えられます。

// JSONでのデータ構造
#[derive(Serialize, Deserialize)]
struct JSONImage {
    // imageはBase64にエンコードされた画像
    image: String,
}

// MessagePackでのデータ構造
#[derive(Serialize, Deserialize)]
struct MPImage {
    // imageは画像データそのもの
    #[serde(with = "serde_bytes")]
    image: Vec<u8>,
}

ただ、同じデータに異なる構造があるのは保守性に欠けます。 これを統一するには、SerializerトレイトDeserializerトレイト にそれぞれあるis_human_readableというメソッドを使って分岐すると良いでしょう。

is_human_readableはデータ形式が人間に読み取れるものかどうかを返すメソッドで、 JSONように人間が読める形式であればtrue、MessagePackのように人間が読めない形式であればfalseを返すことが期待できます。

なお、このメソッドを上手く扱ってくれるクレートはまだないようなので自作します。

use serde::ser;

/// 可読フォーマットならBase64、それ以外ではバイト列で(デ)シリアライズするデータ型。
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
pub struct HumanBytes(pub Vec<u8>);

impl ser::Serialize for HumanBytes {
    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
    where
        S: ser::Serializer,
    {
        if serializer.is_human_readable() {
            // データ形式が人間に読み取れるものであればBase64としてシリアライズ
            let base64 = base64::display::Base64Display::with_config(&*self.0, base64::STANDARD);
            serializer.collect_str(&base64)
        } else {
            // データ形式は人には読めない、ということはバイナリデータを埋め込んでヨシ
            serializer.serialize_bytes(&*self.0)
        }
    }
}

デシリアライズ部分は長くなってしまうため折りたたみます。

デシリアライズ用コード

impl<'de> de::Deserialize<'de> for HumanBytes {
    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
    where
        D: de::Deserializer<'de>,
    {
        if deserializer.is_human_readable() {
            deserializer.deserialize_str(B64Visitor)
        } else {
            deserializer.deserialize_bytes(BinVisitor)
        }
    }
}

struct B64Visitor;

impl<'de> de::Visitor<'de> for B64Visitor {
    type Value = HumanBytes;

    fn expecting(&self, f: &mut fmt::Formatter) -> fmt::Result {
        f.write_str("base64 ASCII text")
    }

    fn visit_str<E>(self, v: &str) -> Result<Self::Value, E>
    where
        E: de::Error,
    {
        base64::decode(v).map(HumanBytes).map_err(de::Error::custom)
    }
}

struct BinVisitor;

impl<'de> de::Visitor<'de> for BinVisitor {
    type Value = HumanBytes;

    fn expecting(&self, f: &mut fmt::Formatter) -> fmt::Result {
        f.write_str("binary data")
    }

    fn visit_bytes<E>(self, v: &[u8]) -> Result<Self::Value, E>
    where
        E: de::Error,
    {
        Ok(HumanBytes(v.to_vec()))
    }
}

さて、MessagePackを扱うにはrmp-serdeというクレートを使うわけですが、ここで1つ注意が必要です。 なんと、現在リリースされているrmp-serdeクレートはis_human_readableメソッドには対応していません(2020/12/23現在)。 現在開発中のバージョンであれば対応しているため、必要に応じてレポジトリを直接参照してください。

[dependencies]
rmp-serde = { git = "https://github.com/3Hren/msgpack-rust.git", rev = "ed72145" }

さいごに

serdeは便利ですが、凝ったことをしようとすると沼にハマりますね。 とは言え、今回はマニアック過ぎただけで基本的には公式サイトを見れば大体の疑問は解消できると思います。

用法用量を守って楽しいserde生活を!

オプティムでは、データでは表せられないエンジニアを募集しています。

ライセンス表記

  • 冒頭の画像はいらすとやさんの画像を使っています。いつもありがとうございます