[Rustのはじめ方] Part33: JSON APIとの連携とシリアライズ

はじめに

現代の多くのアプリケーションは、外部のWeb APIと連携してデータを取得したり、操作したりします。これらのAPIの多くは、データ交換形式としてJSON (JavaScript Object Notation) を採用しています。

RustでJSON APIと連携するには、主に以下の2つの機能が必要になります。

  • HTTPクライアント: APIに対してリクエストを送信し、レスポンスを受信する機能。
  • JSONシリアライズ/デシリアライズ: Rustのデータ構造(構造体など)とJSON形式の文字列を相互に変換する機能。

このステップでは、Rustで非常に人気のあるクレート(ライブラリ)である reqwestserde を使って、JSON APIとの連携とデータのシリアライズ・デシリアライズを行う方法を学びます。

非同期処理について: ネットワーク通信は時間がかかる可能性があるため、通常は非同期処理で行われます。ここでは tokio ランタイムと async/await を使った非同期処理を前提とします。

必要なクレートの準備

まず、Cargo.tomlに必要なクレートを追加します。reqwest はHTTP通信、serde はシリアライズ/デシリアライズのフレームワーク、serde_json はJSON形式の具体的な処理、tokio は非同期ランタイムです。

Cargo.toml[dependencies] セクションに以下のように記述します。

[dependencies]
reqwest = { version = "0.12", features = ["json"] } # HTTPクライアント、json機能を有効化
serde = { version = "1.0", features = ["derive"] } # シリアライズ/デシリアライズのフレームワーク、deriveマクロを有効化
serde_json = "1.0" # JSON形式の処理
tokio = { version = "1", features = ["full"] } # 非同期ランタイム

reqwestjson フィーチャーを有効にすることで、レスポンスボディを直接JSONとしてパースしたり、リクエストボディにJSONデータを簡単に含めたりできるようになります。serdederive フィーチャーは、構造体や列挙型に #[derive(Serialize, Deserialize)] を追加するだけで、簡単にシリアライズ・デシリアライズを実装できるようにします。

serde によるシリアライズとデシリアライズ

serde はRustのデータ構造と様々なデータ形式(JSON, YAML, TOMLなど)を相互に変換するためのフレームワークです。JSONを扱うには serde_json クレートを併用します。

基本的な使い方は、変換したいRustの構造体や列挙型に serde::Serializeserde::Deserialize トレイトを実装することです。多くの場合、deriveマクロを使うのが最も簡単です。

データ構造の定義

例として、ユーザー情報を表す構造体を定義してみましょう。

use serde::{Serialize, Deserialize};
#[derive(Serialize, Deserialize, Debug)] // Debugも追加しておくと表示に便利
struct User { id: u64, username: String, email: String, active: bool,
}

#[derive(Serialize, Deserialize)] を追加するだけで、この User 構造体はJSONとの間で相互に変換可能になります。

シリアライズ (Rust → JSON)

Rustのデータ構造のインスタンスをJSON文字列に変換するには、serde_json::to_stringserde_json::to_string_pretty(整形されたJSON)を使います。

use serde_json;
fn main() { let user = User { id: 1, username: "rustacean".to_string(), email: "user@example.com".to_string(), active: true, }; // JSON文字列にシリアライズ let json_string = serde_json::to_string(&user).expect("シリアライズに失敗しました"); println!("Serialized JSON: {}", json_string); // 整形されたJSON文字列にシリアライズ let pretty_json_string = serde_json::to_string_pretty(&user).expect("シリアライズに失敗しました"); println!("\nPretty Serialized JSON:\n{}", pretty_json_string);
}
// --- User構造体の定義は省略 ---
#[derive(Serialize, Deserialize, Debug)]
struct User { id: u64, username: String, email: String, active: bool,
}
// --- ここまで ---

実行すると、user インスタンスがJSON文字列に変換されて出力されます。

デシリアライズ (JSON → Rust)

JSON文字列をRustのデータ構造のインスタンスに変換するには、serde_json::from_str を使います。

use serde_json;
fn main() { let json_data = r#" { "id": 2, "username": "ferris", "email": "ferris@rust-lang.org", "active": true } "#; // JSON文字列からUser構造体にデシリアライズ let user: User = serde_json::from_str(json_data).expect("デシリアライズに失敗しました"); println!("\nDeserialized User: {:?}", user); println!("Username: {}", user.username);
}
// --- User構造体の定義は省略 ---
#[derive(Serialize, Deserialize, Debug)]
struct User { id: u64, username: String, email: String, active: bool,
}
// --- ここまで ---

json_data 文字列が User 構造体のインスタンスに変換され、その内容が表示されます。

エラーハンドリング: 上記の例では expect を使ってエラー処理を簡略化していますが、実際のアプリケーションでは Result を適切に処理する必要があります。シリアライズやデシリアライズは失敗する可能性があります(例: JSON形式が不正、型が一致しない)。

reqwest によるAPI連携

reqwest クレートを使うと、HTTPリクエストを簡単に送信できます。ここでは、公開されているJSON API (例: JSONPlaceholder) を使って、データの取得 (GET) と送信 (POST) を試してみましょう。

GETリクエスト: APIからデータを取得してデシリアライズ

APIからJSONデータを取得し、それをRustの構造体にデシリアライズする例です。

use reqwest;
use serde::Deserialize;
use tokio; // tokioランタイムを使用
#[derive(Deserialize, Debug)]
struct Post { #[serde(rename = "userId")] // JSONのキー名とRustのフィールド名が異なる場合 user_id: u64, id: u64, title: String, body: String,
}
#[tokio::main] // main関数を非同期関数にする
async fn main() -> Result<(), reqwest::Error> { let api_url = "https://jsonplaceholder.typicode.com/posts/1"; println!("{} からデータを取得中...", api_url); // GETリクエストを送信し、レスポンスを取得 let response = reqwest::get(api_url).await?; // レスポンスボディをJSONとしてデシリアライズ // reqwestのjson()メソッドは内部でserde_jsonを呼び出す if response.status().is_success() { let post: Post = response.json().await?; println!("取得した投稿データ: {:?}", post); println!("タイトル: {}", post.title); } else { println!("エラーが発生しました: {}", response.status()); } Ok(())
}

このコードは、指定されたURLにGETリクエストを送り、返ってきたJSONデータを Post 構造体にデシリアライズして表示します。

  • #[tokio::main] マクロを使って main 関数を非同期関数のエントリーポイントにします。
  • reqwest::get(url).await? でGETリクエストを非同期に実行し、Result を処理します (?演算子)。
  • response.json::().await? でレスポンスボディを直接 Post 型にデシリアライズします。reqwestjson フィーチャーが必要です。
  • #[serde(rename = "userId")] のようにアトリビュートを使うことで、JSONのキー名(キャメルケース)とRustのフィールド名(スネークケース)の違いを吸収できます。

POSTリクエスト: RustデータをシリアライズしてAPIに送信

RustのデータをJSONにシリアライズし、それをAPIにPOSTリクエストで送信する例です。

use reqwest;
use serde::{Serialize, Deserialize};
use tokio;
#[derive(Serialize, Deserialize, Debug)]
struct NewPost { #[serde(rename = "userId")] user_id: u64, title: String, body: String,
}
#[derive(Deserialize, Debug)] // レスポンスを受け取るための構造体
struct CreatedPost { id: u64, #[serde(rename = "userId")] user_id: u64, title: String, body: String,
}
#[tokio::main]
async fn main() -> Result<(), reqwest::Error> { let api_url = "https://jsonplaceholder.typicode.com/posts"; let new_post = NewPost { user_id: 1, title: "Rustからの新しい投稿".to_string(), body: "reqwest と serde を使ってPOSTリクエストを送信しています。".to_string(), }; println!("{} にデータを送信中...", api_url); let client = reqwest::Client::new(); let response = client.post(api_url) .json(&new_post) // 送信するデータをjson()メソッドで指定 (内部でシリアライズされる) .send() .await?; if response.status().is_success() { // APIによっては作成されたリソースが返ってくる let created_post: CreatedPost = response.json().await?; println!("作成された投稿データ: {:?}", created_post); println!("新しい投稿ID: {}", created_post.id); } else { println!("エラーが発生しました: {} {}", response.status(), response.text().await?); } Ok(())
}

このコードは、NewPost 構造体のインスタンスを作成し、それをJSONにシリアライズして指定されたURLにPOSTリクエストとして送信します。

  • reqwest::Client::new() でHTTPクライアントを作成します。ヘッダーの設定など、より細かい制御が可能です。
  • client.post(url).json(&data).send().await? でPOSTリクエストを作成し、.json(&data) でリクエストボディに含めるデータを指定します。このデータは自動的にJSONにシリアライズされます。
  • APIからのレスポンス(通常、作成されたリソースの情報が含まれる)を CreatedPost 構造体にデシリアライズして表示しています。

エラーハンドリングについて

API連携では様々なエラーが発生する可能性があります。

エラーの種類原因の例対処
ネットワークエラーDNS解決失敗、接続タイムアウト、サーバーが見つからないreqwest::Error を適切にハンドリングする。リトライ処理を検討する。
HTTPステータスコードエラー404 Not Found, 401 Unauthorized, 500 Internal Server Error などresponse.status() をチェックし、ステータスコードに応じた処理を行う。エラーレスポンスのボディ(JSON形式の場合もある)をパースして詳細情報を取得する。
デシリアライズエラーJSON形式が不正、期待するフィールドが存在しない、型が一致しないresponse.json()serde_json::from_str が返す Result を適切に処理する。エラー内容をログに出力するなどして原因を特定する。
シリアライズエラー(通常は稀だが)データ構造がシリアライズ不可能な場合serde_json::to_string などが返す Result を処理する。

実際のアプリケーションでは、これらのエラーを網羅的に考慮し、ユーザーに分かりやすいフィードバックを返したり、ログに記録したりする堅牢なエラーハンドリング機構を実装することが重要です。

Rustの Result 型と ? 演算子、そして thiserroranyhow といったエラーハンドリング支援クレートを活用すると、エラー処理をより簡潔かつ効果的に記述できます。

まとめ

このステップでは、RustでJSON APIと連携するための基本的な方法を学びました。

  • serdeserde_json を使って、Rustのデータ構造とJSON文字列を相互に変換(シリアライズ・デシリアライズ)する方法。
  • reqwest を使って、HTTPのGETリクエストやPOSTリクエストを送信し、APIと通信する方法。
  • async/awaittokio を使った非同期処理の基本。
  • API連携における基本的なエラーハンドリングの考え方。

これらの知識は、Webサービス、CLIツール、デスクトップアプリケーションなど、ネットワーク通信を行う多くのRustアプリケーション開発において不可欠です。ぜひ実際に手を動かして、様々なAPIとの連携を試してみてください!

コメントを残す

メールアドレスが公開されることはありません。 が付いている欄は必須項目です