RustのSerdeクレートで共用体を扱う

5 min read読了の目安(約4500字

概要

例えば以下のような JSON ファイルを扱いたいとします。

data.json
[
    { "x" : 100 },
    { "x": "abc" }
]

Rust でこの JSON ファイルをシリアライズ/デシリアライズするにあたって x の値は文字列型又は数値型であることを表現する方法を調べました。

方法 1: Value 型を使う

それぞれのフォーマットのクレートには以下のような型が定義されています。

  • serde_json::Value
  • serde_yml::Value
  • toml::Value

これはいわゆる Any 型であり、それぞれのデータで表現できる型をいずれでも格納できる型となっています。

main.rs
fn main() -> Result<(), Box<dyn std::error::Error>> {
    let json = r#"
        [
            { "x": 100 },
            { "x": "abc" }
        ]
    "#;
    // ここでは、serde_yaml::Valueやtoml::Value型でも表現することができる。
    let v: serde_json::Value = serde_json::from_str(json)?;
    dbg!(v);

    Ok(())
}

ただし、この方法は動的型言語における JSON の扱いと同等であり、データを扱うためには中身を解析する必要があり、面倒ですし可読性も良くありません。
折角 Rust を使っているので型で扱いたいです。

main.rs
fn main() -> Result<(), Box<dyn std::error::Error>> {
    use serde_json::Value;

    let json = r#"
        [
            { "x": 100 },
            { "x": "abc" }
        ]
    "#;

    let data: Value = serde_json::from_str(json)?;

    match data {
        Value::Array(arr) => {
            for item in arr {
                match item {
                    Value::Object(item) => match item.get("x") {
                        Some(Value::Number(x)) => println!("x is Number({})", x),
                        Some(Value::String(x)) => println!("x is String({})", x),
                        _ => unreachable!(),
                    },
                    _ => unreachable!(),
                }
            }
        }
        _ => unreachable!(),
    }

    Ok(())
}

方法 2: 対象の値以外を構造体で定義する

以下のように型を定義することによって Rust の型でデータを扱うことができます。

main.rs
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct Data(pub Vec<Item>);

#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct Item {
    pub x: serde_json::Value,
}

fn main() -> Result<(), Box<dyn std::error::Error>> {
    let json = r#"
        [
            { "x": 100 },
            { "x": "abc" }
        ]
    "#;

    // 型指定は Vec<Item> でもよい
    let data: Data = serde_json::from_str(json)?;

    for item in data.0 {
        use serde_json::Value;

        match item.x {
            Value::Number(x) => println!("x is Number({})", x),
            Value::String(x) => println!("x is String({})", x),
            _ => unreachable!(),
        }
    }

    Ok(())
}

これは x のみ serde_json::Value 型で定義しており、 x 値を任意の値が入ることを考慮したいときに有用です。

方法 3: 列挙型を使う

x 値を文字列か数値のみで扱いたい場合、enum で定義できます。

main.rs
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct Item {
    pub x: X,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(untagged)]
pub enum X {
    String(String),
    Number(i64),
}

fn main() -> Result<(), Box<dyn std::error::Error>> {
    let json = r#"
        [
            { "x": 100 },
            { "x": "abc" }
        ]
    "#;

    // 型指定は Vec<Item> でもよい
    let data: Data = serde_json::from_str(json)?;
    for item in data.0 {
        match item.x {
            X::Number(x) => println!("x is Number({})", x),
            X::String(x) => println!("x is String({})", x),
        }
    }

    Ok(())
}

ポイントは X 型の属性 #[serde(untagged)] でこれを指定しないと、Json をデシリアライズする際に以下のデータを要求されます。

data.json
[
  {
    "x": { "Number": 100 }
  },
  {
    "x": { "String": "abc" }
  }
]

これはタグ付き共用体と呼ばれるものでそのままシリアライズ/シリアライズすることが有用な場面も存在します。(別種の object 型を扱いたいときなど)

タグ付き共用体

タグのつけ方を以下に変更してみます。

data.json
[
    { "type": "number", "x" : 100 },
    { "type": "string",  "x": "abc" }
]
main.rs
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct Data(pub Vec<Item>);

#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(tag = "type")]
pub enum Item {
    #[serde(rename = "number")]
    Number { x: i64 },
    #[serde(rename = "string")]
    String { x: String },
}

fn main() -> Result<(), Box<dyn std::error::Error>> {
    let json = r#"
        [
            { "type": "string", "x": "abc" },
            { "type": "number", "x": 100 }
        ]
    "#;

    let data: Data = serde_json::from_str(json)?;
    for item in data.0 {
        match item {
            Item::Number { x } => println!("x is Number({})", x),
            Item::String { x } => println!("x is String({})", x),
        }
    }

    Ok(())
}

#[serde(tag = "type")] でタグ名の指定、#[serde(rename = "...")] でタグの値の指定ができます。

参考

https://serde.rs/

https://serde.rs/enum-representations.html