serde

【Rust】serde で JSON を扱う基本を分かりやすく解説

【Rust】serde で JSON を扱う基本を分かりやすく解説
naoki-hn

Rust で serde を用いて JSON を扱う方法について、初心者にも分かりやすく解説します。

serde クレートの概要

serde とは

Web API のレスポンスや設定ファイルなど、開発現場では、JSON 形式のデータを扱うことが多くあります。プログラム内の型のデータを JSON のような文字列形式に変換することを「シリアライズ(serialize)」、逆に JSON 文字列を型のデータに変換することを「デシリアライズ(deserialize)」と言います。

Rust では、このシリアライズ・デシリアライズで serde (サード、サーディと読みます)というクレートを使用します。この記事では、serde を使った JSON の基本的な扱い方について紹介します。

この記事で理解してもらいたい内容は以下の通りです。

  • serde は、シリアライズ・デシリアライズの共通の仕組みを提供する。
  • JSON を扱うには serde_json と組み合わせて使う。
  • #[derive(Serialize)] で型をJSONに変換でき、#[derive(Deserialize)] で JSON を型に変換できる。
  • ネストした型や、Vec<T>Option<T> などを含む型もそのまま扱える。

以降では、上記について例を用いつつ紹介していきます。

serdeserde_json

serde 本体は、特定のデータ形式(フォーマット)に依存しない共通の仕組みです。そのため、serde 自体には、「JSON に変換する」「JSON から変換する」といった具体的な処理はありません。

実際に、JSON を扱うためには、JSON フォーマットに対応した serde_json というクレートを組み合わせて使用します。このクレートは、serde の仕組みを利用して、JSON フォーマットの読み書きを実装しています。

このように serde は形式に依存しない設計になっているおかげで、JSON 以外にも YAML (serde_yaml) や TOML (toml) など、各フォーマットに対応するクレートを使って同じ書き方で使用することができます。この記事では、最もよく使われる JSON を例に紹介します。

この記事では、基本となる構造体の型 (struct) を中心に解説しますが、serde は、列挙型 (enum) も扱うことができます。

serde を使用した JSON 操作の基本

serde の導入方法

serdeserde_json は、他のクレートと同様に cargo add または Cargo.toml に追記することで利用できます。

注意点

#[derive(Serialize, Deserialize)] というマクロを使用するためには、serde の derive フィーチャーの有効化が必要である点に注意が必要です。導入時には必ず指定するようにしましょう。

cargo add で追加する場合

cargo add serde --features derive
cargo add serde_json

Cargo.toml に追記する場合

[dependencies]
serde = { version = "1", features = ["derive"] }
serde_json = "1"

例では、メジャーバージョンとして "1" を指定しています。これにより、互換性のある範囲で最新のバージョンが使用されます。もちろん特定のバージョンを指定しても構いません。

導入時には crates.io の serdeserde_json のページを確認してください。なお、実際にインストールされたバージョンは Cargo.lock ファイルで確認できます。

クレートや依存関係の基本については以下を参考にしてください。

クレート(バイナリ/ライブラリ)と依存関係の管理を分かりやすく解説

型を JSON に変換する(Serialize

基本的な方法

構造体 (struct) を JSON 文字列に変換するシリアライズ (Serialize) から見ていきましょう。対象の型に #[derive(Serialize)] を付与し、serde_json::to_string 関数を使用することで、JSON 形式の文字列に変換することができます。

以下の例では、User という型を定義し、JSON 文字列に変換しています。

use serde::Serialize;

// 構造体を定義し、Serialize トレイトを derive することで
// 構造体を JSON にシリアライズできるようにする
#[derive(Serialize, Debug)]
struct User {
    name: String,
    age: u32,
}

fn main() -> Result<(), serde_json::Error> {
    let user = User {
        name: String::from("Alice"),
        age: 30,
    };

    // 構造体を JSON 文字列に変換(シリアライズ)
    let json = serde_json::to_string(&user)?;
    println!("{json}");

    Ok(())
}
【実行結果】
{"name":"Alice","age":30}

定義した型に対して #[derive(Serialize)] を付けておくことで、serde_json::to_string にわたすだけで JSON 文字列に変換できていることが分かります。

なお、serde_json::to_string は変換に失敗する可能性があることから、戻り値は Result 型となります。今回の例では、? 演算子を使ってエラー (serde_json::Error) を上位へ伝播しています。もちろん match を用いて処理する方法も適切ですので設計に応じて検討しましょう。

エラー処理の基本については以下を参考にしてください。

エラー処理の基本を分かりやすく解説

改行やインデントを入れて整形する方法

改行やインデントを入れて整形した、人間が読みやすい形式の JSON 文字列にしたい場合は、serde_json::to_string_pretty を使用します。

use serde::Serialize;

#[derive(Serialize, Debug)]
struct User {
    name: String,
    age: u32,
}

fn main() -> Result<(), serde_json::Error> {
    let user = User {
        name: String::from("Alice"),
        age: 30,
    };

    // 読みやすいように改行・インデント付きの JSON 文字列に変換する
    let json = serde_json::to_string_pretty(&user)?;
    println!("{json}");

    Ok(())
}
【実行結果】
{
  "name": "Alice",
  "age": 30
}

設定ファイルの出力やデバッグ時など、見やすさを重視したい場合には、serde_json::to_string_pretty を使用するのが便利です。

JSON を型に変換する(Deserialize

次に、JSON 文字列を構造体 (struct) に変換するデシリアライズ (Deserialize) を見ていきましょう。対象の型に #[derive(Deserialize)] を付与し、serde_json::from_str 関数を使用することで、JSON 形式の文字列から構造体に変換することができます。

use serde::Deserialize;

// 構造体を定義し、Deserialize トレイトを derive することで
// JSON 文字列を構造体にデシリアライズできるようにする
#[derive(Deserialize, Debug)]
struct User {
    name: String,
    age: u32,
}

fn main() -> Result<(), serde_json::Error> {
    let json = r#"{"name":"Alice","age":30}"#;

    // JSON 文字列を構造体に変換(デシリアライズ)
    let user: User = serde_json::from_str(json)?;
    println!("{user:?}");

    Ok(())
}
【実行結果】
User { name: "Alice", age: 30 }

定義した型 (User)に対して #[derive(Deserialize)] を付けておき、serde_json::from_str に JSON 文字列を渡せば User 構造体に変換できます。なお、「let user: User」 のように変換先の型注釈を明示している点に注意してください。serde_json::from_str は様々な型に変換できる関数のため、どの型に変換するのかを伝える必要があります。

また、serde_json::from_str も変換に失敗する可能性があることから、戻り値は Result 型となります。例えば、フィールドが一致しない場合や、型が合わない場合にエラーとなります。

JSON 文字列を r#"…"# という形式で記載しています。これは raw 文字列リテラル と呼ばれる書き方です。文字列中のダブルクォート(")をエスケープせずにそのまま書けるため、JSON のように " を含む文字列を扱う際に便利です。

SerializeDeserialize を同時に使用する

実際の開発では、ある型に対してシリアライズとデシリアライズを両方行えるようにするケースがほとんどです。その場合には、#[derive(Serialize, Deserialize)] のように両方をまとめて指定します。

use serde::{Deserialize, Serialize};

// Serialize と Deserialize の両方を derive することで
// シリアライズとデシリアライズの両方に対応できる
#[derive(Serialize, Deserialize, Debug)]
struct User {
    name: String,
    age: u32,
}

fn main() -> Result<(), serde_json::Error> {
    let user = User {
        name: String::from("Alice"),
        age: 30,
    };

    // 構造体を JSON 文字列に変換(シリアライズ)
    let json = serde_json::to_string(&user)?;
    println!("{json}");

    // JSON 文字列を構造体に変換(デシリアライズ)
    let deserialized_user: User = serde_json::from_str(&json)?;
    println!("{deserialized_user:?}");

    Ok(())
}
【実行結果】
{"name":"Alice","age":30}
User { name: "Alice", age: 30 }

以降では、いくつかよく使用するパターンを紹介します。その際には、#[derive(Serialize, Deserialize)] のように両方を付けた形で紹介していきます。

よく使用するパターン

ネストした型を含む場合

型を定義する際に、構造体のフィールドに別の構造体を持たせることはよくあります。このような場合に、シリアライズ・デシリアライズをする場合には両方に #[derive(Serialize, Deserialize)] を付けて定義しておく必要があることに注意してください。

以下の例では、User 構造体のフィールドとして Address 構造体を持っています。

use serde::{Deserialize, Serialize};

#[derive(Serialize, Deserialize, Debug)]
struct Address {
    city: String,
    zip: String,
}

#[derive(Serialize, Deserialize, Debug)]
struct User {
    name: String,
    address: Address,
}

fn main() -> Result<(), serde_json::Error> {
    let user = User {
        name: String::from("Alice"),
        address: Address {
            city: String::from("Tokyo"),
            zip: String::from("100-0001"),
        },
    };

    // 構造体を JSON 文字列に変換(シリアライズ)
    let json = serde_json::to_string_pretty(&user)?;
    println!("{json}");

    // JSON 文字列を構造体に変換(デシリアライズ)
    let deserialized_user: User = serde_json::from_str(&json)?;
    println!("{deserialized_user:?}");

    Ok(())
}
【実行結果】
{
  "name": "Alice",
  "address": {
    "city": "Tokyo",
    "zip": "100-0001"
  }
}
User { name: "Alice", address: Address { city: "Tokyo", zip: "100-0001" } }

例のようにネストした構造体を含めて JSON 文字列に変換できていることが分かります。また、デシリアライズでも、ネストした JSON をそのまま User 構造体に変換できています。

Vec<T> や配列のフィールドを含む場合

複数の値を持つコレクションである Vec<T> についても特別な対応をすることなく、そのまま扱うことが可能です。Vec<T> は JSON の配列に対応します。

以下の例では、先ほどの例における AddressVec<Address> として表現しています。

use serde::{Deserialize, Serialize};

#[derive(Serialize, Deserialize, Debug)]
struct Address {
    city: String,
    zip: String,
}

#[derive(Serialize, Deserialize, Debug)]
struct User {
    name: String,
    addresses: Vec<Address>,
}

fn main() -> Result<(), serde_json::Error> {
    let user = User {
        name: String::from("Alice"),
        addresses: vec![
            Address {
                city: String::from("Tokyo"),
                zip: String::from("100-0001"),
            },
            Address {
                city: String::from("Osaka"),
                zip: String::from("530-0001"),
            },
        ],
    };

    // 構造体を JSON 文字列に変換(シリアライズ)
    let json = serde_json::to_string_pretty(&user)?;
    println!("{json}");

    // JSON 文字列を構造体に変換(デシリアライズ)
    let deserialized_user: User = serde_json::from_str(&json)?;
    println!("{deserialized_user:?}");

    Ok(())
}
【実行結果】
{
  "name": "Alice",
  "addresses": [
    {
      "city": "Tokyo",
      "zip": "100-0001"
    },
    {
      "city": "Osaka",
      "zip": "530-0001"
    }
  ]
}
User { name: "Alice", addresses: [Address { city: "Tokyo", zip: "100-0001" }, Address { city: "Osaka", zip: "530-0001" }] }

Vec<Address> が JSON の配列として表現されていることが分かります。例は、作成した構造体に対してですが、Vec<i32>Vec<String> のように基本的な型の場合も同様です。

なお、[T; N] のような固定長配列も同様に扱えますが、デシリアライズ時に要素数が一致しないとエラーになります。実用上は要素数を柔軟に扱える Vec<T> を使うことがほとんどです。

Option<T> のフィールドを含む場合

値が存在する場合と存在しない場合がある場合には、Option<T> を使用します。serde では、Option<T> も自然に扱うことができます。

use serde::{Deserialize, Serialize};

#[derive(Serialize, Deserialize, Debug)]
struct User {
    name: String,
    email: Option<String>,
}

fn main() -> Result<(), serde_json::Error> {
    let user1 = User {
        name: String::from("Alice"),
        email: Some(String::from("alice@example.com")),
    };
    let user2 = User {
        name: String::from("Bob"),
        email: None,
    };

    // 構造体を JSON 文字列に変換(シリアライズ)
    let json1 = serde_json::to_string(&user1)?;
    let json2 = serde_json::to_string(&user2)?;
    println!("{json1}");
    println!("{json2}");

    // JSON 文字列を構造体に変換(デシリアライズ)
    let deserialized_user1: User = serde_json::from_str(&json1)?;
    let deserialized_user2: User = serde_json::from_str(&json2)?;
    println!("{deserialized_user1:?}");
    println!("{deserialized_user2:?}");

    Ok(())
}
【実行結果】
{"name":"Alice","email":"alice@example.com"}
{"name":"Bob","email":null}
User { name: "Alice", email: Some("alice@example.com") }
User { name: "Bob", email: None }

Option<T> を使用する場合、Nonenull として JSON 文字列に出力されます。デシリアライズの場合は、JSON 文字列に当該キーが存在しないか、null であれば None に、値があれば Some(値) という形に変換されます。

null を出力せずキー自体を出力したくない場合

Option 型の値が None の際に、null を出力するのではなく、キー自体を出力したくないような場合には、#[serde(skip_serializing_if = "Option::is_none")] を対象のフィールドに付与します。

...(省略)...

#[derive(Serialize, Deserialize, Debug)]
struct User {
    name: String,
    // None の場合はキーごと出力しない
    #[serde(skip_serializing_if = "Option::is_none")]
    email: Option<String>,
}
...(省略)...
【実行結果】
{"name":"Alice","email":"alice@example.com"}
{"name":"Bob"}
User { name: "Alice", email: Some("alice@example.com") }
User { name: "Bob", email: None }

例の結果のようにキー自体が出力されなくなったことが分かります。

上記のような出力制御のための属性は、構造体自体やフィールドなどに付与することができます。詳細は公式ページのこちらを参照してください。

まとめ

Rust で serde を用いて JSON を扱う方法について解説しました。serde の入門として最低限理解してほしい内容を中心に説明し、よくある使用例なども紹介しました。

serde は、外部 API との通信や設定ファイルの読み書きなど、様々な場面で利用される非常に重要なクレートで、Rust の学習時に代表例としてよく出てくるものです。今回紹介した基本をしっかりと押さえておくことで今後の様々なクレートの活用にもつながります。

serde の公式ドキュメントは以下を参考にしてください。

ソースコード

上記で紹介しているソースコードについては GitHub にて公開しています。参考にしていただければと思います。

あわせて読みたい
Rust プログラミング入門
Rust プログラミング入門

ABOUT ME
ホッシー
ホッシー
システムエンジニア
はじめまして。当サイトをご覧いただきありがとうございます。
私は製造業のメーカーで、DX推進や業務システムの設計・開発・導入を担当しているシステムエンジニアです。これまでに転職も経験しており、以前は電機メーカーでシステム開発に携わっていました。

これまでの業務を通じてさまざまなプログラミング言語や技術に触れてきましたが、その中でもRustの設計思想に惹かれ、この言語についてもっと深く学びたい、そしてその魅力を発信していきたいと思い、このサイトを立ち上げました。

自身の学びを整理しつつ、同じようにRustに興味を持つ方のお役に立てるような情報を発信していければと思っています。どうぞよろしくお願いいたします。

※キャラクターデザイン:ゼイルン様
記事URLをコピーしました