SerdeによるRustでのきめ細かなJSONシリアライゼーション制御
Min-jun Kim
Dev Intern · Leapcell

はじめに
現代のWebサービスやデータ交換の世界では、JSONはユビキタスなフォーマットです。Rustは、パフォーマンスと安全性に重点を置いており、効率的なシリアライゼーションとデシリアライゼーションのためにserdeクレートをよく利用します。serdeは通常、そのマジックをシームレスに実行しますが、デフォルトのシリアライゼーション動作が理想的とは言えないシナリオが頻繁に発生します。おそらく、APIはsnake_caseのフィールド名を期待しているが、Rustの構造体はcamelCaseを好むかもしれません。あるいは、フィールドがNoneである場合にJSONペイロードからオプションフィールドを省略する必要があるかもしれません。これらの些細に見える不一致は、システム統合やデータ処理において大きな摩擦を引き起こす可能性があります。幸いなことに、serdeは#[serde(rename_all)] や #[serde(skip_serializing_if)] のような強力な属性を提供しており、シリアライゼーションプロセスをきめ細かく制御でき、JSON出力をニーズに合わせて正確に調整できます。この記事では、これらの属性を掘り下げ、これらが開発者に高度にカスタマイズされた相互運用可能なJSON表現を作成することをどのように可能にするかを実証します。
Serdeのシリアライゼーション制御の理解
実践的な例に進む前に、議論に関連するコアなserdeの概念を簡単に定義しましょう。
- シリアライゼーション: データ構造(Rustの
structやenumなど)を、送信または保存に適した形式(JSON文字列など)に変換するプロセス。 - デシリアライゼーション: 外部形式からRustデータ構造へのデータの逆プロセス。
#[derive(Serialize)]:serdeによって提供されるプロシージャルマクロで、Rust型がシリアライズされるために必要なコードを自動生成します。- 属性: コンパイラまたは他のツールにメタデータを提供するためにRustで使用される特別な注釈。
serdeはカスタマイズに属性を利用します。
さて、シリアライゼーションのスーパーパワーを与えてくれる主要な属性を探ってみましょう。
#[serde(rename_all)] によるフィールド名の変更
外部APIや既存システムとの統合において、共通の課題は命名規則の調和です。Rustは通常、フィールド名にsnake_caseを好みますが、多くのJSON API、特にJavaScriptやJavaエコシステムに由来するものは、camelCase、PascalCase、あるいはkebab-caseを使用するかもしれません。#[serde(rename = "new_name")] を使用して各フィールドを手動で名前変更することは、大きな構造体にとっては面倒でエラーが発生しやすくなります。
構造体レベルで適用される#[serde(rename_all)] 属性は、シリアライゼーション中(#[derive(Deserialize)]も存在する場合、デシリアライゼーション中)にその構造体内のすべてのフィールドに命名規則の変換を自動的に適用することで、便利なソリューションを提供します。
例を挙げましょう。
use serde::{Serialize, Deserialize}; #[derive(Serialize, Deserialize, Debug)] #[serde(rename_all = "camelCase")] struct UserProfile { user_id: String, first_name: String, last_name: String, email_address: Option<String>, } fn main() { let user = UserProfile { user_id: "u123".to_string(), first_name: "Alice".to_string(), last_name: "Smith".to_string(), email_address: Some("alice.smith@example.com".to_string()), }; let json = serde_json::to_string_pretty(&user).unwrap(); println!("Serialized JSON (camelCase):\n{}", json); // 出力: // { // "userId": "u123", // "firstName": "Alice", // "lastName": "Smith", // "emailAddress": "alice.smith@example.com" // } let json_input = r#"{ "userId": "u456", "firstName": "Bob", "lastName": "Johnson", "emailAddress": null }"#; let deserialized_user: UserProfile = serde_json::from_str(json_input).unwrap(); println!("Deserialized User: {:?}", deserialized_user); // 出力: Deserialized User: UserProfile { user_id: "u456", first_name: "Bob", last_name: "Johnson", email_address: None } }
この例では、#[serde(rename_all = "camelCase")] は user_id を userId に、first_name を firstName に、というように自動的に変換します。その他の一般的な変換には、snake_case、PascalCase、kebab-case、SCREAMING_SNAKE_CASE があります。これにより、ボイラープレートが大幅に削減され、コードの可読性が向上します。
#[serde(skip_serializing_if)] によるフィールドの条件付き省略
もう一つの頻繁な要件は、特定の条件下でシリアライズされた出力からフィールドを省略することです。例えば、オプションフィールドがNoneの場合、nullを送信するのではなく、完全にJSONから除外したい場合があります。あるいは、フィールドがデフォルト値を持っている場合に省略したい場合もあります。
個々のフィールドに適用される#[serde(skip_serializing_if)] 属性により、述語関数を指定できます。この関数がフィールドの値に対してtrueを返した場合、そのフィールドはシリアライズされた出力から完全に省略されます。
UserProfileの例を拡張してこれを実証しましょう。
use serde::{Serialize, Deserialize}; #[derive(Serialize, Deserialize, Debug)] #[serde(rename_all = "camelCase")] struct UserProfileV2 { user_id: String, first_name: String, last_name: String, #[serde(skip_serializing_if = "Option::is_none")] email_address: Option<String>, #[serde(skip_serializing_if = "Vec::is_empty")] favorite_colors: Vec<String>, #[serde(skip_serializing_if = "is_default_age")] age: u8, } fn is_default_age(age: &u8) -> bool { *age == 0 // 0が私たちの「デフォルト」または未設定の年齢だと仮定します } fn main() { let user1 = UserProfileV2 { user_id: "u123".to_string(), first_name: "Alice".to_string(), last_name: "Smith".to_string(), email_address: Some("alice.smith@example.com".to_string()), favorite_colors: vec!["blue".to_string(), "green".to_string()], age: 30, }; let user2 = UserProfileV2 { user_id: "u456".to_string(), first_name: "Bob".to_string(), last_name: "Johnson".to_string(), email_address: None, // これはスキップされます favorite_colors: vec![], // これはスキップされます age: 0, // is_default_ageによりスキップされます }; println!("User 1 JSON:\n{}", serde_json::to_string_pretty(&user1).unwrap()); // 出力: // { // "userId": "u123", // "firstName": "Alice", // "lastName": "Smith", // "emailAddress": "alice.smith@example.com", // "favoriteColors": [ // "blue", // "green" // ], // "age": 30 // } println!("\nUser 2 JSON:\n{}", serde_json::to_string_pretty(&user2).unwrap()); // 出力: // { // "userId": "u456", // "firstName": "Bob", // "lastName": "Johnson" // } }
UserProfileV2では:
#[serde(skip_serializing_if = "Option::is_none")]は、Option型に対して標準ライブラリのis_noneメソッドを活用する非常に一般的なパターンです。email_addressがNoneの場合、JSONには現れません。#[serde(skip_serializing_if = "Vec::is_empty")]は同様にVecのis_emptyメソッドを呼び出して、空のリストを省略します。#[serde(skip_serializing_if = "is_default_age")]は、カスタム関数is_default_ageの使用を示しています。この関数はフィールド値への参照を取り、フィールドをスキップすべき場合にtrueを返します。
この属性は、特に更新されたフィールドのみが送信されるPATCHリクエストや、オプションパラメータの場合に、よりクリーンで軽量なJSONペイロードを生成するために非常に役立ちます。
包括的な制御のための属性の組み合わせ
serde属性の真の力は、それらを組み合わせることで発揮されます。複雑なシリアライゼーション動作を実現するために、同じ構造体またはフィールドに複数の属性を使用できます。
Product構造体があるシナリオを考えてみましょう。すべてのフィールドがJSONでPascalCaseであることを望みます。
2. オプションのdescriptionフィールドをNoneの場合は省略します。
3. tagsリストを空の場合は省略します。
4. stock_countフィールドは、明示的に設定されていない場合は0をデフォルトとし、0の場合は省略されるようにします。これは#[serde(default)]とskip_serializing_ifを組み合わせます。明確にするためにエイリアスも付けます。
use serde::{Serialize, Deserialize}; #[derive(Serialize, Deserialize, Debug)] #[serde(rename_all = "PascalCase")] struct Product { id: String, name: String, #[serde(skip_serializing_if = "Option::is_none")] description: Option<String>, #[serde(skip_serializing_if = "Vec::is_empty")] tags: Vec<String>, #[serde(default)] // JSONからのデシリアライズ時にstock_countのデフォルト値を意味します #[serde(skip_serializing_if = "is_zero")] stock_count: u32, } fn is_zero(num: &u32) -> bool { *num == 0 } impl Default for Product { fn default() -> Self { Product { id: String::new(), name: String::new(), description: None, tags: Vec::new(), stock_count: 0, } } } fn main() { let product1 = Product { id: "prod-101".to_string(), name: "Super Widget".to_string(), description: Some("A high-quality widget for all your needs.".to_string()), tags: vec!["gadget".to_string(), "electronics".to_string], stock_count: 50, }; let product2 = Product { id: "prod-202".to_string(), name: "Basic Gadget".to_string(), description: None, tags: Vec::new(), stock_count: 0, // これはインスタンス化時に提供されない場合に暗黙的に生成され、その後スキップされます }; println!("Product 1 JSON:\n{}", serde_json::to_string_pretty(&product1).unwrap()); // 出力: // { // "Id": "prod-101", // "Name": "Super Widget", // "Description": "A high-quality widget for all your needs.", // "Tags": [ // "gadget", // "electronics" // ], // "StockCount": 50 // } println!("\nProduct 2 JSON:\n{}", serde_json::to_string_pretty(&product2).unwrap()); // 出力: // { // "Id": "prod-202", // "Name": "Basic Gadget" // } }
Productでは:
#[serde(rename_all = "PascalCase")]は、すべてのフィールドがPascalCaseであることを保証します。descriptionはNoneの場合は省略されます。tagsは空の場合は省略されます。stock_countは#[serde(default)]とカスタムis_zero関数を#[serde(skip_serializing_if)]で使用しています。これは、JSONにStockCountが含まれていない場合、デフォルトで0になり、シリアライズ時にstock_countが0の場合はスキップされることを意味します。これにより、デフォルト以外の在庫数のみが存在するクリーンなJSONが可能になります。
これらの属性は、柔軟で堅牢、そして慣用的なRustアプリケーションを作成するために不可欠であり、多様なJSON APIとシームレスにやり取りできます。手動のデータ変換ロジックの必要性を最小限に抑え、シリアライゼーションの懸念をデータ構造の近くに宣言的に保ちます。
結論
#[serde(rename_all)] や #[serde(skip_serializing_if)] のような属性を使用したserdeによるRustでのJSONシリアライゼーションのカスタマイズは、高い相互運用性を実現し、クリーンで効率的なペイロードを生成するための強力な手法です。Rust構造体上で直接、命名規則や条件付きフィールドの省略を宣言的に定義することにより、ボイラープレートコードを大幅に削減し、アプリケーションのデータ表現が外部要件と完全に一致することを保証できます。これらの属性を習得することは、開発者がJSON中心の世界でパフォーマンスが高く、非常に適応性の高いRustアプリケーションを作成することを可能にします。