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

Rust

はじめに 👋

現代の多くのアプリケーションは、外部の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との連携を試してみてください!🚀

コメント

タイトルとURLをコピーしました