Rust の JSON シリアライズ・デシリアライズを調整する

Rust の JSON シリアライズ・デシリアライズを調整する

ふとしです。

Rust での JSON シリアライズ・デシリアライズは serde と serde_json というクレートを使用するのが一般的です。

基本的に提供される derive を付与するだけで事足りますが、今回はプログラム内で使用される型の形状と JSON として形状が一致しないデータがあったため (実際は不要でしたが) Serialize トレイトと Deserialize トレイトを実装しました。

実装に関して少し特徴的だったので、シリアライズ・デシリアライズの作業メモとして日記に残します。

トレイトの実装

今回は以下のようなデータ構造に対する調整を行いました。

pub struct RightId<T> {
    id: u64,
    _p: PhantomData<T>,
}

単純なデータ構造ですが、今回は HashMap のキーとして使い、その HashMap をシリアライズ・デシリアライズしなければなりません。つまり JSON のオブジェクトのキーになるので、最終的に文字列にシリアライズできる必要があります。

derive でそのままシリアライズすると {"id":0,"_p":null} という形状になるので、オブジェクトのキーには使えません。そこで、どのように取り扱うかを独自に実装します。

Serialize

シリアライズのための Serialize トレイトの実装は平易です。

Deserialize もですが値を直接返しません。引数として渡される serializer が持つ任意の関数を使って値を渡します。

今回は単なる数値として扱われるように serialize_u64 で必要な値だけを渡します。

impl<T> Serialize for RightId<T> {
    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
    where
        S: Serializer,
    {
        serializer.serialize_u64(self.id)
    }
}

Deserialize

デシリアライズは少し複雑です。

Desirialize では「ソースから得た値がある型であった時にどのように扱うか」を実装した Visotor を引数で渡される deserialize に渡さなければなりません。

Visitor トレイトにはそれぞれの型用にそれぞれのデフォルト実装があり、デフォルト実装ではエラーを返します。今回、RightId が想定される位置に u64 が来た時のみデシリアライズが可能なので visit_u64 が Ok を返すようにします。(デフォルトでは u8, u16 などは visit_u64 を引くようになっているので考慮不要です)

deserializer に Visitor を渡して得られた u64 を使って RightId を作成すればデシリアライズの完了です。

struct RawIdVisitor;

impl<'de> Visitor<'de> for RawIdVisitor {
    type Value = u64;

    fn expecting(&self, formatter: &mut Formatter) -> std::fmt::Result {
        formatter.write_str("not id(u64)")
    }

    fn visit_u64<E>(self, v: u64) -> Result<Self::Value, E>
    where
        E: serde::de::Error,
    {
        Ok(v)
    }
}

impl<'de, T> Deserialize<'de> for RightId<T> {
    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
    where
        D: Deserializer<'de>,
    {
        let id = deserializer.deserialize_u64(RawIdVisitor)?;
        Ok(Self {
            id,
            _p: Default::default(),
        })
    }
}

これで無事 RightId から数値へ、数値から RightId でシリアライズ・デシリアライズが行えるようになりました。

ただし、今回のような場合は serde にビルトインされている機能を使えば Serialize と Desirialize を実装する必要はありません。

Serialize と Desirialize を実装する前に検討すること

シリアライズ・デシリアライズの両面での振る舞いは attribute で調整できる部分が多くあります。カスタムを考える前に、attribute の説明書を読んで検討しましょう。

skip

単純にフィールドが必要でない場合は skip が使えます。

pub struct RightId<T> {
    id: u64,

    #[serde(skip)]
    _p: PhantomData<T>,
}

シリアライズの際には JSON に含めず、デシリアライズの際は Default::default() でデータを復元するため、ファントムデータにはうってつけの attribute です。

rename

今回は関係ありませんが、特に外部の Web API を使う際には rename をよく使います。

pub struct Foo {
    #[serde(rename="type")]
    type_name: String,
}

type は予約語なので Rust のコード内ではフィールド名として使えませんが、データ構造には頻出のフィールド名です。そこで rename で扱いを変えることができます。

from, into

今回はこれが本当に必要だったものです。

個々のフィールドのみではなく構造体自体にも attribute を与えられます。from と into はそれぞれ From トレイトと Into トレイトを前提としたシリアライズとデシリアライズを行います。

#[derive(Serialize, Deserialize)]
#[serde(from = "u64", into = "u64")]
pub struct RightId<T> {
    id: u64,
    _p: PhantomData<T>,
}

impl<T> From<u64> for RightId<T> {
    fn from(id: u64) -> Self {
        Self {
            id,
            _p: Default::default(),
        }
    }
}

impl<T> Into<u64> for RightId<T> {
    fn into(self) -> u64 {
        self.id
    }
}

これでオブジェクトのキーとして問題なくシリアライズ・デシリアライズが行われました。

おわりに

serde は定番のトレイトとして使われ続けており、そのようなクレートには一般的なケースを簡単に解決する実装がすでにある場合があります。

使う前には説明書をちゃんと読もうと反省しました。