Rustでシリアライズ、デシリアライズしたい
ゲームのデータをセーブ、ロードするために、Rustのシリアライズ化フレームワークserde-rsを調査しました。
serde-rs
基本
データ構造を定義したら、以下のようにderiveを用いてSerializeトレートとDeserializeトレートをimplすると、データ構造をシリアライズ、デシリアライズできるようになります。シンプルな実装です。
use serde::{Serialize, Deserialize};
# [derive(Serialize, Deserialize)]
pub struct PlayerType {
MAN,
COM
}
# [derive(Serialize, Desirialize)]
pub struct Player {
id: u32,
player_type: PlayerType
}
fn main() {
let player = Player { id: 1, player_type: PlayerType::MAN };
// json形式にシリアライズ
// {"id":1,"player_type":"MAN"}
let serialized = serde_json::to_string(&player).unwrap();
// json形式からデシリアライズ
// Player { id: 1, player_type: MAN }
let deserialized: Sample = serde_json::from_str(&serialized).unwrap();
// 確認
match deserialized.aa {
AA::BB => println!("aa"),
_ => println!("nn"),
}
}
Serdeの特徴
- Serdeデータモデルを用いて多くの型、フォーマットのデータを扱える
- deriveを用いて、タグの名前やタグの有無を簡単に設定可能(属性設定)
- データ欠損、非共通フィールドに動的に対応可能
- カスタマイズ性が高い
1. Serdeデータモデル
シリアライズする時、データ構造を中間的なSerdeデータモデルにマッピングし、そのモデルを意図したデータフォーマットに変換します。デシリアライズするときはその逆を行います。
このSerdeデータモデルの型はほとんどのRustの型と一致しているので、Rustのデータ構造を幅広くシリアライズできます。
OS依存の文字列型の問題を扱うときなど、ユースケースに応じたマッピングも可能であると、こちらに記述されています。
以下は利用可能なデータ構造とデータフォーマットです。
2. 属性の設定
serdeのderiveを用いて以下の3つのデータ属性を簡単に設定可能です。
- Container属性:structやenum全体の属性
- Varient属性:enumのvarientの1つの属性
- Field属性:structのフィールドの1つかenumのvarientの1つの属性
Container属性の例を1つ紹介します。
// シリアライズする時にタグの名前と内容物の名前を指定する
// データから指定のタグのものを抜き出したい時などに便利
# [derive(Serialize, Deserialize)]
# [serde(tag = "player_type", content = "detail")]
pub enum PlayerType {
MAN { id: u32 },
COM { id: u32, com_level: ComLevel }
}
// JSONにシリアライズすると以下が出力される
// ・ 今回
// {"player_type": "MAN", "detail": {"id": ... }},
// {"player_type": "COM", "detail": {"id": ..., "com_level": ...}}
// ・ 何も指定しなかった場合
// {"MAN": {"id":...,}}
// {"COM": {"id":..., "com_level": ...}}
タグを消すときは#[serde(untagged)]を指定します。上記例だと、"MAN"、"COM"も出力されなくなります。
3. データ欠損、非共通フィールド
2項で説明したField属性についてserdeのderiveを用いるとデフォルト値を設定することができ、データ欠損に対応できます。
# [derive(Serialize, Deserialize)]
pub struct Player {
id: u32,
#[serde(default)] // デシリアライズ時にこのフィールドの値が存在しない時にPlayerInfoのデファルト値を補う
player_info: PlayerInfo
}
# [derive(Serialize, Deserialize)]
pub struct PlayerInfo {
player_type: PlayerType,
player_color: PlayerColor,
}
/// PlayerInfoのデフォルト関数
impl Default for PlayerInfo {
fn default() -> Self {
PlayerInfo {
player_type: Player_Type::MAN,
player_color: Color::Red,
}
}
}
/*
例えばlet json = r#"[{"id": 2}]"#;
をデシリアライズするとデータ構造にplayer_infoの項をPlayerInfoのデフォルト値が補う。
*/
同様にField属性を用いて非共通フィールドにも対応可能です。同じデータ構造の中で、あるデータにないけど、別のデータには必要みたいな状況の時に使います。例えばPlayerタイプがマニュアルならコンピュータの強さは必要ないけど、CPUなら必要みたいな状況です。
# [derive(Serialize, Deserialize)]
struct Player {
id: u32,
player_type: PlayerType,
#[serde(flatten)]
extra: ComLevel,
}
// こうすることで、デシリアライズする時、"com_level": "Hard"がという項があっても大丈夫です。
4. カスタマイズ
かゆい所に手を届かせたいときはSerializeトレートとDeserializeトレートをderiveせずに自分でimplすることでカスタマイズできるそうです。
詳細はこちら。
まとめ
かなり簡単に使えそうなので開発中のゲームに使っていこうと思いました。