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 トレイトにはそれぞれの型用にそれぞれのデフォルト実装があり、デフォルト実装ではエラーを返します。今回、RightIdvisit_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
ただし、今回のような場合は 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 は定番のトレイトとして使われ続けており、そのようなクレートには一般的なケースを簡単に解決する実装がすでにある場合があります。
使う前には説明書をちゃんと読もうと反省しました。