跳至內容

JSON 轉 Rust

從 JSON 範例生成附 serde derive 的 Rust struct。

輸入

這個工具的用途

從 JSON 樣本產生帶 #[derive(Serialize, Deserialize)] Rust struct。可為 null 的欄位 成為 Option<T>、不是合法 snake_case 識別字的 JSON 鍵 會加上 #[serde(rename = "…")], 巢狀物件成為各自的 struct。底層使用 quicktype,完全在你的瀏覽器內運作。

使用步驟

貼上 JSON(或載入範例),即可在右側讀到 Rust struct。每個巢狀物件 成為獨立 struct 並帶 #[derive(Serialize, Deserialize)];在 Cargo.toml 加上 serde = { version = "1", features = ["derive"] } 就可運作。

輸入: {"id":42,"name":"devsmiths","createdAt":"2024-03-11T08:24:00Z","stars":1280,"public":true,"contributors":[{"login":"ada","commits":51,"admin":true},{"login":"linus","commits":33,"admin":false}],"homepage":null}

輸出(Rust):

use serde::{Deserialize, Serialize};

#[derive(Debug, Serialize, Deserialize)]
pub struct Root {
    pub id: i64,
    pub name: String,
    #[serde(rename = "createdAt")]
    pub created_at: String,
    pub stars: i64,
    pub public: bool,
    pub contributors: Vec<Contributor>,
    pub homepage: Option<serde_json::Value>,
}

#[derive(Debug, Serialize, Deserialize)]
pub struct Contributor {
    pub login: String,
    pub commits: i64,
    pub admin: bool,
}

限制與邊界情況

  • 預設 derives 是 DebugSerialize Deserialize。需要的型別請手動加上 Clone PartialEqHash——產生器保守是因為 derive everything 會拖累編譯時間。
  • 不是合法 snake_case Rust 識別字的 JSON 鍵會加上 #[serde(rename = "…")]。 若整個 struct 的鍵都採同一 casing,改用 struct 層級的 #[serde(rename_all = "camelCase")] 較精簡—— 手動整併即可,產生器逐欄輸出 rename。
  • 整數預設為 i64。若 ID 一定為正,可手動改 u64; serde 兩者皆可,但 Rust 的型別系統能讓 invariant 免費。
  • 可為 null 的欄位變為 Option<T>。若也希望缺漏鍵 反序列化為 None,請加上 #[serde(default)] (切換 all optional 時產生器會自動加上)。
  • 樣本中為 null 的欄位變為 Option<serde_json::Value>——產生器沒有資訊判斷 真實型別。確認後請手動收緊。多型情況可用 #[serde(untagged)] 在 enum 上。
  • 日期字串保持為 String。若需要型別化日期,引入 chronotime crate,改寫欄位型別並加上 #[serde(with = "chrono::serde::ts_seconds")]

常見問題

為什麼只有 Debug + Serialize + Deserialize — Clone 呢?
保守預設。為每個型別 derive Clone、PartialEq、Hash、Eq 會拖累編譯時間(每個 derive 都會跑 proc-macro),且多數 JSON DTO 不需要全部。請為特定 struct 手動加 `#[derive(Clone, PartialEq)]`。若多數型別都需要 Clone,一行 sed 即可:`s/Debug, Serialize, Deserialize/Clone, Debug, PartialEq, Serialize, Deserialize/`。
為什麼產生器逐欄輸出 serde rename,而不是 #[serde(rename_all = "camelCase")]?
因為真實 payload 中的 struct 不一定每個欄位都採同一 casing(legacy JSON 常混用 camelCase 與 snake_case)。逐欄 rename 永遠安全;`rename_all` 只在每個鍵都符合該慣例時安全。確認 struct 的鍵 casing 一致後可手動整併——struct 上的 `rename_all` 較短且編譯較快。
如何讓缺漏鍵反序列化為 None 而非錯誤?
兩種方式。欄位層級:在欄位旁加 `#[serde(default)]`——serde 使用 `Option::default()`(即 `None`)填入缺漏鍵。Struct 層級:`#[serde(default)]` 放在 struct 上會對每個欄位呼叫 `Default::default()`,需要實作或 derive `Default`。產生器的「all optional」會在每個 Option 欄位上輸出欄位層級的形式。
為什麼 null 欄位被標為 Option<serde_json::Value>?
因為樣本只顯示該欄位為 null,產生器沒有依據去產生真實型別。`serde_json::Value` 是逃生口——能反序列化任何東西。請手動收緊:若你知道該欄位有時是字串,改為 `Option<String>`;若是多型 union,用帶 `#[serde(untagged)]` 的 enum。
ISO 8601 日期字串可以變成 chrono::DateTime 嗎?
不會自動。把 `created_at: String` 改為 `created_at: DateTime<Utc>`,加上 `#[serde(with = "chrono::serde::ts_seconds")]` 或 `chrono::serde::ts_rfc3339`(依格式)。若用 `time` crate(更接近 stdlib 的替代),則為 `OffsetDateTime` 配 `serde-well-known` 功能。
i64 / u64 / f64 與 wire format 的差距有多大?
JSON 只有一種數字型別——JavaScript 的雙精度浮點。2^53 以下的值是精確整數;再大就會在資料到達 serde 之前已失去精度。對 64-bit 無號 ID(Discord snowflake、Twitter ID 等),producer 應將其序列化為字串,然後用 `#[serde(deserialize_with = ...)]` 配字串到 u64 的輔助函式。產生器無法從樣本偵測這點。

內容審閱者: