LoginSignup
5
2

More than 3 years have passed since last update.

Serde-rsの調査

Posted at

Rustでシリアライズ、デシリアライズしたい

ゲームのデータをセーブ、ロードするために、Rustのシリアライズ化フレームワークserde-rsを調査しました。
スクリーンショット 2020-04-21 9.07.53.png

serde-rs

基本

データ構造を定義したら、以下のようにderiveを用いてSerializeトレートとDeserializeトレートをimplすると、データ構造をシリアライズ、デシリアライズできるようになります。シンプルな実装です。

kihon
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の特徴

  1. Serdeデータモデルを用いて多くの型、フォーマットのデータを扱える
  2. deriveを用いて、タグの名前やタグの有無を簡単に設定可能(属性設定)
  3. データ欠損、非共通フィールドに動的に対応可能
  4. カスタマイズ性が高い

1. Serdeデータモデル

シリアライズする時、データ構造を中間的なSerdeデータモデルにマッピングし、そのモデルを意図したデータフォーマットに変換します。デシリアライズするときはその逆を行います。
このSerdeデータモデルの型はほとんどのRustの型と一致しているので、Rustのデータ構造を幅広くシリアライズできます。
OS依存の文字列型の問題を扱うときなど、ユースケースに応じたマッピングも可能であると、こちらに記述されています。

以下は利用可能なデータ構造とデータフォーマットです。

2. 属性の設定

serdeのderiveを用いて以下の3つのデータ属性を簡単に設定可能です。

Container属性の例を1つ紹介します。

attributes
// シリアライズする時にタグの名前と内容物の名前を指定する
// データから指定のタグのものを抜き出したい時などに便利
#[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を用いるとデフォルト値を設定することができ、データ欠損に対応できます。

default
#[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なら必要みたいな状況です。

flatten
#[derive(Serialize, Deserialize)]
struct Player {
    id: u32,
    player_type: PlayerType,

    #[serde(flatten)]
    extra: ComLevel,
}

// こうすることで、デシリアライズする時、"com_level": "Hard"がという項があっても大丈夫です。

4. カスタマイズ

かゆい所に手を届かせたいときはSerializeトレートとDeserializeトレートをderiveせずに自分でimplすることでカスタマイズできるそうです。
詳細はこちら

まとめ

かなり簡単に使えそうなので開発中のゲームに使っていこうと思いました。

5
2
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
5
2