LoginSignup
37
24

More than 3 years have passed since last update.

serdeのかゆいところに手が届くserde_with

Posted at

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は人間が読み書きしやすいよう、DisplayFromStrを実装して文字列との相互変換が可能だとします。PhoneEmailかの区別はプレフィックスで行います。つまり、次のような動作になります。

#[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がデフォルトで実装する列挙型のシリアライズ方式になっています。これは、ContactSerialize/Deserializeをderiveで実装しているためです。これでも悪くはありませんが、せっかくContactDisplayを実装しているので、こちらに合わせた表現法にしたいところです。ContactSerializeDeserializeを自前で実装しても良いのですが、Contactは別の場所でも使っており、デフォルトのSerialize/Deserializeの実装は変えたくないとします。その場合、2つの解決策が考えられます。

serdeの属性を使う

serdeでは構造体のフィールドにserialize_withdeserialize_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型ですが、連絡先を複数持つように変更する場合を考えます。当然contactVecにするわけですが、この場合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クレートでかなり補えるので、覚えていて損はないでしょう。

37
24
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
37
24