🐡

Rustでstructで想定していないJSONに対応する

2024/12/05に公開

目的

ユニークビジョン株式会社 Advent Calendar 2024の12/3の記事です。

APIの呼び出し結果がJSONで返ってくる場合に、Rustではそれをstructで受け取りたいです。
当初リファレンス通りに実装したとしても、ある日突然値が追加されてしまうと取りこぼすことになってしまいます。
そこで取りこぼさないためのテクニックを紹介します。

ちなみに自分が公開しているcrateには実装してあります。
𝕏(旧Twitter)
Pinterest
TiktokBusiness
TiktokV2

説明

idとcontentが必須でkeyがオプションのJSONが来ることを想定します。
sturctは以下のようになります。

#[derive(Serialize, Deserialize, Debug, Clone, Default)]
pub struct Data {
    pub id: String,
    pub content: String,

    #[serde(skip_serializing_if = "Option::is_none")]
    pub key: Option<String>,
}

skip_serializing_ifはserializeした時にkeyがNoneなら出力しない調整になります。

オプションあり

let data = json!({
    "id": "1",
    "content": "Hello, World!",
    "key": "value",
});

let res = serde_json::from_value::<Data>(data);
println!("オプションあり\n{:?}", res);
println!("{}\n", serde_json::to_string(&res.unwrap()).unwrap());
オプションあり
Ok(Data { id: "1", content: "Hello, World!", key: Some("value") })
{"id":"1","content":"Hello, World!","key":"value"}

オプション無し

let data = json!({
    "id": "1",
    "content": "Hello, World!",
});

let res: Result<Data, serde_json::Error> = serde_json::from_value::<Data>(data);
println!("オプション無し\n{:?}", res);
println!("{}\n", serde_json::to_string(&res.unwrap()).unwrap());
オプション無し
Ok(Data { id: "1", content: "Hello, World!", key: None })
{"id":"1","content":"Hello, World!"}

設定した通りserializeされた結果にkeyは出てきません。

必須無し

let data = json!({
    "id": "1",
});

let res: Result<Data, serde_json::Error> = serde_json::from_value::<Data>(data);
let res = serde_json::from_value::<Data>(data);
println!("必須無し\n{:?}\n", res);
必須無し
Err(Error("missing field `content`", line: 0, column: 0))

必須が無いとエラーになります。

想定外あり

let data = json!({
    "id": "1",
    "content": "Hello, World!",
    "abc": "efg"
});

let res = serde_json::from_value::<Data>(data);
println!("想定外あり\n{:?}", res);
println!("{}\n", serde_json::to_string(&res.unwrap()).unwrap());
想定外あり
Ok(Data { id: "1", content: "Hello, World!", key: None })
{"id":"1","content":"Hello, World!"}

想定外の情報はstructにもserializeされたjsonにも出てこないです。

想定外あり。extra付き

そこで想定外なJSONが来ても対応できるようにextraフィールドを追加します。
そこにはflattenを指定します。
詳しくはserdeのリファレンス(Struct flattening)を参考にしてください。

use std::collections::HashMap as Map;
#[derive(Serialize, Deserialize, Debug, Clone, Default)]
pub struct DataWithExtra {
    pub id: String,
    pub content: String,

    #[serde(skip_serializing_if = "Option::is_none")]
    pub key: Option<String>,

    #[serde(flatten, skip_serializing_if = "Map::is_empty")]
    pub extra: Map<String, serde_json::Value>,
}
let data = json!({
    "id": "1",
    "content": "Hello, World!",
    "abc": "efg"
});

let res = serde_json::from_value::<DataWithExtra>(data);
println!("想定外あり。extra付き\n{:?}", res);
println!("{}\n", serde_json::to_string(&res.unwrap()).unwrap());
想定外あり。extra付き
Ok(DataWithExtra { id: "1", content: "Hello, World!", key: None, extra: {"abc": String("efg")} })
{"id":"1","content":"Hello, World!","abc":"efg"}

extraフィールドに保存されました! serializeした結果にも出ています。

想定外無し。extra付き

let data = json!({
    "id": "1",
    "content": "Hello, World!",
});

let res = serde_json::from_value::<DataWithExtra>(data);
println!("想定外無し。extra付き\n{:?}", res);
println!("{}", serde_json::to_string(&res.unwrap()).unwrap());
想定外無し。extra付き
Ok(DataWithExtra { id: "1", content: "Hello, World!", key: None, extra: {} })
{"id":"1","content":"Hello, World!"}

想定外が無い場合はserializeした結果にextraは出てきません。

コード

今回のコードは以下になります。

Cargo.toml
[package]
name = "json"
version = "0.1.0"
edition = "2021"

[dependencies]
serde = { version = "1", features = ["derive"] }
serde_json = "1"
main.rs
use serde::{Deserialize, Serialize};
use serde_json::json;
use std::collections::HashMap as Map;

#[derive(Serialize, Deserialize, Debug, Clone, Default)]
pub struct Data {
    pub id: String,
    pub content: String,

    #[serde(skip_serializing_if = "Option::is_none")]
    pub key: Option<String>,
}

#[derive(Serialize, Deserialize, Debug, Clone, Default)]
pub struct DataWithExtra {
    pub id: String,
    pub content: String,

    #[serde(skip_serializing_if = "Option::is_none")]
    pub key: Option<String>,

    #[serde(flatten, skip_serializing_if = "Map::is_empty")]
    pub extra: Map<String, serde_json::Value>,
}

fn main() {
    let data = json!({
        "id": "1",
        "content": "Hello, World!",
        "key": "value",
    });

    let res = serde_json::from_value::<Data>(data);
    println!("オプションあり\n{:?}", res);
    println!("{}\n", serde_json::to_string(&res.unwrap()).unwrap());

    let data = json!({
        "id": "1",
        "content": "Hello, World!",
    });

    let res: Result<Data, serde_json::Error> = serde_json::from_value::<Data>(data);
    println!("オプション無し\n{:?}", res);
    println!("{}\n", serde_json::to_string(&res.unwrap()).unwrap());

    let data = json!({
        "id": "1",
    });

    let res = serde_json::from_value::<Data>(data);
    println!("必須無し\n{:?}\n", res);

    let data = json!({
        "id": "1",
        "content": "Hello, World!",
        "abc": "efg"
    });

    let res = serde_json::from_value::<Data>(data);
    println!("想定外あり\n{:?}", res);
    println!("{}\n", serde_json::to_string(&res.unwrap()).unwrap());

    let data = json!({
        "id": "1",
        "content": "Hello, World!",
        "abc": "efg"
    });

    let res = serde_json::from_value::<DataWithExtra>(data);
    println!("想定外あり。extra付き\n{:?}", res);
    println!("{}\n", serde_json::to_string(&res.unwrap()).unwrap());

    let data = json!({
        "id": "1",
        "content": "Hello, World!",
    });

    let res = serde_json::from_value::<DataWithExtra>(data);
    println!("想定外無し。extra付き\n{:?}", res);
    println!("{}", serde_json::to_string(&res.unwrap()).unwrap());
     
}
実行結果
オプションあり
Ok(Data { id: "1", content: "Hello, World!", key: Some("value") })
{"id":"1","content":"Hello, World!","key":"value"}

オプション無し
Ok(Data { id: "1", content: "Hello, World!", key: None })
{"id":"1","content":"Hello, World!"}

必須無し
Err(Error("missing field `content`", line: 0, column: 0))

想定外あり
Ok(Data { id: "1", content: "Hello, World!", key: None })
{"id":"1","content":"Hello, World!"}

想定外あり。extra付き
Ok(DataWithExtra { id: "1", content: "Hello, World!", key: None, extra: {"abc": String("efg")} })
{"id":"1","content":"Hello, World!","abc":"efg"}

想定外無し。extra付き
Ok(DataWithExtra { id: "1", content: "Hello, World!", key: None, extra: {} })
{"id":"1","content":"Hello, World!"}

Discussion