[serde] flatten + with&remote + rename
問題
- 既存のJSONに
xx_start
やxx_end
といった名前のフィールドが含まれている - Rustの(デ)シリアライズフレームワークであるserdeを用いて、これまでは単純にデシリアライズしていた
- 可能であれば
std::ops::Range
などに直接変換したい
結論
- withとremote
- rename (対象の構造体のメンバ変数が可視な場合)
もしくは getterとstd::ops::From (対象の構造体のメンバ変数が不可視な場合) - flatten または Newtypeパターンの構造体
といったserdeの属性の濫用組み合わせで任意のデータ群と任意の構造体を直接(de)serializeできます。
例)
json{"xx_start":3,"xx_end":5}
↕️
構造体 SampleFlattenRemote { range_inclusive: 3..=5 }
今回用いるserdeの機能の紹介
シンプルな(de)serialize
#[derive(Serialize, Deserialize)]
struct SampleSimple {
xx_start: i32,
xx_end: i32,
}
対象のstruct,enumにSerialize
やDeserialize
を実装することでSerializeやDeserializeが可能になります。deriveマクロも利用でき、その場合は全てのメンバがSerialize
,Deserialize
を実装している必要があります。
rename
#[derive(Serialize, Deserialize, Debug)]
struct SampleRename {
#[serde(rename = "xx_start")]
start: i32,
#[serde(rename = "xx_end")]
end: i32,
}
renameは、メンバ名の代わりに指定した名前に基づいて(de)serializeを行います。
flatten
#[derive(Serialize, Deserialize, Debug)]
struct SampleFlatten<T> {
#[serde(flatten)]
value_flatten: T,
}
#[derive(Serialize, Deserialize, Debug)]
struct SampleNewtype<T>(T);
flattenは、対象のシリアライズキーを外側の構造体に展開します。
またserdeの仕様として、Newtype構造体(1要素tupleの構造体)は自動でflattenされます。
remote (& with)
#[derive(Serialize, Deserialize, Debug)]
#[serde(remote = "Range<i32>")]
struct RemoteRange {
start: i32,
end: i32,
}
////////
#[derive(Serialize, Deserialize, Debug)]
#[serde(remote = "RangeInclusive<i32>")]
struct RemoteRangeInclusive {
#[serde(getter = "RangeInclusive::start")]
start: i32,
#[serde(getter = "RangeInclusive::end")]
end: i32,
}
impl From<RemoteRangeInclusive> for RangeInclusive<i32> {
fn from(value: RemoteRangeInclusive) -> Self {
value.begin..=value.end
}
}
////////
#[derive(Serialize, Deserialize, Debug)]
struct SampleNewTypeRemote(#[serde(with = "RemoteRange")] Range<i32>);
#[derive(Serialize, Deserialize, Debug)]
struct SampleFlattenRemote {
#[serde(with = "RemoteRangeInclusive")]
range_inclusive: RangeInclusive<i32>,
}
remoteは、外部の構造体についてシリアライズやデシリアライズを行うための機能です。
SerizalizeやDeserializeを実装することができない外部のcrateの構造体などに対して利用するのが基本的な使い方です。
#[serde(with = "RemoteRange")] range: Range<i32>
の形で参照することで、rangeの(デ)シリアライズがRemoteRangeの定義に基づいて処理されます。[1]
(デ)シリアライズ時のremote元とremote先の相互変換は、
それぞれのメンバ変数が同名であることや、(remote先の構造体のメンバが可視の場合限定)
getter
とFromトレイト(remote先の構造体のメンバが不可視の場合はこちら)
によって行われます。
(なおserdeでは元々std::ops::Range類にSerialize,Deserializeが実装されています)
本題の解決
rename, remote,そしてflattenは互いに併用することが可能です。
renameでフィールド名を"xx_start"や"xx_end"に置き換え、
remoteで構造体をRange
やRangeInclusive
に変換し、
flatten(もしくはNewType)とwithでremoteを指定し展開します。
#[derive(Serialize, Deserialize, Debug)]
#[serde(remote = "Range<i32>")]
struct RemoteRange {
+ #[serde(rename = "xx_start")]
start: i32,
+ #[serde(rename = "xx_end")]
end: i32,
}
////////
#[derive(Serialize, Deserialize, Debug)]
#[serde(remote = "RangeInclusive<i32>")]
struct RemoteRangeInclusive {
+ #[serde(rename = "xx_start", getter = "RangeInclusive::start")]
- #[serde(getter = "RangeInclusive::start")]
start: i32,
+ #[serde(rename = "xx_end", getter = "RangeInclusive::end")]
- #[serde(getter = "RangeInclusive::end")]
end: i32,
}
impl From<RemoteRangeInclusive> for RangeInclusive<i32> {
fn from(value: RemoteRangeInclusive) -> Self {
value.begin..=value.end
}
}
////////
#[derive(Serialize, Deserialize, Debug)]
struct SampleNewTypeRemote(#[serde(with = "RemoteRange")] Range<i32>);
+// ↑勝手にflattenされる
#[derive(Serialize, Deserialize, Debug)]
struct SampleFlattenRemote {
+ #[serde(flatten, with = "RemoteRangeInclusive")]
- #[serde(with = "RemoteRangeInclusive")]
range_inclusive: RangeInclusive<i32>,
}
成果物(サンプル)
sample_check(..)
で実際にシリアライズとデシリアライズを行い、構造体のdebug出力(とおまけでシリアライズで元と同じjsonが得られること)を確認しています。
フルバージョンはこちら(Rust Playground)
use std::ops::{Range, RangeInclusive};
use serde::{Deserialize, Serialize};
#[derive(Serialize, Deserialize, Debug)]
#[serde(remote = "Range<i32>")]
struct RemoteRange {
#[serde(rename = "value_begin")]
start: i32,
#[serde(rename = "value_end")]
end: i32,
}
#[derive(Serialize, Deserialize, Debug)]
#[serde(remote = "RangeInclusive<i32>")]
struct RemoteRangeInclusive {
#[serde(getter = "RangeInclusive::start")]
value_begin: i32,
#[serde(getter = "RangeInclusive::end")]
value_end: i32,
}
impl From<RemoteRangeInclusive> for RangeInclusive<i32> {
fn from(value: RemoteRangeInclusive) -> Self {
value.value_begin..=value.value_end
}
}
#[derive(Serialize, Deserialize, Debug)]
struct SampleNewTypeRemote(#[serde(with = "RemoteRange")] Range<i32>);
#[derive(Serialize, Deserialize, Debug)]
struct SampleFlattenRemote {
#[serde(flatten, with = "RemoteRangeInclusive")]
range_inclusive: RangeInclusive<i32>,
}
fn main() {
let sample_json = r#"{"value_begin":3,"value_end":5}"#;
sample_check::<SampleNewTypeRemote>(sample_json);
sample_check::<SampleFlattenRemote>(sample_json);
}
fn sample_check<'de, T: Serialize + Deserialize<'de> + std::fmt::Debug>(sample_json: &'de str) {
//deserialize
let de: T = serde_json::from_str(sample_json).unwrap();
println!("{de:?}");
//serialize
let ser = serde_json::to_string(&de).unwrap();
assert_eq!(sample_json, ser)
}
SampleNewTypeRemote(3..5)
SampleFlattenRemote { range_inclusive: 3..=5 }
環境
- cargo 1.70.0
edition = "2021"
[dependencies]
serde = { version = "1.0.164", features = ["derive"] }
serde_json = "1.0.99" # サンプル用
余談: renameが何パターンか必要な場合のmacro化
ある構造体に対してrenameがいくつか必要な場合のために、ここではstd::ops::Range
とRangeInclusive
を例に無理やりマクロ化してみました。
Range<$value_type>
を文字列リテラルとしてgetterに渡さなければならないところは、
内部でモジュールを作成してtype aliasを作成することで解決しました。
serde_range_rename_macro.rs
use std::ops::Range;
/// serde(getter)用
pub fn range_start<T>(range: &Range<T>) -> &T {
&range.start
}
/// serde(getter)用
pub fn range_end<T>(range: &Range<T>) -> &T {
&range.end
}
#[macro_export]
macro_rules! serde_range_rename {
( $helper_name:ident { $start:ident .. $end:ident : $value_type:ty } ) => {
#[allow(unused)]
mod $helper_name {
use super::*;
use ::serde::{Deserialize, Serialize};
use $crate::serde_range_rename_macro as helper;
// $value_type をremote,getterに直接渡せないためaliasに
type R = ::std::ops::Range<$value_type>;
type RI = ::std::ops::RangeInclusive<$value_type>;
// $start, $end をrenameに直接渡せないため名前を直接変えgetterで紐付け
#[derive(Serialize, Deserialize)]
#[serde(remote = "self::R")]
pub(super) struct Range {
#[serde(getter = "self::helper::range_start")]
$start: $value_type,
#[serde(getter = "self::helper::range_end")]
$end: $value_type,
}
// $start, $end をrenameに直接渡せないため名前を直接変えgetterで紐付け
#[derive(Serialize, Deserialize)]
#[serde(remote = "self::RI")]
pub(super) struct RangeInclusive {
#[serde(getter = "self::RI::start")]
$start: $value_type,
#[serde(getter = "self::RI::end")]
$end: $value_type,
}
impl From<self::Range> for self::R {
fn from(v: self::Range) -> Self {
v.$start..v.$end
}
}
impl From<self::RangeInclusive> for self::RI {
fn from(v: self::RangeInclusive) -> Self {
v.$start..=v.$end
}
}
}
};
//複数種類を同時に宣言する用
( $( $helper_name:ident { $start:ident .. $end:ident : $value_type:ty} ),+ $(,)*) => {
$( $crate::serde_range_rename!($helper_name{ $start..$end:$value_type }); )+
};
}
#[cfg(test)]
mod test {
use std::ops::RangeInclusive;
use super::*;
use serde::{Deserialize, Serialize};
serde_range_rename! {
begin_end { begin..end : i32 },
first_last { first..last : i32 },
left_right { left..right : u64 },
}
#[derive(Serialize, Deserialize, Debug, PartialEq)]
struct Test {
#[serde(flatten, with = "begin_end::Range")]
be: Range<i32>,
#[serde(flatten, with = "first_last::RangeInclusive")]
fl: RangeInclusive<i32>,
#[serde(flatten, with = "left_right::Range")]
lr: Range<u64>,
}
#[test]
fn test() {
let json = r#"
{
"begin": 2,
"end" : 3,
"first": 5,
"last" : 7,
"left" : 11,
"right": 13
}
"#;
assert_eq!(
serde_json::from_str::<Test>(json).unwrap(),
Test {
be: 2..3,
fl: 5..=7,
lr: 11..13
}
)
}
}
参考
-
Overview · Serde
- Attributes · Serde
- Struct flattening · Serde
- Derive for remote crate · Serde
- Structs and enums in JSON · Serde (Newtype構造体のシリアライズについて)
おわりに
株式会社クロスビットでは、デスクレスワーカーのためのHR管理プラットフォームを開発しています。
一緒に開発を行ってくれる各ポジションのエンジニアを募集中です。
-
なお、このときRemoteRangeそのものを(De)Serizalizeすることはできません。 ↩︎
Discussion