serdeの機能で様々な形態のJSONを列挙型として扱う
JSONはREST API呼び出し、データ保存、多言語との連携などに数多く使用されている。 一方で、JSONは言語でサポートされる表現が整数と浮動小数点数・文字列・配列・オブジェクト・そしてnull程度しかなく、それ以上に複雑な表現はこれらの基本機能を組み合わせて表現することになる。 機能の組み合わせ方には複数の方法があり、特に複数の型の構造体やクラスが混在する時の表現形式は複数ある。
Rustでは、表現したいデータ型が既知であれば、複数の型のうちどれかであることを表現するために列挙型が使える。 そして、Rustのシリアライザ・デシリアライザのライブラリであるserdeを用いて、列挙型とJSONの相互変換をすることができる。
列挙体の4種の表現
serdeで取り扱える列挙体の表現形式は4種類ある*1。 それぞれexternally tagged、internally tagged、adjacently tagged、untaggedの4種類が指定可能である。
以下の説明では、列挙体の構成子(variant)の4形式をそれぞれunit型構成子、newtype型構成子、tuple型構成子、struct型構成子と呼ぶ。 それぞれはRustのコードで以下のように書かれる。
enum Varints { Unit, Newtype(i64), Tuple(i64, i64), Struct{x: i64, y: i64}, }
以下のサンプルコードの完全なソースコードは以下のリポジトリにある。
GitHub - IgaguriMK/serde-enum-samples
Externally tagged (デフォルト)
externally taggedは、値の外側にオブジェクトのキーとしてタグが付いた形式である。 これは、2018年12月時点でserdeのデフォルト指定であり、何の指定も行わなかった場合この形式が使用される。
この形式は構成子を読んでから内容を読めること、JSON以外にも多くのフォーマットで利用可能なこと、すべてのパターンの構成子がうまく表現できることなどから、デフォルトの形式として選択されているようだ。 (tuple構成子は内容が配列として表現される)
{ "Variant": {"field1": "value1", "field2": "value2"}}
列挙型の宣言時に形式の指定を行わないことでこの形式を使用することができる。
#[derive(Debug, Serialize, Deserialize)] enum Member { Permanent { id: u64, name: String, nickname: Option<String>, }, SingleChannel { channel_id: u64, name: String, nickname: Option<String>, }, }
実際の動作例は以下の通り。
Rustでの値
[Permanent { id: 1, name: "Jebediah Kerman", nickname: Some("Jeb") }, Permanent { id: 2, name: "Bob Kerman", nickname: None }, SingleChannel { channel_id: 8, name: "Kamler Kerman", nickname: None }]
[{"Permanent":{"id":1,"name":"Jebediah Kerman","nickname":"Jeb"}},{"Permanent":{"id":2,"name":"Bob Kerman","nickname":null}},{"SingleChannel":{"channel_id":8,"name":"Kamler Kerman","nickname":null}}]
Internally tagged
internally taggedは、オブジェクトの中にタグとなるキーと値のペアが含まれる形式である。
{ "field1": "value1", "tag": "Variant", "field2": "value2"}
この形式を使用するには、列挙型の宣言にアトリビュートで #[serde(tag="tag")]
というようにタグとなるフィールドを指定する。
この形式はtuple構成子が含まれる場合には利用できず、unit構成子はタグだけが含まれるオブジェクトになる。
newtype構成子は、中身が構造体の場合のみ、その構造体が書かれたstruct構成子と同様に利用できる。
internally tagged形式を使う際には、列挙型に対応する構成子がないタグをすべてother指定した構成子にデシリアライズさせることができる(後述)。
#[derive(Debug, Serialize, Deserialize)] #[serde(tag = "type")] enum Member { Permanent { id: u64, name: String, nickname: Option<String>, }, SingleChannel { channel_id: u64, name: String, nickname: Option<String>, }, }
Rustでの値
[Permanent { id: 1, name: "Jebediah Kerman", nickname: Some("Jeb") }, Permanent { id: 2, name: "Bob Kerman", nickname: None }, SingleChannel { channel_id: 8, name: "Kamler Kerman", nickname: None }]
[{"type":"Permanent","id":1,"name":"Jebediah Kerman","nickname":"Jeb"},{"type":"Permanent","id":2,"name":"Bob Kerman","nickname":null},{"type":"SingleChannel","channel_id":8,"name":"Kamler Kerman","nickname":null}]
Adjacently tagged
adjacently tagged形式は、オブジェクトの中にタグとなるキーと値のペアと、内容となるキーと値のペアが含まれる形式である。 タグと内容が隣接している(adjacently)ためこの名前がついている。
{ "t": "Variant", "c": {"field1": "value1", "field2": "value2"}}
この形式を使用するには、列挙型の宣言にアトリビュートで #[serde(tag="t", content="c")]
というようにタグとなるフィールドと内容となるフィールドを指定する。
この形式はすべての種類の構成子に使用できる。
adjacently tagged形式においても、列挙型に対応する構成子がないタグをすべてother指定した構成子にデシリアライズさせることができる(後述)。
#[derive(Debug, Serialize, Deserialize)] #[serde(tag = "t", content = "c")] enum Member { Permanent { id: u64, name: String, nickname: Option<String>, }, SingleChannel { channel_id: u64, name: String, nickname: Option<String>, }, }
Rustでの値
[Permanent { id: 1, name: "Jebediah Kerman", nickname: Some("Jeb") }, Permanent { id: 2, name: "Bob Kerman", nickname: None }, SingleChannel { channel_id: 8, name: "Kamler Kerman", nickname: None }]
[{"t":"Permanent","c":{"id":1,"name":"Jebediah Kerman","nickname":"Jeb"}},{"t":"Permanent","c":{"id":2,"name":"Bob Kerman","nickname":null}},{"t":"SingleChannel","c":{"channel_id":8,"name":"Kamler Kerman","nickname":null}}]
Untagged
untagged形式は、JSONにタグを含まない形式である。
{"field1": "value1", "field2": "value2"}
この形式を使用するには、アトリビュートで #[serde(untagged)]
と指定する。
この形式はすべての種類の構成子に対して使用できるが、列挙型の構造次第では一意にシリアライズ・デシリアライズができない場合が存在する(後述)。
#[derive(Debug, Serialize, Deserialize)] #[serde(untagged)] enum Member { Permanent { id: u64, name: String, nickname: Option<String>, }, SingleChannel { channel_id: u64, name: String, nickname: Option<String>, }, }
Rustでの値
[Permanent { id: 1, name: "Jebediah Kerman", nickname: Some("Jeb") }, Permanent { id: 2, name: "Bob Kerman", nickname: None }, SingleChannel { channel_id: 8, name: "Kamler Kerman", nickname: None }]
[{"id":1,"name":"Jebediah Kerman","nickname":"Jeb"},{"id":2,"name":"Bob Kerman","nickname":null},{"channel_id":8,"name":"Kamler Kerman","nickname":null}]
Untagged形式の注意点
untagged形式では、シリアライズ時は単に中身を書き出し、デシリアライズ時には列挙型の構成子を前から順番に試すことでデシリアライズする。 したがって、untagged形式を使う際にはうまくデシリアライズされるように構成子の順番を工夫する必要がある。
例えば、整数・浮動小数点数・文字列のどれかになる列挙型を定義する場合、以下のようになる。 (コード例)
use failure::Error; use serde_derive::{Deserialize, Serialize}; pub fn run() -> Result<(), Error> { println!("\n======== Untagged enum with multiple types ========\n"); let string = r#"[1.0, 42, "foo"]"#; let values: Vec<Values> = serde_json::from_str(&string)?; println!("String: {}", string); println!("Decoded: {:?}", values); Ok(()) } #[derive(Debug, Serialize, Deserialize)] #[serde(untagged)] enum Values { Int(i64), Float(f64), Str(String), }
上記のコードを実行すると、以下のように正しく整数・浮動小数点数・文字列にデシリアライズされる。
======== Untagged enum with multiple types ======== String: [1.0, 42, "foo"] Decoded: [Float(1.0), Int(42), Str("foo")]
しかし、以下のように浮動小数点数の方を前にすると、整数は必ず浮動小数点数としてもデシリアライズできるため、整数も浮動小数点数にデシリアライズされてしまう。 (コード例)
#[derive(Debug, Serialize, Deserialize)] #[serde(untagged)] enum Values { Float(f64), Int(i64), Str(String), }
======== Untagged enum with multiple types ======== String: [1.0, 42, "foo"] Decoded: [Float(1.0), Float(42.0), Str("foo")]
serdeの他の機能との組み合わせ
other
internally tagged形式、adjacently tagged形式のどちらかを使用している時、unit構成子を列挙型の最後にして #[serde(other)]
アトリビュートをつけることで、タグがどの構成子にも該当しない時の値にすることができる。
(コード例)
#[derive(Debug, Serialize, Deserialize)] #[serde(tag = "education")] enum Education { #[serde(rename = "high school")] HighSchool { name: String }, #[serde(rename = "college")] College { name: String, speciality: String }, #[serde(rename = "bachelor")] Bachelor { name: String, speciality: String }, #[serde(rename = "master")] Master { name: String, speciality: String }, #[serde(rename = "doctor")] Doctor { name: String, speciality: String }, #[serde(other)] Other, }
[ {"education": "high school", "name": "Tokyo metropolitan Hibiya High School"}, {"education": "college", "name": "Kyouritsu Women's Junior College", "speciality": "english literature"}, {"education": "bachelor", "name": "Meiji University", "speciality": "laws"}, {"education": "master", "name": "Tokyo Institute of Technology", "speciality": "engineering"}, {"education": "doctor", "name": "The University of Tokyo", "speciality": "science"}, {"education": "baka", "name": "バカ田大学"} ]
[HighSchool { name: "Tokyo metropolitan Hibiya High School" }, College { name: "Kyouritsu Women\'s Junior College", speciality: "english literature" }, Bachelor { name: "Meiji University", speciality: "laws" }, Master { name: "Tokyo Institute of Technology", speciality: "engineering" }, Doctor { name: "The University of Tokyo", speciality: "science" }, Other]
tagがnullであった場合、other指定があってもデシリアライズでエラーが発生する。 (コード例)
共通部を別の構造体にくくりだす
serdeでは構造体のフィールドが構造体のときに #[serde(flatten)]
アトリビュートを指定することで、内側の構造体を外側の構造体に展開することができる。
これはstruct構成子のフィールドにも使用できるので、うまく使用することで共通部分を別の構造体として扱うことができる。
共通部分しかない場合には、newtype構成子を使用することもできる。
これは委譲を用いたコードとの親和性が高い。
use failure::Error; use serde_derive::{Deserialize, Serialize}; pub fn run() -> Result<(), Error> { println!("\n======== Extract fields to structs ========\n"); let string = r#"[ {"op": "Add", "type": "Miscellaneous", "name": "clean up room"}, {"op": "Add", "type": "Technical", "name": "fix Wi-Fi"}, {"op": "Take", "type": "Technical"}, {"op": "Take", "type": "Miscellaneous"} ]"#; let operations: Vec<Operation> = serde_json::from_str(&string)?; println!("String: {}", string); println!("Decoded: {:?}", operations); Ok(()) } #[derive(Debug, Serialize, Deserialize)] #[serde(tag = "op")] enum Operation { Add { #[serde(flatten)] task_type: TaskType, #[serde(flatten)] task: Task, }, Take(TaskType), } #[derive(Debug, Serialize, Deserialize)] #[serde(tag = "type")] enum TaskType { Technical, Miscellaneous, } #[derive(Debug, Serialize, Deserialize)] struct Task { name: String, }
おわりに
serdeはRustの汎用シリアライザ・デシリアライザフレームワークとして、Rustの型と汎用のデータ構造の変換部分と、汎用のデータ構造と各フォーマットの変換部分をうまく分離するように設計されている。 serdeの機能をうまく活用することで、複数のフォーマットとの互換性を保ちつつ、整理されたRustのコードを実現できるかもしれない。 公式ドキュメントにはより多くの機能の解説や例が載っているため、目を通しておくとよいだろう。