serdeに入門しよう!
serdeとは?
serdeはRust向けのシリアライゼーションライブラリです。
serialize + deserializeでser + de、合わせてserdeです。
serdeはシリアライズとデシリアライズを行う仕組みを提供し、実際にJSONやTOMLなどのフォーマットに変換するのはserde_jsonやtomlなどそれぞれのcrateが担当します。
本記事ではserde_jsonでJSONにシリアライズするとして説明します。
また、今回の例では簡単のため適切なエラーハンドリングは行わずunwrap()
を使用しています。
実際に使用する場合は適切なエラーハンドリングを行うようにしてください。
基本的な使い方
まず、依存の導入をしましょう!
# serdeをderive featureを有効にして導入
cargo add serde --features derive
# JSONに変換するcrateであるserde_jsonを導入
cargo add serde_json
上記のコマンド実行により、Cargo.tomlは以下のようになります。
バージョンはタイミングによって変わります。
[dependencies]
serde = { version = "1.0.228", features = ["derive"] }
serde_json = "1.0.145"
serdeはderiveマクロとして使用することが一般的です。
deriveマクロ
deriveマクロはボイラープレート的な実装をマクロの力を使って代わりに実装させる機能です。
例えば、Default
やClone
などのtraitは同名のderiveマクロを提供しています。
#[derive(Default, Clone)]
struct MyValue {
value1: String,
value2: i32,
}
上記のような型について考えます。
これを展開すると大体以下のようになります。
struct MyValue {
value1: String,
value2: i32,
}
impl Default for MyValue {
fn default() -> Self {
Self {
value1: String::default(),
value2: i32::default(),
}
}
}
impl Clone for MyValue {
fn clone(&self) -> Self {
Self {
value1: self.value1.clone(),
value2: self.value2.clone(),
}
}
}
このような何度も同じことを書かないといけない状態を避けるためderiveマクロを使用します。
SerializeとDeserialize
serdeにderive
featureとともに依存に入れるとserde::Serialize
とserde::Deserialize
というderiveマクロが使用可能になります。
use serde::{Serialize, Deserialize};
#[derive(Serialize, Deserialize)]
struct MyValue {
value_one: String,
value_two: i32,
}
上記をJSONにシリアライズする場合は以下のように行います
let value = MyValue { value_one: "v1".to_string(), value_two: 42 };
let json = serde_json::to_string(&value).unwrap();
この場合のjson
の値は以下のようになります。
{"value_one":"v1","value_two":42}
逆にデシリアライズする例は以下のようになります。
let json = r#"{"value_one":"v1","value_two":42}"#;
let value: MyValue = serde_json::from_str(json).unwrap();
これだけでも十分に使えますね。
名前を変えてみる
特定の項目名を変更する
use serde::{Serialize, Deserialize};
#[derive(Serialize, Deserialize)]
struct MyValue {
#[serde(rename = "renamed")]
value_one: String,
value_two: i32,
}
この場合は以下のようになります。
{"renamed":"v1","value_two":42}
非常に簡単ですね。
全部キャメルケース / ケバブケース ...に変える
serdeではデフォルトのキー名はフィールドと同じ名前になります。つまりRustの命名規則から基本的にはスネークケースになります。
ですが、すべてのフィールドで同じ命名規則を適用するのは大変です。そのための機能がserdeには備わっています。
use serde::{Serialize, Deserialize};
#[derive(Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
struct MyValue {
value_one: String,
value_two: i32,
}
これをJSONにすると以下のようになります。
{"valueOne":"v1","valueTwo":42}
ここで使える命名法[1]の名前を以下に示します。
名前 | どういう感じになるか |
---|---|
lowercase |
{"value_one":"v1","value_two":42} |
UPPERCASE |
{"VALUE_ONE":"v1","VALUE_TWO":42} |
PascalCase |
{"ValueOne":"v1","ValueTwo":42} |
camelCase |
{"valueOne":"v1","valueTwo":42} |
snake_case |
{"value_one":"v1","value_two":42} |
SCREAMING_SNAKE_CASE |
{"VALUE_ONE":"v1","VALUE_TWO":42} |
kebab-case |
{"value-one":"v1","value-two":42} |
SCREAMING-KEBAB-CASE |
{"VALUE-ONE":"v1","VALUE-TWO":42} |
デフォルトとスキップ
Optionとdefault
まず、以下のコードを見てください。
use serde::{Serialize, Deserialize};
#[derive(Serialize, Deserialize)]
struct MyValue {
value_one: Option<String>,
value_two: i32,
}
このコードはvalue_one
の型がOption<String>
になっています。
serdeではOption
はnullableな値として扱われます。
上記のコードでvalue_one
にNone
を渡してJSON化したものが以下です。
{"value_one":null,"value_two":42}
このように、null
となっていますね。
Option
を使用することで、渡さない場合はnull
にすることができます。
逆にデシリアライズのときはどうでしょうか?
まず、先程出力した以下のJSONをデシリアライズしてみましょう。
{"value_one":null,"value_two":42}
これを読み込むと以下のような値になります。
MyValue { value_one: None, value_two: 42 }
次に、null
の項目を省略し以下のJSONをデシリアライズしてみましょう。
{"value_two":42}
デシリアライズするコードは以下です。
let json = r#"{"value_two":42}"#;
let value: MyValue = serde_json::from_str(&json).unwrap();
結果はnull
を入れた場合と同様に以下になります。
MyValue { value_one: None, value_two: 42 }
デシリアライズした際にデフォルトの値を決めたいこともあると思います。
例えば以下のような設定ファイルの場合です。
{
"listen": "127.0.0.1:8080"
}
この設定ファイルはサーバーのホスト名とポートを指定する設定ファイルです。
多くのサーバーではデフォルトで使用するIPやポートが決まっています。
その場合は以下のように記述します。
#[derive(Serialize, Deserialize, Debug)]
struct Config {
#[serde(default = "default_listen")]
listen: String,
}
fn default_listen() -> String {
"127.0.0.1:8080".to_string()
}
デフォルトの値を指定する場合は#[serde(default)]
を使用します。
今回は、=
でデフォルトの値を作る関数を指定していますが、省略することもできます。
省略した場合、Default::default
が使用されます。
今回はdefault_listen
という関数を作成し、それを使用しています。
これを使い以下のJSONをデシリアライズしてみましょう。
{}
Config { listen: "127.0.0.1:8080" }
skip
特定のフィールドをシリアライズやデシリアライズしないように指定することもできます。
シリアライズの際は、そのフィールドは項目として存在しなくなり、デシリアライズの際は、Default::default
か#[serde(default)]
で指定した関数が使用されます。
以下の例を見てみましょう。
use serde::{Serialize, Deserialize};
#[derive(Serialize, Deserialize, Debug)]
struct SkipSerde {
#[serde(skip)]
skip1: String,
#[serde(skip, default = "skip2_default")]
skip2: String,
}
fn skip2_default() -> String {
"skip2_default value".to_string()
}
これを、空のJSON ({}
) でデシリアライズすると、以下の値になります。
SkipSerde { skip1: "", skip2: "skip2_default value" }
String
のDefault::default()
の値は空文字列なので、skip1
の値は""
となります。
また、#[serde(skip_serializing)]
でシリアライズを行う際のみスキップしたり、#[serde(skip_deserializing)]
でデシリアライズを行う際のみスキップすることもできます。
シリアライズの条件付きskip
シリアライズの際には条件付きでskipする機能もあります。
以下に例を示します。
use serde::{Serialize, Deserialize};
#[derive(Serialize, Deserialize, Debug)]
struct SkipSerde {
#[serde(skip_serializing_if = "Vec::is_empty")]
values: Vec<String>,
}
#[serde(skip_serializing_if = "Vec::is_empty")]
の部分がその機能です。
この例ではVec::is_empty
がtrue
の際にシリアライズがスキップされます。つまり、要素が空の場合はシリアライズされないことになります。
以下にそれを使用したコード例を示します。
fn main() {
let value = SkipSerde { values: vec![] };
let json = serde_json::to_string(&value).unwrap();
println!("1: {json}");
let value = SkipSerde { values: vec!["value".to_string()] };
let json = serde_json::to_string(&value).unwrap();
println!("2: {json}");
}
これの出力は以下のようになります。
1: {}
2: {"values":["value"]}
確かに要素がない場合は項目自体が出力されなくなりました。
enum
Rustには非常に強力なenum機能があります。
この強さはserdeでも同様に活かされます。
まず、普通にenumをシリアライズしてみましょう。
use serde::{Serialize, Deserialize};
#[derive(Serialize, Deserialize, Debug)]
enum EnumSerde {
UnitStyle,
TupleStyle(i32),
StructStyle { value1: i32 },
}
fn main() {
let value = EnumSerde::UnitStyle;
let json = serde_json::to_string(&value).unwrap();
println!("unit: {json}");
let value = EnumSerde::TupleStyle(42);
let json = serde_json::to_string(&value).unwrap();
println!("tuple: {json}");
let value = EnumSerde::StructStyle { value1: 42 };
let json = serde_json::to_string(&value).unwrap();
println!("struct: {json}");
}
これを実行すると以下のようになります。
unit: "UnitStyle"
tuple: {"TupleStyle":42}
struct: {"StructStyle":{"value1":42}}
tag
上記の通常通りのシリアライズはExternally taggedと呼ばれています。
StructStyle
を見るとわかりやすいですが、"StructStyle"
キーの値として、enumの値が入っています。つまり、項目名が外側 (External) にあるということです。
JSONでは値の内側に項目名を入れる形式の方が一般的ですし、パースもしやすいです。
その場合は以下のように修正します。
use serde::{Serialize, Deserialize};
#[derive(Serialize, Deserialize, Debug)]
#[serde(tag = "type")]
enum EnumSerde {
UnitStyle,
// TupleStyle(i32),
StructStyle { value1: i32 },
}
fn main() {
let value = EnumSerde::UnitStyle;
let json = serde_json::to_string(&value).unwrap();
println!("unit: {json}");
// let value = EnumSerde::TupleStyle(42);
// let json = serde_json::to_string(&value).unwrap();
// println!("tuple: {json}");
let value = EnumSerde::StructStyle { value1: 42 };
let json = serde_json::to_string(&value).unwrap();
println!("struct: {json}");
}
この例では、#[serde(tag = "type")]
をつけています。
unit: {"type":"UnitStyle"}
struct: {"type":"StructStyle","value1":42}
tuple形式では値の方の項目名がつけられないため、エラーになってしまいます。
tuple形式で使用する場合は、tag
と一緒にcontent
を指定します。
use serde::{Serialize, Deserialize};
#[derive(Serialize, Deserialize, Debug)]
#[serde(tag = "type", content = "value")]
enum EnumSerde {
TupleStyle(i32),
}
fn main() {
let value = EnumSerde::TupleStyle(42);
let json = serde_json::to_string(&value).unwrap();
println!("tuple: {json}");
}
これを実行すると以下のようになります。
tuple: {"type":"TupleStyle","value":42}
untagged
逆に項目名を出力しないようにすることもできます。
それがuntagged
です。
以下のコードは、untagged
を使用した例です。
use serde::{Serialize, Deserialize};
#[derive(Serialize, Deserialize, Debug)]
#[serde(untagged)]
enum EnumSerde {
Variant1 { value: i32 },
Variant2 { value: String },
}
fn main() {
let value = EnumSerde::Variant1 { value: 42 };
let json = serde_json::to_string(&value).unwrap();
println!("JSON: {json}");
let json = r#"{"value": "string value"}"#;
let value: EnumSerde = serde_json::from_str(json).unwrap();
println!("Value: {value:?}");
}
これの出力結果は以下のようになります。
JSON: {"value":42}
Value: Variant2 { value: "string value" }
このuntagged
はバリアントを識別するタグがないため、デシリアライズ時には定義されている順番に各バリアントを試していきます。
今回はフィールド名は一致していますが、i32
とString
という型の違いで識別可能なため適切に選択できます。ただし、同じ構造のバリアントが複数ある場合は、最初にマッチしたものが選ばれるため注意が必要です。
flatten
ある構造体に別の構造を埋め込み平坦化する機能もあります。
これを行う場合には#[serde(flatten)]
を使用します。
まずは、以下のコードを見てみましょう。
use serde::{Serialize, Deserialize};
#[derive(Serialize, Deserialize, Debug)]
struct StructContent {
value_i32: i32,
}
#[derive(Serialize, Deserialize, Debug)]
#[serde(tag = "type")]
enum EnumContent {
Variant { value: i32 },
}
#[derive(Serialize, Deserialize, Debug)]
struct StructSerde {
flatten_struct: StructContent,
flatten_enum: EnumContent,
}
fn main() {
let value = StructSerde {
flatten_enum: EnumContent::Variant { value: 42 },
flatten_struct: StructContent { value_i32: 42 },
};
let json = serde_json::to_string(&value).unwrap();
println!("JSON: {json}");
}
これは#[serde(flatten)]
を使っていないパターンです。
これを実行すると以下のような結果になります。
JSON: {"flatten_struct":{"value_i32":42},"flatten_enum":{"type":"Variant","value":42}}
flatten_struct
やflatten_enum
など各項目のキーが含まれていますね。
これらの項目に#[serde(flatten)]
をつけてみましょう。
use serde::{Serialize, Deserialize};
#[derive(Serialize, Deserialize, Debug)]
struct StructContent {
value_i32: i32,
}
#[derive(Serialize, Deserialize, Debug)]
#[serde(tag = "type")]
enum EnumContent {
Variant { value: i32 },
}
#[derive(Serialize, Deserialize, Debug)]
struct StructSerde {
#[serde(flatten)]
flatten_struct: StructContent,
#[serde(flatten)]
flatten_enum: EnumContent,
}
fn main() {
let value = StructSerde {
flatten_enum: EnumContent::Variant { value: 42 },
flatten_struct: StructContent { value_i32: 42 },
};
let json = serde_json::to_string(&value).unwrap();
println!("JSON: {json}");
}
これを実行すると以下のようになります。
JSON: {"value_i32":42,"type":"Variant","value":42}
各キーが埋め込まれた形になりましたね。
serdeではこのように別のstructやenumのキーを埋め込む機能があります。
tomlで使う
serdeはシリアライズとデシリアライズを提供する機能です。
そのため、当然JSON以外でも使用できます。
今回は試しにtomlで使用してみましょう。
まずは依存を追加します。
cargo add toml
そして、serde_json
の部分をtoml
に変えます。
use serde::{Serialize, Deserialize};
#[derive(Serialize, Deserialize, Debug)]
struct StructContent {
value_i32: i32,
}
#[derive(Serialize, Deserialize, Debug)]
#[serde(tag = "type")]
enum EnumContent {
Variant { value: i32 },
}
#[derive(Serialize, Deserialize, Debug)]
struct StructSerde {
#[serde(flatten)]
flatten_struct: StructContent,
#[serde(flatten)]
flatten_enum: EnumContent,
}
fn main() {
let value = StructSerde {
flatten_enum: EnumContent::Variant { value: 42 },
flatten_struct: StructContent { value_i32: 42 },
};
let tml = toml::to_string(&value).unwrap();
println!("{tml}");
}
println!
や変数名もTOML向けに変更しました。
これを実行すると以下になります。
value_i32 = 42
type = "Variant"
value = 42
出力がJSONからTOMLに変わりましたね!
serdeではこのように簡単にシリアライズフォーマットを変えることが可能です。
今回はserde_json
とtoml
がたまたまto_string
という関数名でシリアライズする形式だったため、その部分の修正は必要ありませんでしたが、crateによってシリアライズやデシリアライズをするメソッド名は変わるため、使用するcrateのドキュメントを参照するようにしてください。
まとめ
Rustのserdeは非常に便利なシリアライズとデシリアライズの仕組みを提供するcrateです。
カスタムシリアライザなしで、いろいろな構造を作ることができるので非常に便利です。
Rustを使うならぜひ使ってみてください!
Discussion