JSON を Rust 構造体に変換:serde struct 生成ガイド(2026)
ペイロードを JSON to Rust 変換ツール に貼り付け、生成された serde struct をコピーする。JSON から Rust 構造体への作業はこれで全部だ。インストール不要、アップロードもなし、すべてブラウザ内で完結する。「今すぐ構造体が欲しい」という場面を数秒で片付けてくれる。
とはいえ、構造体を生成することと、正しい構造体を生成することは別の問題だ。変換ツールにできるのは推測だけ。そして Rust では、この推測の失敗のコストが跳ね上がる。TypeScript なら推測を外しても、無視できる緩い any に落ちるだけだ。ところが serde では、推測を外すとデシリアライズの時点で失敗する。float が i64 に流し込まれたり、非オプショナルなフィールドに予期しない null が来たりすると、serde_json::from_str はデータの代わりに Err を返してくる。だからこそ、構造体を正しく作ることは、ほとんどの言語よりも Rust で重みを持つ。
以下では、推論が実際にどう動くのか、多くの変換ツールが間違える数値型付けのルール、#[serde(rename)] と #[serde(rename_all)] をいつ使い分けるか、そして出力が常にコンパイルできるよう日付・動的キー・Rust の予約語をどう扱うかを解説する。
JSON を Rust に変換する方法
JSON を Rust へ変換するのは 3 ステップだ。
- JSON を貼り付ける。 オブジェクト、配列、あるいは生の API レスポンスを入力ボックスに落とし込む。変換は即座に、完全にクライアントサイドで走る。
- 出力を調整する。 ルート構造体の名前を変える、serde の derive を切り替える、
DebugやCloneを足す、あるいはpubの可視性を外して、自分のクレートのスタイルに合わせる。 - コピーまたはダウンロードする。 生成された Rust をワンクリックで取得し、そのままプロジェクトに貼り付ける。
トランザクション的な流れはこれで全部だ。入力が minify されている、あるいは妥当か自信がない場合は、まず JSON整形ツール に通し、変換ツールがきれいで整形済みの JSON から作業できるようにする。このガイドの残りでは、ツールが独力で推論できないケースを自分で直せるよう、出力の読み方を説明する。
JSON の型は Rust にどう対応するか
すべての JSON 値には Rust の対応物があるが、その対応は完全な一対一ではない。
| JSON 値 | Rust 型 |
|---|---|
"text" | String |
42 | i64(大きな値は u64、それを超えると f64) |
3.14、2e3 | f64 |
true / false | bool |
null | Option<T>(または Option<serde_json::Value>) |
[1, 2, 3] | Vec<T> |
{ ... } | 名前付きの struct |
作業の中心になるのはオブジェクトだ。典型的な REST ペイロードを見てみよう。
{
"id": 101,
"name": "Ada Lovelace",
"email": "ada@example.com",
"active": true,
"roles": ["admin", "user"]
}
変換ツールは serde 対応の構造体を生成する。
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Root {
pub id: i64,
pub name: String,
pub email: String,
pub active: bool,
pub roles: Vec<String>,
}
各スカラーはプリミティブに対応し、roles は Vec<String> になる。きれいに対応しない唯一の JSON 型が number だ。JSON には数値型がひとつしかないのに対し、Rust は i64・u64・f64 から選ばせてくる。この選択については後ほど丸ごと一節を割く。ほとんどのジェネレーターが間違えるのがここだからだ。
変換ツールはどう構造体を推論するか
投げ込むもののほぼすべてをカバーするのは 4 つのルールだ。オブジェクトの形ごとに 1 つの構造体、配列のキー単位マージ、精密な数値型付け、そして慣用的なフィールド名。
構造的推論:オブジェクトごとに名前付き構造体を 1 つ
異なるオブジェクトの形はそれぞれ独立した名前付き構造体になる。ネストされたオブジェクトはインライン化されず、別の参照される定義として引き上げられる。
{ "repo": "serde", "owner": { "login": "dtolnay", "id": 100 } }
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Root {
pub repo: String,
pub owner: Owner,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Owner {
pub login: String,
pub id: i64,
}
ネストされた owner は独自の Owner 構造体になり、フィールドから参照される。同一の形は重複排除されるので、同じ構造を持つ 2 つのフィールドは、コピーを生成する代わりに単一の構造体を共有する。
配列のマージと Option フィールド
オブジェクトの配列を渡すと、変換ツールはそれらをキー単位でマージする。一部の要素には存在するが他にはないキーは Option になる。
{ "users": [{ "id": 1, "nick": "x" }, { "id": 2 }] }
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Root {
pub users: Vec<User>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct User {
pub id: i64,
pub nick: Option<String>,
}
id はすべての要素に現れるので、必須のままだ。nick は 2 人目のユーザーに欠けているので、Option<String> になる。ここにないものに注目してほしい。#[serde(default)] がない。serde はすでに Option をオプショナルとして扱い、欠けているキーを None にデシリアライズするので、この属性は冗長になる。
正しい数値型付け:i64、u64、f64
これは競合ツールが飛ばしているルールであり、実際のペイロードで壊れるのもここだ。ジェネレーターは 3 段階を適用する。整数は i64 に対応する。値が i64::MAX を超えると u64 に昇格する。u64::MAX を超えると f64 にフォールバックする。1.0 や 2e3 のように小数点や指数付きで書かれたトークンは、大きさに関係なく f64 に対応する。
{ "user_id": 12000000000000000000, "balance": 19.99, "retries": 3 }
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Root {
pub user_id: u64,
pub balance: f64,
pub retries: i64,
}
ここでは user_id が i64::MAX より大きいので u64 に収まり、それでもラウンドトリップできる。balance は小数点を持つので、整数ではなく f64 だ。これが重要なのは、serde がデシリアライズ時に型を強制するからだ。snowflake や Twitter 風の ID を i32 として型付けした構造体を渡すと値がオーバーフローする。float フィールドを i64 として型付けすると、serde は黙って切り捨てる代わりにエラーを返す。この 3 段階のルールこそが、生成された rust struct from json を、最初の大きな ID でパニックさせず、デシリアライズし続けさせるものだ。
snake_case フィールドと #[serde(rename)]
JSON のキーはしばしば camelCase だが、慣用的な Rust のフィールドは snake_case だ。変換ツールはフィールド名を変え、正確な JSON キーへ対応づける #[serde(rename)] を追加する。
{ "login": "octocat", "publicRepos": 15, "followerCount": 9001, "createdAt": "2011-01-25" }
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Root {
pub login: String,
#[serde(rename = "publicRepos")]
pub public_repos: i64,
#[serde(rename = "followerCount")]
pub follower_count: i64,
#[serde(rename = "createdAt")]
pub created_at: String,
}
login のようにすでに snake_case のキーには rename が付かない。他のものはワイヤーフォーマットへきれいに対応づけられつつ、Rust のコードは自然に読める。
#[serde(rename)] と #[serde(rename_all)]
ツールがフィールドごとの #[serde(rename)] を出力するのは、それが常に機能するからだ。ひとつのペイロードが同じオブジェクト内で camelCase・snake_case・不規則なキーを混在させていても効く。存在しないかもしれない共通の規約について推論する必要が一切ない。
構造体のすべてのフィールドがひとつの規約を共有している場合は、それらの属性をコンテナレベルの単一の rename にまとめられる。フィールドごとの行を削除し、構造体に #[serde(rename_all = "camelCase")] をひとつ付ける。
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct Root {
pub login: String,
pub public_repos: i64,
pub follower_count: i64,
pub created_at: String,
}
どちらのバージョンも同じ JSON をデシリアライズする。混在または不規則なキーにはフィールドごとの rename を、構造体が均一でノイズを減らしたいときは rename_all を使う。serde は同じ属性向けに "snake_case"・"kebab-case"・"SCREAMING_SNAKE_CASE" といった他の規約も備えている。
Rust の予約語と識別子にできないキー
JSON のキーはただの文字列なので、API が type や match という名前のキーを送ってくるのを止めるものは何もない。どちらも Rust では予約語だ。ジェネレーターはこれらを合法な識別子と rename にサニタイズする。
{ "type": "user", "match": true, "first-name": "Ada" }
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Root {
#[serde(rename = "type")]
pub type_: String,
#[serde(rename = "match")]
pub match_: bool,
#[serde(rename = "first-name")]
pub first_name: String,
}
末尾アンダースコアの形(type_)は、r#type のような生の識別子よりもあえて選ばれている。生の識別子はすべての位置ですべての予約語を表現できるわけではなく、特に self・crate・super は生の識別子として拒否される。だからサニタイズ+rename のアプローチが、常にコンパイルできる唯一の方法だ。first-name や先頭が数字の 2fa のような識別子にできないキーも同じ扱いを受ける。妥当な Rust の名前と、リテラルな JSON キーへの rename だ。
serde_json でデシリアライズする
構造体が手に入ったら、依存関係を Cargo.toml に追加する。
[dependencies]
serde = { version = "1", features = ["derive"] }
serde_json = "1"
あとはパースは 1 行だ。この例は完結していて、そのままコンパイルできる。
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Root {
pub id: i64,
pub name: String,
pub email: String,
pub active: bool,
pub roles: Vec<String>,
}
fn main() -> Result<(), serde_json::Error> {
let data = r#"{"id":101,"name":"Ada Lovelace","email":"a@ex.com","active":true,"roles":["admin"]}"#;
let root: Root = serde_json::from_str(data)?;
println!("{} has {} role(s)", root.name, root.roles.len());
Ok(())
}
Deserialize derive がすべての仕事をこなす。文字列には serde_json::from_str、バイトスライスには from_slice、ファイルや HTTP ボディには from_reader を使い、値を JSON に戻すには serde_json::to_string を使う。これが型を正しく作ったことへの見返りだ。生成した serde derive はランタイムのバリデーターなので、Ok を返す serde_json deserialize の呼び出しは、データが構造体に一致したという保証であって、コンパイラが一致すると思い込んでいるだけではない。
日付、動的キー、未知のフィールド
一部の形は素の構造体フィールドには対応しない。ジェネレーターはそうしたものに妥当なデフォルトを与え、あとは自分で手動で締めていく。
日付。 JSON には日付型がないので、ISO や RFC 3339 のタイムスタンプは素の String として届く。本格的な日付処理には、フィールドを chrono の型に切り替え、chrono の serde 機能を有効にする(chrono = { version = "0.4", features = ["serde"] })。すると serde が RFC 3339 を自動的にパースする。
use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Event {
pub name: String,
pub created_at: DateTime<Utc>,
}
動的キー。 ID から値へのマップのようにキーがランタイムで変わる場合、固定の構造体は誤った形だ。String をキーとする HashMap を使う。
use std::collections::HashMap;
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Root {
pub scores: HashMap<String, i64>,
}
未知の余分なフィールド。 型付き構造体を保ちつつ、モデル化しなかったキーも取り込むには、HashMap を裏付けにした #[serde(flatten)] フィールドを追加する。本当に動的な値には serde_json::Value が万能の受け皿になる。本当に必要なのが型の生成ではなく構造の検証である場合、JSON Schema 検証ガイド が契約を端から端まで強制する方法をカバーしている。
json-to-rust vs quicktype vs typeshare vs 手書き
JSON から Rust の型を作る唯一の最善の方法はない。JSON がどこに存在し、どちらの方向へ変換するかによる。
| アプローチ | 向いている用途 | 備考 |
|---|---|---|
| オンライン変換ツール(本ツール、transform.tools) | 単発の変換、機密なペイロード、インストール不要 | サンプルから推論、完全にクライアントサイド |
| quicktype | 多言語出力、パイプラインのコード生成 | 同じくサンプル駆動。CLI またはウェブ |
| typeshare | 既存の Rust 型を TS、Swift、Kotlin と共有 | 逆方向:Rust から他言語へ |
| schemafy / typify | JSON Schema からの生成 | 入力はスキーマであってサンプルではない |
| 構造体を手書き | 小さなペイロード、serde の学習 | 完全な制御、規模が大きいと手間でミスも起きやすい |
腹に落とす価値のある違いはこうだ。オンライン変換ツールと quicktype はどちらも JSON のサンプルから型を推論するのに対し、typeshare は逆方向に走り、注釈付きの Rust 型を TypeScript や Swift に変える。schemafy と typify は例データではなく JSON Schema を入力に取る。コードベースが Rust ではなく TypeScript なら、JSON to TypeScript 変換ツール に同じサンプル駆動のアプローチが当てはまり、JSON to TypeScript インターフェースガイド がそちら側を掘り下げてカバーしている。
JSON から Rust を生成するときのよくある落とし穴
生成された構造体は出発点だ。ライブデータで出力を信頼する前に、以下に気をつけてほしい。
- 単一のサンプルではすべての形が明らかになることはめったにない。 オプショナルなフィールドは複数の配列要素からしか推論できない。
Optionの推論が 1 個の運のいいオブジェクトからの推測ではなく正確になるよう、代表的な配列を貼り付ける。 - 空の配列や混在した配列は
serde_json::Valueにフォールバックする。 推論する材料が何もないのだから、それが正直な答えだ。具体的な要素型を得るには、より豊かなサンプルを与える。 - 大きな ID を
i32に狭めてはいけない。 Snowflake などの ID は 2^53 を超え、32 ビットのフィールドをオーバーフローさせる。生成されたi64またはu64を保つこと。 - 常に
nullのフィールドは、素のオプショナルではなくOption<serde_json::Value>になる。 ツールには推論する型がないので、プレースホルダーとして扱い、型が分かったら本物の型を与える。 serde_json::Valueにはserde_json依存が必要だ。 出力にValueが含まれていてCargo.tomlにそのクレートがなければ、コードはコンパイルされない。Stringのままの日付では日付計算ができない。 タイムスタンプを比較したり整形したりする予定なら、文字列を手でパースする代わりに chrono の型に切り替える。
よくある質問
構造体が正しく見えても serde が私の JSON を拒否するのはなぜ?
ほぼ常に型の不一致だ。典型的な原因は 2 つで、浮動小数点値が i64 フィールドに着地しているケースと、ときどき欠けるのに Option として型付けされていないキーだ。serde はデシリアライズ時に型を強制するので、フィールドを f64 や Option<T> に直すか、代表的なサンプルから構造体を再生成する。
JSON の整数はどの Rust 数値型になるべき?
デフォルトは i64。値が i64::MAX を超えるときは u64 に昇格させ、u64::MAX を超えたら f64 にフォールバックする。小数点や指数付きで書かれた数値は、サイズに関係なく f64 に対応する。serde は整数フィールドにデシリアライズされた float を拒否するからだ。
JSON がフィールドを省略するとき、そのフィールドをオプショナルにするには?
Option<T> として型付けする。serde は Option を自動的にオプショナルとして扱い、欠けているキーを None にデシリアライズするので、#[serde(default)] は不要だ。変換ツールは、サンプルの一部のアイテムにキーが欠けているとき、フィールドに Option を付ける。
#[serde(rename)] と #[serde(rename_all)] のどちらを使うべき?
ペイロードが命名スタイルを混在させているときは、フィールドごとの #[serde(rename)] を使う。常に機能する。構造体のすべてのフィールドがひとつの規約に従っているなら、フィールドごとの属性を削除し、構造体に #[serde(rename_all = "camelCase")] をひとつ付ける。どちらも同一にデシリアライズする。
type のような Rust の予約語である JSON キーはどう扱う?
ジェネレーターは、元のキーへ戻す #[serde(rename = "type")] とともに type_ を出力する。これは r#type のような生の識別子より堅牢だ。生の識別子はすべての位置で self・crate・super をカバーできないからだ。
JSON の日付が date 型ではなく String として型付けされるのはなぜ?
JSON には日付型がないので、タイムスタンプはワイヤー上では単なる文字列であり、String が正直なデフォルトだ。本格的な日付処理には、フィールドを chrono の DateTime<Utc> や NaiveDate に変え、chrono の serde 機能を有効にする。すると serde が RFC 3339 をパースしてくれる。
生成された構造体に JSON をデシリアライズするには?
serde_json を Cargo.toml に追加し、let root: Root = serde_json::from_str(json)?; と書く。残りは Deserialize derive が処理する。バイトスライスには from_slice、ファイルや HTTP ボディには from_reader を使う。
オンラインの JSON to Rust struct 変換ツールを使うとき、私の JSON は非公開?
そのとおり。変換は JavaScript で 100% ブラウザ内で走る。トークン・ID・顧客データを含むあなたの JSON は、ページを離れることもサーバーに送られることも決してない。準備ができたら、自分のペイロードで JSON to Rust 変換ツールを試す。