↔️

[serde] flatten + with&remote + rename

2023/07/24に公開

問題

  • 既存のJSONに xx_startxx_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にSerializeDeserializeを実装することで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で構造体をRangeRangeInclusiveに変換し、
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)

main.rs
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::RangeRangeInclusiveを例に無理やりマクロ化してみました。
Range<$value_type>を文字列リテラルとしてgetterに渡さなければならないところは、
内部でモジュールを作成してtype aliasを作成することで解決しました。

serde_range_rename_macro.rs
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
            }
        )
    }
}

参考

おわりに

株式会社クロスビットでは、デスクレスワーカーのためのHR管理プラットフォームを開発しています。
一緒に開発を行ってくれる各ポジションのエンジニアを募集中です。
https://x-bit.co.jp/recruit/
https://herp.careers/v1/xbit
https://note.com/xbit_recruit

脚注
  1. なお、このときRemoteRangeそのものを(De)Serizalizeすることはできません。 ↩︎

クロスビットテックブログ

Discussion