Rustのserdeは非常に強力なフレームワークですが、重用していると時折serdeの標準機能では対応できないケースに出くわします。そんな時に便利なserde_withクレートをご紹介します。
Display/FromStrとの連携
誰かの連絡先を管理するのに、次のようなContact
列挙型があるとします。Phone
なら電話番号、Email
ならメールアドレスが格納されます。
#[derive(Clone, PartialEq, Eq, Debug, Hash, Serialize, Deserialize)]
pub enum Contact {
Phone(String),
Email(String),
}
Contact
は人間が読み書きしやすいよう、Display
とFromStr
を実装して文字列との相互変換が可能だとします。Phone
かEmail
かの区別はプレフィックスで行います。つまり、次のような動作になります。
#[test]
fn contact_test() {
// 文字列への変換
assert_eq!(
Contact::Phone("0123-45-6789".into()).to_string(),
"phone:0123-45-6789"
);
assert_eq!(
Contact::Email("taro@example.com".into()).to_string(),
"email:taro@example.com"
);
// 文字列からのパース
assert_eq!(
"phone:0123-45-6789".parse(),
Ok(Contact::Phone("0123-45-6789".into()))
);
assert_eq!(
"email:taro@example.com".parse(),
Ok(Contact::Email("taro@example.com".into()))
);
}
さて、このContact
型を使ってPerson
構造体を定義します。
#[derive(Debug, Serialize, Deserialize)]
struct Person {
name: String,
contact: Contact,
}
このPerson
にはSerialize
をderiveで実装しているので、serde_jsonでjsonに変換してあげることができます。
let person = Person {
name: "Taro".into(),
contact: Contact::Email("taro@example.com".into()),
};
let s = serde_json::to_string(&person).unwrap();
println!("{}", s);
これの実行結果は次のようになります。
{"name":"Taro","contact":{"Email":"taro@example.com"}}
contact
フィールドの値が{"Email":"taro@example.com"}
となっており、serdeがデフォルトで実装する列挙型のシリアライズ方式になっています。これは、Contact
がSerialize
/Deserialize
をderiveで実装しているためです。これでも悪くはありませんが、せっかくContact
はDisplay
を実装しているので、こちらに合わせた表現法にしたいところです。Contact
にSerialize
とDeserialize
を自前で実装しても良いのですが、Contact
は別の場所でも使っており、デフォルトのSerialize
/Deserialize
の実装は変えたくないとします。その場合、2つの解決策が考えられます。
serdeの属性を使う
serdeでは構造体のフィールドにserialize_with
とdeserialize_with
を指定して、シリアライズ/デシリアライズを担当する関数を指定することができます。
#[derive(Debug, Serialize, Deserialize)]
struct Person {
name: String,
#[serde(serialize_with = "contact_serde")] // シリアライズに使う関数を指定
contact: Contact,
}
// 自前のシリアライズを行う関数
fn contact_serde<S: serde::Serializer>(contact: &Contact, s: S) -> Result<S::Ok, S::Error> {
s.serialize_str(&contact.to_string()) // to_stringはDisplayに基づき文字列へ変換する
}
fn main() {
let person = Person {
name: "Taro".into(),
contact: Contact::Email("taro@example.com".into()),
};
let s = serde_json::to_string(&person).unwrap();
println!("{}", s);
}
この実行結果は以下のようになります。
{"name":"Taro","contact":"email:taro@example.com"}
というわけで、Contact
に実装されたDisplay
の通りcontact
フィールドの値が設定されました。ここでは省略しましたが、deserialize_with
を指定すればこのJSONを元のContact
へデシリアライズすることもできるでしょう。
serde_with/DisplayFromStr を使う
serdeの標準機能だと、自前でシリアライズ/デシリアライズする関数を記述する必要がありますが、serde_with
はそこを含めてクレートが提供してくれます。使い方は、対象となる構造体の前に#[serde_as]
と属性を書き、対象のフィールドの前に#[serde_as(as = "...")]
のように対応するシリアライズ方法を記述するというものです。シリアライズをDisplay
に、デシリアライズをFromStr
を使って行いたい場合は、その名の通りDisplayFromStr
を指定します。
use serde_with::{serde_as, DisplayFromStr};
#[serde_as]
#[derive(Debug, Serialize, Deserialize)]
struct Person {
name: String,
#[serde_as(as = "DisplayFromStr")]
contact: Contact,
}
fn main() {
let person = Person {
name: "Taro".into(),
contact: Contact::Email("taro@example.com".into()),
};
let s = serde_json::to_string(&person).unwrap();
println!("JSON:\n{}", s);
let json = r#"{"name":"Hanako","contact":"email:hanako@example.com"}"#;
let person: Person = serde_json::from_str(json).unwrap();
println!("Rust構造体:\n{:?}", person);
}
シリアライズ・デシリアライズ両方を自動的に定義してくれるので、JSONとの相互変換も可能です。この実行結果は次のようになります。
JSON:
{"name":"Taro","contact":"email:taro@example.com"}
Rust構造体:
Person { name: "Hanako", contact: Email("hanako@example.com") }
コレクション型と使う
Vec
先程のPerson
型ですが、連絡先を複数持つように変更する場合を考えます。当然contact
はVec
にするわけですが、この場合serdeのserialize_with
だとうまく表現できません1。このようなコレクション型と組み合わせて使う時、serde_withだと直感的に使うことができます。対応するフィールドに与える属性を#[serde_as(as = "Vec<DisplayFromStr>")]
とするだけです。
use serde_with::{serde_as, DisplayFromStr};
#[serde_as]
#[derive(Debug, Serialize, Deserialize)]
struct Person {
name: String,
#[serde_as(as = "Vec<DisplayFromStr>")]
contact: Vec<Contact>,
}
fn main() {
let person = Person {
name: "Taro".into(),
contact: vec![
Contact::Email("taro@example.com".into()),
Contact::Phone("0123-45-6789".into()),
],
};
let s = serde_json::to_string(&person).unwrap();
println!("JSON:\n{}", s);
let json = r#"{"name":"Hanako","contact":["email:hanako@example.com"]}"#;
let person: Person = serde_json::from_str(json).unwrap();
println!("Rust構造体:\n{:?}", person);
}
この実行結果は以下の通りとなります。
JSON:
{"name":"Taro","contact":["email:taro@example.com","phone:0123-45-6789"]}
Rust構造体:
Person { name: "Hanako", contact: [Email("hanako@example.com")] }
HashMap
Contact
型をキーとしたHashMap
をJSONから読み込みたいとします。JSONは文字列しかキーにできないので、serdeのデフォルト実装ではContact
をキーとしたHashMap
はJSONで表現することができません。serde_withを使えば、HashMap
のキーにContact
を利用することができます。
use serde_with::{serde_as, DisplayFromStr};
use std::collections::HashMap;
#[serde_as]
#[derive(Debug, Serialize, Deserialize)]
struct ContactList(#[serde_as(as = "HashMap<DisplayFromStr, _>")] HashMap<Contact, String>);
fn main() {
let json = r#"
{
"email:taro@example.com": "boss",
"phone:0123-45-6789": "friend"
}"#;
let list: ContactList = serde_json::from_str(json).unwrap();
println!("{:?}", list);
}
この実行結果は以下のようになります。
ContactList({Email("taro@example.com"): "boss", Phone("0123-45-6789"): "friend"})
独自のシリアライズ/デシリアライズ方法を実装する
これまで用いてきたDisplayFromStr
ですが、これはSerializeAs
トレイトとDeserializeAs
トレイトを実装した構造体(unit struct)です。これらのトレイトを実装した独自の構造体を定義すれば、DisplayFromStr
と同様に、serde_as
と合わせて使うことで独自のシリアライズ/デシリアライズ方法を定義することができます。
例として、u8
型を漢数字の文字列としてシリアライズ/デシリアライズするKansuji
型を作ってみます。
use serde_with::{serde_as, DeserializeAs, SerializeAs};
struct Kansuji;
impl SerializeAs<u8> for Kansuji {
fn serialize_as<S>(source: &u8, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
let s = match *source {
1 => "一",
2 => "ニ",
3 => "三",
4 => "四",
5 => "五",
6 => "六",
7 => "七",
8 => "八",
9 => "九",
10 => "十",
11 => "十一",
12 => "十二",
_ => todo!(), // 13以上は無視する手抜き実装
};
serializer.serialize_str(s)
}
}
impl<'de> DeserializeAs<'de, u8> for Kansuji {
fn deserialize_as<D>(deserializer: D) -> Result<u8, D::Error>
where
D: serde::Deserializer<'de>,
{
let s = String::deserialize(deserializer).map_err(serde::de::Error::custom)?;
let i = match s.as_ref() {
"一" => 1,
"ニ" => 2,
"三" => 3,
"四" => 4,
"五" => 5,
"六" => 6,
"七" => 7,
"八" => 8,
"九" => 9,
"十" => 10,
"十一" => 11,
"十二" => 12,
_ => todo!(), // 13以上は無視する手抜き実装
};
Ok(i)
}
}
#[serde_as]
#[derive(Debug, Serialize, Deserialize)]
struct Date {
#[serde_as(as = "Kansuji")]
month: u8,
#[serde_as(as = "Kansuji")]
day: u8,
}
fn main() {
let date = Date {
month: 1,
day: 1,
};
let s = serde_json::to_string(&date).unwrap();
println!("JSON:\n{}", s);
let json = r#"{"month":"八","day":"八"}"#;
let date: Date = serde_json::from_str(&json).unwrap();
println!("Rust構造体:\n{:?}", date);
}
この実行結果は以下のようになります。
JSON:
{"month":"一","day":"一"}
Rust構造体:
Date { month: 8, day: 8 }
先述のコレクション型との連携と合わせると、かなり柔軟にserdeを使いこなせるようになります。
最後に
serdeをうまく使えるようになれば、JSON等各種フォーマットの取り扱いが格段に楽になります。serdeだけだと上手く表現できない場合も、このserde_withクレートでかなり補えるので、覚えていて損はないでしょう。