Serde とは?
Rust の serde クレート は、Rust プログラム内で定義したユーザー型(struct や enum)を、JSON や YAML、BSON といった様々なデータ形式にシリアライズ/デシリアライズするためのライブラリです。 Serde という名前は、Serialize + Deserialize から来ています。 発音は、すぁーでぃ です。
Serde を使う準備
Serde を使うためには、ベースとなる serde
クレートに加えて、扱いたいデータフォーマット用のクレート(Serializer
/ Deserialize
実装)を依存関係に追加しておく必要があります。
例えば、JSON であれば serde_json
、YAML であれば serde_yaml
です。
Serde の derive
マクロを有効にするために、--features=derive
オプションを指定する必要があることに注意してください。
Cargo.toml
に次のような依存関係が追加されていれば準備 OK です。
基本的な使い方 (to_string, from_str)
次のサンプルコードでは、ユーザー定義の構造体 (Book
) のインスタンスから JSON 文字列への変換(シリアライズ)と、その逆の、JSON 文字列から構造体インスタンスへの変換(デシリアライズ)を行っています。
注: コードのシンプル化ため、ここでは Result#unwrap
メソッドを使っていますが、プロダクトコードでは正しく Result
を処理してください(参考: Result の基本)。
ユーザー定義型(struct
や enum
)を、任意の Serializer
/ Deserializer
実装(serde_json
など)で変換するには、その型に Serialize トレイト および Desrialize トレイト を実装しておく必要があります。
これは、ユーザー定義型を、Serde が処理できる汎用的なデータモデル に変換するための実装ですが、シンプルな構成の型であれば、上記のように #[derive(Serialize, Deserialize)]
属性を付加するだけで、デフォルト実装を提供してくれます。
Serialize
と Deserialize
の実装がコンパイル時に自動生成されます。
JSON ライブラリの設計によっては、アプリケーションの実行時にデータ型をリフレクションで処理するという方法も考えられますが、Serde は変換用の実装コードをコンパイル時に生成するという設計を採用しています。
これにより、実行時に高速かつ安全に動作することを保証しています。必要に応じて、serde::ser::Serialize
トレイトを実装することで、独自のシリアライズ処理 を提供することができます。
JSON ファイルへの保存と読み込み (to_writer, from_reader)
serde_json
クレートは、io::Write
への書き込みを行う to_writer / to_writer_pretty 関数や、io::Read
からの読み込みを行う from_reader 関数を提供しています。
これらの関数を利用して、ファイルやネットワークストリームに対して読み書きを行えます。
JSON ファイルへの保存
JSON ファイルの読み込み
フィールド名を変更する
デフォルトでは JSON フィールド名は、Rust の構造体のフィールド名がそのまま使われますが、構造体の定義に #[serde(rename_all)]
属性を付けると、対応付ける JSON フィールド名のルールをまとめて変更できます。
rename_all
の値として、他にも次のようなルールを指定できます。
"lowercase"
"UPPERCASE"
"PascalCase"
"camelCase"
"snake_case"
"SCREAMING_SNAKE_CASE"
"kebab-case"
"SCREAMING-KEBAB-CASE"
構造体のフィールドに #[serde(rename = "別名")]
属性を付けると、各フィールドを指定した名前でリネームすることができます。
デシリアライズ時に、別の JSON フィールド名でも読み込めるようにするには、#[serde(alias = "name")]
で別名を指定します。
これは、JSON へのシリアライズには影響しないことに注意してください。
別名は複数指定することができます。
この機能は恒久的には使うべきではないかもしれませんが、JSON ファイルのフォーマットを段階的に移行したいときに便利です。
未知の JSON フィールドが見つかったらエラーにする
デシリアライズしようとしている JSON データに、未知のフィールドが含まれているとき(Rust のユーザー定義型に対応するフィールドがないとき)、デフォルトではそのフィールドの値は無視されます。 つまり、構造体のインスタンスの生成は問題なく実行されます。
JSON データに未知のフィールドが含まれているときにエラーにしたい場合は、ユーザー定義型に deny_unknown_fields
属性 (container attribute) を付加します。
Nullable や存在しない JSON フィールドを扱う
JSON の null 値を扱う
Rust には null
という概念は存在しませんが、値が存在しないかもしれないフィールドは Option 列挙型で表現できます。
例えば、JSON データの title
フィールドの値として null
が含まれている可能性がある場合は、対応する構造体の title
フィールドを次のように Option
型にします。
これで、次のような null
値を含む JSON ファイルを読み込めます。
Rust 側で値を参照すると Option::None
という値として参照できます。
JSON にフィールドが存在しないとき
次のように、JSON データに対象のフィールド自体が存在しない場合も、Option
型でハンドルできます。
この場合も、Option::None
という値が格納されます。
構造体のフィールドを Option
型にする代わりに、#[serde(default)]
属性を付けて、その型のデフォルト値を入れることもできます。
次のように定義すると、JSON データに対応するフィールドが存在しないときに、String
型のデフォルト値である空文字列 (""
) が格納されます(#[serde(default = "func_name")]
として、任意のデフォルト値生成関数を呼び出すこともできます)。
ただし、これは、JSON にフィールドが存在しない場合のみ機能するもので、値として null
が含まれている場合はエラーになることに注意してください(その場合は Option
型を使う必要があります)。
参考: Default value for a field · Serde
シリアライズ/デシリアライズの対象外にする
特定の構造体フィールドを Serde のシリアライズ/デシリアライズの対象外にするには、次のような属性をフィールドに付加します。
#[serde(skip)]
… シリアライズとデシリアライズの対象外にする。#[serde(skip_serializing)]
シリアライズの対象外にする。#[serde(skip_deserializing)]
デシリアライズの対象外にする。
デシリアライズの対象外になっている構造体フィールドは、Default::default()
が返すデフォルト値で初期化されます。
もし、#[serde(default = "func_name")]
という属性値がセットされている場合は、指定した関数がデフォルト値の生成のために呼び出されます。
他にも、属性を使って次のように細かな制御を行うことができます。
// Option 値が None のときは JSON フィールドを出力しない
#[serde(skip_serializing_if = "Option::is_none")]
comment: Option<String>,
// 文字列が空の場合は JSON のフィールドを出力しない、ただし、読み込み時は空文字列として初期化する
#[serde(default, skip_serializing_if = "String::is_empty")]
serial: String,
// ベクターが空の場合は JSON のフィールドを出力しない、ただし、読み込み時は空のベクターとして初期化する
#[serde(default, skip_serializing_if = "Vec::is_empty")]
authors: Vec<String>,
// マップが空の場合は JSON のフィールドを出力しない、ただし、読み込み時は空のマップとして初期化する
#[serde(default, skip_serializing_if = "HashMap::is_empty")]
attributes: HashMap<String, String>,
形式の不明な JSON ファイルを読み込む (serde_json::Value)
どのようなフィールドが含まれているかわからない JSON ファイルを読み込む場合は、任意の JSON データ型を示す serde_json::Value
として読み込みます。
serde_json::Value
は次のような定義の列挙型 (enum
) で、JSON で表現できるデータ型がバリアントとして定義されています。
pub enum Value {
Null, // null
Bool(bool), // bool
Number(Number), // 数値
String(String), // 文字列
Array(Vec<Value>), // 配列
Object(Map<String, Value>), // オブジェクト
}
ここでは、次のような JSON ファイルを読み込んでみます。 「オブジェクトの配列」の形になっているということまでは分かっているものとします。
次の例では、games.json
を Value
型として読み込み、その内容を表示しています。
まず、下記の行で games.json
ファイル全体を汎用的な Value
型として読み出しています。
let games_json: Value = load_games();
Value
は列挙型なので、その内容を参照するには、if let
構文でどのバリアントなのかを判別してから参照する必要があります(参考: enum 型の使い方)。
今回の games.json
は配列形式で記述されていると想定し、次のようにして Value::Array
バリアント(内容は Vec<Value>
型)として参照しています。
if let Value::Array(games) = &games_json {
// ... games を Vec<Value> 型として参照できる ...
}
Value::Array
バリアントとして取り出した games
は Vec<Value>
型なので、for-in
ループで列挙することができます。
そして、games
の中の個々の要素 game
はオブジェクト形式なので、Value::Object
バリアントとして参照することができます。
下記のコードでは、as_object()
を使って Value::Object
バリアントとして取り出していますが、ここでも if let
を使って Value::Object
バリアントかどうかを判別するのでも OK です。
for game in games {
for (key, value) in game.as_object().unwrap() {
println!("{}: {}", key, value);
}
}
このようにすれば、どんなフィールドを持っているか不明な JSON ファイルを処理できますが、できればデータ型をちゃんと定義して扱いたいですね。