前回はserdeの基本を理解することができましたので、次はserdeに登場するアトリビュートのうち、実務上よく利用するものを紹介したいと思います。具体的にはいくつかのアトリビュートと、実務上はenumのシリアライズ・デシリアライズで毎回迷いごとが発生してドキュメントを確認する傾向にあるので、それを確認していきます。
rename_all
、rename
たとえばよく利用するケースとして、Rustは変数名やフィールド名はすべてsnake_case
ですが、フロントエンド側ではcamelCase
として受け取りたいという場合があります。何も設定せずにそのままRustのシリアライズ結果をJSONで返すとスネークケースで返ってしまいますが、rename_all
アトリビュートを使用するとこれを調整することができます。
また、rename
アトリビュートを用いると、フィールドの名前を別の名前に変換させることができます。この変換はシリアライズ、デシリアライズそれぞれ指定することもできます。何も指定しない場合、両方とも名前を変更します。
下記は、構造体のすべてのフィールドをキャメルケースに変更するケースと(rename_all
を使用)、構造体のフィールドのうち一部を別名に変更する例(rename
を使用)を示したものです。
use serde::{Deserialize, Serialize}; #[derive(Serialize, Deserialize)] #[serde(rename_all = "camelCase")] struct RenameAllFields { rename_this: i32, rename_that: bool, } #[derive(Serialize, Deserialize)] struct RenameOnlySpecificField { #[serde(rename = "wow")] renamed_field: i32, do_not_rename: bool, } fn main() -> Result<(), Box<dyn std::error::Error>> { let all_fields = RenameAllFields { rename_this: 1, rename_that: true, }; println!("{}", serde_json::to_string_pretty(&all_fields)?); let specific_field = RenameOnlySpecificField { renamed_field: 42, do_not_rename: false, }; println!("{}", serde_json::to_string_pretty(&specific_field)?); Ok(()) }
出力結果は次のようになります。
{ "renameThis": 1, "renameThat": true } { "wow": 42, "do_not_rename": false }
skip
skip
は特定のフィールドにつけておくと、そのフィールドのシリアライズやデシリアライズをさせない(スキップさせる)ことができるアトリビュートです。たとえば秘匿情報が含まれるなどして特定のフィールドをJSONとして返させたくない場合に、そのフィールドに#[serde(skip)]
というアトリビュートをセットしておくと、そのフィールドは読み飛ばしてくれます[*1]。
簡単な例として、
- あるyamlファイルに読み込み先のファイルのパス情報が書いてあるが、後で読み込ませるようにしたい&実は別の形式のファイルで一気に読み込ませるのが難しい。
- 読み込み先のファイルの設定は追加情報として同一構造体に保持させたい。
という(半ば無理矢理な)条件があったとします。この例を実現するためには、
- まずそのファイルのパス情報を読み込んでおく。
- しかるべきタイミングで
load
というメソッドを呼び出し、構造体の内部情報を書き換える。
という処理順序を実現する必要があったとしましょう。
まず設定ファイルですが、次のように2つのファイルが用意されているとします。
# config/path.yaml path: config/application_config.yaml
# config/application_config.yaml host: localhost port: 9090 env: dev
次に構造体とそれに関連して必要になる実装を用意します。下記のように実装できるでしょう。
use std::{collections::HashMap, error::Error, fmt::Display, path::PathBuf}; use serde::{Deserialize, Serialize}; use serde_yaml::Value; #[derive(Debug)] pub struct EasyError { details: String, } impl Display for EasyError { fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { write!(f, "{}", self.details) } } impl Error for EasyError { fn description(&self) -> &str { &self.details } } #[derive(Debug, Eq, PartialEq, Hash)] pub struct Attribute(String); #[derive(Debug)] pub enum ConfigValue { String(String), Number(u64), } #[derive(Serialize, Deserialize, Debug)] pub struct Config { path: PathBuf, #[serde(skip)] extra_config: HashMap<Attribute, ConfigValue>, } impl Config { pub fn load(&mut self) -> Result<(), Box<dyn std::error::Error>> { let rdr = std::fs::File::open(&self.path)?; let deserialized: HashMap<String, Value> = serde_yaml::from_reader(&rdr)?; for (k, v) in deserialized { match v { Value::String(s) => { self.extra_config .insert(Attribute(k), ConfigValue::String(s)); } Value::Number(n) => { self.extra_config.insert( Attribute(k), ConfigValue::Number(n.as_u64().ok_or(EasyError { details: "数値ではないものが入っているかもしれません。".into(), })?), ); } _ => {} } } Ok(()) } } fn main() -> Result<(), Box<dyn std::error::Error>> { let rdr = std::fs::File::open("config/path.yaml").unwrap(); let mut config_base: Config = serde_yaml::from_reader(&rdr)?; println!("{:?}", config_base); config_base.load()?; println!("{:?}", config_base); Ok(()) }
重要な点として、extra_config
は最初Config
をyamlからデシリアライズした(つまり、config/path.yaml
を読み込んだ)時点では読み込まれません。フィールドとしても元となる設定ファイルに存在しない関係で、読み飛ばしを起こさせたいでしょう。そこで#[serde(skip)]
の出番、というわけです。
これを実行すると、次のようなログを得られます。config/path.yaml
ファイルにはextra_config
というフィールドは一切ないものの正常に一度path
を読み込めており、その後追加のload
メソッドの呼び出しにより、extra_config
が後から埋められました。
Config { path: "config/application_config.yaml", extra_config: {} } Config { path: "config/application_config.yaml", extra_config: {Attribute("env"): String("dev"), Attribute("port"): Number(9090), Attribute("host"): String("localhost")} }
flatten
serdeはネストさせた構造体であっても簡単に扱うことができますが、場合によっては、構造体の表現上はネストさせたフィールドを、JSON上ではネストさせずに扱いたいケースがあると思います。こういう場合にflatten
が使用できます。ページネーションは実装方法によってはそうなるもののひとつで、たとえば次のように実装できます。
use serde::{Deserialize, Serialize}; #[derive(Serialize, Deserialize, Debug)] struct Item { price: u32, } #[derive(Serialize, Deserialize, Debug)] struct Items { items: Vec<Item>, #[serde(flatten)] pagination: Pagination, } #[derive(Serialize, Deserialize, Debug)] struct Pagination { offset: u64, limit: u64, total: u64, } fn main() -> Result<(), Box<dyn std::error::Error>> { let items = Items { items: vec![Item { price: 100 }, Item { price: 200 }], pagination: Pagination { offset: 0, limit: 10, total: 100, }, }; println!("{}", serde_json::to_string_pretty(&items)?); Ok(()) }
flatten
を指定すると、特定のフィールドの子構造体ないしは値(HashMap
など)を平坦化して、親となる構造体にそのまま展開できます。上述のコードを展開すると次のように、Pagination
構造体のフィールドがItems
の一部として展開されます。
{ "items": [ { "price": 100 }, { "price": 200 } ], "offset": 0, "limit": 10, "total": 100 }
serdeの公式ドキュメントには、型定義をとくにしないJSONを、HashMap
なフィールドに#[serde(flatten)]
を適用することにより実現した例があります。たまに有用なケースなので押さえておくとよいと思います。
serialize_with
/ deserialize_with
_with
を使うと、特定のフィールドのシリアライズ・デシリアライズ時に、特定の関数を呼び出して処理を行わせることができます。今回はdeserialize_with
を使った簡単な例を示しておきます。
この例では、"1000 JPY"
のような値がprice
というフィールドに入ったJSONを渡すものとします。ただRustの構造体上では、価格それ自体と単位は、それぞれvalue
とcurrency
に分けたいという要件があるとしましょう。serdeがデフォルトで提供する機能ではこうしたデシリアライズは不可能ですが、専用の処理をする関数(parse_with_currency
)をひとつ噛ませることで、こうしたケースにも対応することができます。
#![allow(unused)] use serde::Deserialize; use serde_json::json; #[derive(Deserialize, Debug)] struct Price { value: u64, currency: String, } #[derive(Deserialize, Debug)] struct Item { #[serde(deserialize_with = "parse_with_currency")] price: Price, } fn parse_with_currency<'de, D: serde::Deserializer<'de>>(deserializer: D) -> Result<Price, D::Error> { let s = String::deserialize(deserializer)?; let mut split = s.splitn(2, ' '); let value = split.next().unwrap().parse().unwrap(); let unit = split.next().unwrap().to_string(); Ok(Price { value, currency: unit, }) } fn main() -> Result<(), Box<dyn std::error::Error>> { let json = json!({ "price": "1000 JPY" }); let item: Item = serde_json::from_value(json)?; println!("{:?}", item); Ok(()) }
これを実行すると次のようにデシリアライズできます。
Item { price: Price { value: 1000, currency: "JPY" } }
なお、シリアライズ時の特殊ケースへの対応はserialize_with
でできます。
New Typeの扱い
serdeはとくに特別な指定なくNew Typeを綺麗に扱ってくれます。たとえばUserId
というNew Typeを用意したとして、このNew Typeを元のString型としてJSONにシリアライズする際に返したり、あるいはJSONのid
フィールドの文字列はUserId
にデシリアライズして値を格納したりすることができます。
use serde::{Deserialize, Serialize}; use serde_json::json; #[derive(Serialize, Deserialize, Debug)] struct UserId(String); #[derive(Serialize, Deserialize, Debug)] struct User { id: UserId, name: String, } fn main() -> Result<(), Box<dyn std::error::Error>> { let user = User { id: UserId("aaa".into()), name: "paild".into(), }; println!("{}", serde_json::to_string_pretty(&user)?); let json = json!({ "id": "aaa", "name": "paild" }); let user = serde_json::from_value::<User>(json)?; println!("{:?}", user); Ok(()) }
ちなみに、New Type Patternというわけではありませんが、下記のように構造体に名前付きでひとつだけフィールドを持つものを綺麗に扱うには#[serde(transparent)]
を使用します。
use serde::{Deserialize, Serialize}; use serde_json::json; #[derive(Serialize, Deserialize, Debug)] #[serde(transparent)] struct UserId { value: String, } #[derive(Serialize, Deserialize, Debug)] struct User { id: UserId, name: String, } fn main() -> Result<(), Box<dyn std::error::Error>> { let user = User { id: UserId { value: "aaa".into(), }, name: "paild".into(), }; println!("{}", serde_json::to_string_pretty(&user)?); let json = json!({ "id": "aaa", "name": "paild" }); let user = serde_json::from_value::<User>(json)?; println!("{:?}", user); Ok(()) }
JSONへのシリアライズ時にはvalue
というフィールド名はなくなり文字列が直に登録され、構造体へのデシリアライズ時にはvalue
フィールドに元のJSONの文字列が入っていることが確かめられます。
{ "id": "aaa", "name": "paild" } User { id: UserId { value: "aaa" }, name: "paild" }
仮に#[serde(transparent)]
を指定しなかった場合、そもそも期待されるJSONが下記のようになります。こうならないようにするのが#[serde(transparent)]
です。
{ "id": { "value": "aaa" }, "name": "paild" }
enumの表現
Rustで何かのデータを表現する際、enumは外せません。enumはひとつの型に対して複数のヴァリアント(variant)を持ち得るわけですが、ヴァリアントひとつひとつは情報を特別に持つことになります。
たとえば、タスクの終了時のステータスを表現するTaskStatus
というenumを定義したとしましょう。このenumは成功(TaskStatus::Succeeded
)と失敗(TaskStatus::Failed
)のヴァリアントを持つとします。各々のヴァリアントはそれぞれ、「成功」と「失敗」という別々の情報を持っているといえます。
一方で、Rustの型表現の上ではヴァリアントは型にはならないため、TaskStatus
という単一の型で表現されるに過ぎません。型情報からはヴァリアントは落とされてしまうのです。他方、JSON上はヴァリアントの情報を特別視して扱いたいことが多いでしょう。serdeはシリアライズする際にこのギャップを上手に埋め合わせる必要がありますが、このギャップの埋め方にいくつか方法が用意されています。それを今回は紹介します。
enumの表現は一通り網羅的にこの記事で説明しますが、改めて公式ドキュメントも確認されるとよいでしょう。
Externally Tagged
ヴァリアント名をそのままフィールド名として使用させてしまう方法です。オブジェクトの「外に」ヴァリアントの名前がタグ付けされます。serdeはデフォルトでこの方式を採ります。
[ { "executed_by": "admin", "status": { "Succeeded": { "finished_at": "2020-01-01 12:00:00" } } }, { "executed_by": "admin", "status": { "Failed": { "failed_at": "2020-01-01 12:00:00", "reason": "Task failed because of timeout" } } } ]
これを実現するために、次のようなコードを書きました。
use serde::{Deserialize, Serialize}; #[derive(Serialize, Deserialize)] struct Task { executed_by: String, status: TaskStatus, } #[derive(Serialize, Deserialize)] enum TaskStatus { Succeeded { finished_at: String }, Failed { failed_at: String, reason: String }, } fn main() -> Result<(), Box<dyn std::error::Error>> { let tasks = vec![ Task { executed_by: "admin".into(), status: TaskStatus::Succeeded { finished_at: "2020-01-01 12:00:00".into(), }, }, Task { executed_by: "admin".into(), status: TaskStatus::Failed { failed_at: "2020-01-01 12:00:00".into(), reason: "Task failed because of timeout".into(), }, }, ]; println!("{}", serde_json::to_string_pretty(&tasks)?); Ok(()) }
たとえばrename_all
と組み合わせてタグ名称をスネークケースにしたり、Task
のstatus
フィールドにflatten
を適用して、status
というフィールド名を使用しないようにすることも応用として可能です。こうした場合には、多少このExternally Taggedを利用しやすくなるケースが出てくるかもしれません。
また、serdeはあらゆるenumのヴァリアントの表現方法に対応しています。今回は構造体タイプのヴァリアントを例示しましたが、他にもタプル、New Type、Unit(中に何もデータを持たないもの)にも対応しています[*2]。
ただJSONとしてはどこか不恰好というか、"Succeeded"
や"Failed"
といったタグはできればオブジェクトの中に特定のフィールドとともにしまわれていて欲しい感じがするかもしれません。それを実現する手段として、次の「Internally Tagged」が利用できます。
Internally Tagged
先ほどのヴァリアント名によるタグ付け情報を、オブジェクトの「内部に」特定のフィールド名を紐付けて移動してしまう方法です。やはり同様に、先にJSONの結果を示します。kind
という新しいフィールドがオブジェクトの中に追加され、そこにヴァリアント名が入るように変化しました。
[ { "executed_by": "admin", "status": { "kind": "Succeeded", "finished_at": "2020-01-01 12:00:00" } }, { "executed_by": "admin", "status": { "kind": "Failed", "failed_at": "2020-01-01 12:00:00", "reason": "Task failed because of timeout" } } ]
これを実現するために、次のように実装を修正しました。具体的には、#[serde(tag = "kind")]
というアトリビュートをTaskStatus
に付与しました。tag = "<name>"
という指定により、<name>
のタグ付けをすることができるようになります。
use serde::{Deserialize, Serialize}; #[derive(Serialize, Deserialize)] struct Task { executed_by: String, status: TaskStatus, } #[derive(Serialize, Deserialize)] #[serde(tag = "kind")] enum TaskStatus { Succeeded { finished_at: String }, Failed { failed_at: String, reason: String }, } fn main() -> Result<(), Box<dyn std::error::Error>> { let tasks = vec![ Task { executed_by: "admin".into(), status: TaskStatus::Succeeded { finished_at: "2020-01-01 12:00:00".into(), }, }, Task { executed_by: "admin".into(), status: TaskStatus::Failed { failed_at: "2020-01-01 12:00:00".into(), reason: "Task failed because of timeout".into(), }, }, ]; println!("{}", serde_json::to_string_pretty(&tasks)?); Ok(()) }
Adjacently Tagged
公式ドキュメントによればHaskellでよく使われる方法らしいです。
まずはJSONの出力結果を見てもらうのが早いと思うので、先に結果を示しておきます。
[ { "executed_by": "admin", "status": { "kind": "Succeeded", "data": { "finished_at": "2020-01-01 12:00:00" } } }, { "executed_by": "admin", "status": { "kind": "Failed", "data": { "failed_at": "2020-01-01 12:00:00", "reason": "Task failed because of timeout" } } } ]
kind
はInternally Taggedと同様でヴァリアント名を持っています。これに加えて、先ほどはオブジェクトに直に記載されていたTaskStatus::Succeeded
のfinished_at
やTaskStatus::Failed
のfailed_at
、reason
がdata
というフィールドによって包まれています。kind
、data
というタグ名、コンテンツの2つの組み合わせがJSONデータ上で隣り合わせに(adjacently)置かれるのがこのパターンの特徴です。
これを実現するには、Internally Taggedと同様にまずはtag
アトリビュートを定義するのと、加えてcontent
を追加で定義するとこの方式を利用できます。
use serde::{Deserialize, Serialize}; #[derive(Serialize, Deserialize)] struct Task { executed_by: String, status: TaskStatus, } #[derive(Serialize, Deserialize)] #[serde(tag = "kind", content = "data")] enum TaskStatus { Succeeded { finished_at: String }, Failed { failed_at: String, reason: String }, } fn main() -> Result<(), Box<dyn std::error::Error>> { let tasks = vec![ Task { executed_by: "admin".into(), status: TaskStatus::Succeeded { finished_at: "2020-01-01 12:00:00".into(), }, }, Task { executed_by: "admin".into(), status: TaskStatus::Failed { failed_at: "2020-01-01 12:00:00".into(), reason: "Task failed because of timeout".into(), }, }, ]; println!("{}", serde_json::to_string_pretty(&tasks)?); Ok(()) }
Untagged
最後にタグをまったくつけない方法としてuntaggedというものがあります。これを使うとenumのヴァリアント名の情報は一切破棄され、中に持つデータのフィールドのみが使用されます。これもやはり、結果のJSONを見た方が早いので出力結果を載せておきます。
[ { "executed_by": "admin", "status": { "finished_at": "2020-01-01 12:00:00" } }, { "executed_by": "admin", "status": { "failed_at": "2020-01-01 12:00:00", "reason": "Task failed because of timeout" } } ]
これを実現するには次のようなコードを書きます。#[serde(untagged)]
とするだけです。
use serde::{Deserialize, Serialize}; #[derive(Serialize, Deserialize)] struct Task { executed_by: String, status: TaskStatus, } #[derive(Serialize, Deserialize)] #[serde(untagged)] enum TaskStatus { Succeeded { finished_at: String }, Failed { failed_at: String, reason: String }, } fn main() -> Result<(), Box<dyn std::error::Error>> { let tasks = vec![ Task { executed_by: "admin".into(), status: TaskStatus::Succeeded { finished_at: "2020-01-01 12:00:00".into(), }, }, Task { executed_by: "admin".into(), status: TaskStatus::Failed { failed_at: "2020-01-01 12:00:00".into(), reason: "Task failed because of timeout".into(), }, }, ]; println!("{}", serde_json::to_string_pretty(&tasks)?); Ok(()) }
罠:Untagged特有の挙動
Untaggedさせたいケースはさほど多くはないと思うのですが、デシリアライズさせたいJSONと照らし合わせた時に、Rustの構造体側でenumを使用するときれいに行くために利用したくなるケースがあると思います。一方でUntaggedにはハマりどころがあり、これで時間を溶かすケースがままあるため、最後に紹介しておきます。
これはデシリアライズ限定での意外なハマり方なのですが、ヴァリアントの中の構造がまったく同じ場合はまずヴァリアントを区別できず、上手にデシリアライズできません。まったく同じ構造をもつヴァリアント同士の場合、enum上の定義順で先に来るものが優先して適用されるようです。
下記はTaskStatus::Failed
のフィールドを、タスクが終了した時刻のみ残し、かつフィールド名をfinished_at
に変更したコードです。
use serde::{Deserialize, Serialize}; use serde_json::json; #[derive(Serialize, Deserialize, Debug)] struct Task { executed_by: String, status: TaskStatus, } #[derive(Serialize, Deserialize, Debug)] #[serde(untagged)] enum TaskStatus { Succeeded { finished_at: String }, Failed { finished_at: String }, } fn main() -> Result<(), Box<dyn std::error::Error>> { let source = json!([ { "executed_by": "admin", "status": { "finished_at": "2020-01-01 12:00:00" } }, { "executed_by": "admin", "status": { "finished_at": "2020-01-02 12:00:00", } } ]); let tasks: Vec<Task> = serde_json::from_value(source)?; println!("{:#?}", tasks); Ok(()) }
このコードを実行すると、すべてTaskStatus::Succeeded
として認識されます。
[ Task { executed_by: "admin", status: Succeeded { finished_at: "2020-01-01 12:00:00", }, }, Task { executed_by: "admin", status: Succeeded { finished_at: "2020-01-02 12:00:00", }, }, ]
まったく同じ構造を持つヴァリアントを取り違えるだけではありません。先ほどのコードに、もともとTaskStatus::Failed
にいたreason
というフィールドを戻したとしても、やはり同様のことが起きます。
use serde::{Deserialize, Serialize}; use serde_json::json; #[derive(Serialize, Deserialize, Debug)] struct Task { executed_by: String, status: TaskStatus, } #[derive(Serialize, Deserialize, Debug)] #[serde(untagged)] enum TaskStatus { Succeeded { finished_at: String }, Failed { finished_at: String, reason: String }, } fn main() -> Result<(), Box<dyn std::error::Error>> { let source = json!([ { "executed_by": "admin", "status": { "finished_at": "2020-01-01 12:00:00" } }, { "executed_by": "admin", "status": { "finished_at": "2020-01-01 12:00:00", "reason": "Task failed because of timeout" } } ]); let tasks: Vec<Task> = serde_json::from_value(source)?; println!("{:?}", tasks); Ok(()) }
期待値としては、そもそもfinished_at
とreason
がいるのだから、Task::Succeeded
とTask::Failed
の2つが出力されるはずだろう、と思われるはずです。しかし現実には、
[ Task { executed_by: "admin", status: Succeeded { finished_at: "2020-01-01 12:00:00", }, }, Task { executed_by: "admin", status: Succeeded { finished_at: "2020-01-01 12:00:00", }, }, ]
なんと、両者共にTaskStatus::Succeeded
としてデシリアライズされてしまっています。serdeはフィールドとenumのヴァリアントを最初から順番に辿ってデシリアライズをかけ、一番最初にヒットしたものを使用します。タグがない以上、どうしてもこのような挙動になってしまうことは理解できます。ドキュメントには一応それらしい文章は書いてあるため仕様といえば仕様なのでしょうが、知らないと数時間溶かします(溶かしたことがあります)。
これを回避するには、残念ながらヴァリアントの定義順序に気をつける以外に方法がないようです。今回の場合は、TaskStatus::Failed
がTaskStatus::Succeeded
の後ろにあることが問題であったため、TaskStatus::Failed
を前に定義してやれば解決できます。つまり、下記のコードが解決します。
use serde::{Deserialize, Serialize}; use serde_json::json; #[derive(Serialize, Deserialize, Debug)] struct Task { executed_by: String, status: TaskStatus, } #[derive(Serialize, Deserialize, Debug)] #[serde(untagged)] enum TaskStatus { Failed { finished_at: String, reason: String }, Succeeded { finished_at: String }, } fn main() -> Result<(), Box<dyn std::error::Error>> { let source = json!([ { "executed_by": "admin", "status": { "finished_at": "2020-01-01 12:00:00" } }, { "executed_by": "admin", "status": { "finished_at": "2020-01-01 12:00:00", "reason": "Task failed because of timeout" } } ]); let tasks: Vec<Task> = serde_json::from_value(source)?; println!("{:#?}", tasks); Ok(()) }
これにより無事にデシリアライズできました。
[ Task { executed_by: "admin", status: Succeeded { finished_at: "2020-01-01 12:00:00", }, }, Task { executed_by: "admin", status: Failed { finished_at: "2020-01-01 12:00:00", reason: "Task failed because of timeout", }, }, ]
untaggedを用いる場合、ユニットテストをきちんと書くことや、insta
をはじめとしたスナップショットテストで実行結果を追跡し、おかしなものが紛れ込まないかを確かめるのが大事そうです。
ソースコード
ソースコードは下記のGitHubリポジトリにすべて置いてあります。
*1:もっとも、そうした秘匿情報をクライアントに返す可能性がある構造体にそもそも含んでいるのは事故の元です。DTOを利用して詰め替えるなどした方が良いでしょう
*2:Rustのenumのヴァリアントの形式についてはこのドキュメントを参照: https://doc.rust-lang.org/beta/reference/items/enumerations.html