LoginSignup
4
2

More than 1 year has passed since last update.

【Rust】serdeでPhantomDataを扱う

Posted at

serdeにおいてPhantomData型はデフォルトでは空の値として扱われ,例えば以下のVideo型を

use serde::{Deserialize, Serialize};
use std::marker::PhantomData;

#[derive(Debug, Serialize, Deserialize, PartialEq)]
struct Video<T> {
    file_type: PhantomData<T>,
    file_name: String,
}

impl<T> Video<T> {
    fn new(file_name: String) -> Video<T> {
        Video {
            file_name,
            file_type: PhantomData,
        }
    }
}

#[derive(Debug, PartialEq)]
struct Mp4;

#[derive(Debug, PartialEq)]
struct Avi;

jsonにシリアライズした場合,file_typenullになります.デシリアライズの際はnullが任意のPhantomData型になります.

let video = Video::<Mp4>::new("my video 1".to_string());
assert_eq!(
	serde_json::to_string(&video).unwrap(),
	r#"{"file_type":null,"file_name":"my video 1"}"#
);

let video_json = r#"{"file_type":null,"file_name":"my video 2"}"#;
assert_eq!(
	serde_json::from_str::<Video<Avi>>(video_json).unwrap(),
	Video::<Avi>::new("my video 2".to_string())
)

多くの場合はデフォルトの挙動が望ましいと思いますが,他の言語でも扱う場合などPhantomDataを文字列等の他の型へ変換したいこともあると思います.今回はPhantomDataを文字列としてシリアライズ・デシリアライズする方法を二つ紹介します.コード全体はこちら.

まずMp4AviなどのUnitタイプにTryFrom<String>Into<String>を実装します.Cloneトレイトは後者の方法のみで必要です.DefaultトレイトはPhantomDataから文字列に変換するときに利用します.

#[derive(Clone, Debug, Default, PartialEq)]
struct Mp4;

impl TryFrom<String> for Mp4 {
    type Error = String;
    fn try_from(value: String) -> Result<Self, Self::Error> {
        if value.as_str() == stringify!(Mp4) {
            Ok(Mp4)
        } else {
            Err("Parse Error".to_string())
        }
    }
}

impl From<Mp4> for String {
    fn from(_: Mp4) -> Self {
        stringify!(Mp4).to_string()
    }
}

#[derive(Clone, Debug, Default, PartialEq)]
struct Avi;

impl TryFrom<String> for Avi {
    type Error = String;
    fn try_from(value: String) -> Result<Self, Self::Error> {
        if value.as_str() == stringify!(Avi) {
            Ok(Avi)
        } else {
            Err("Parse Error".to_string())
        }
    }
}

impl From<Avi> for String {
    fn from(_: Avi) -> Self {
        stringify!(Avi).to_string()
    }
}

serde_withを利用する

serde(serialize_with = "path")serde(deserialize_with = "path")フィールドアトリビュートを指定することでシリアライズとデシリアライズを特定の関数で行うことができます.
以下の関数ではPhantomData<T>をシリアライズ・デシリアライズしています.serialize_phantomではDefaultトレイトを使って与えた型パラメーターから値を作成,文字列に変換しています.deserialize_phantomでは与えた型パラメーターに文字列が変換できるかチェックしています.serde::de::Error::customではDisplayトレイトを実装した任意の値を渡してエラーを作成できます.

use serde::{Deserializer, Serializer};

fn serialize_phantom<S, T>(_: &PhantomData<T>, s: S) -> Result<S::Ok, S::Error>
where
    S: Serializer,
    T: Into<String> + Default,
{
    let type_str: String = T::default().into();
    s.serialize_str(&type_str)
}

fn deserialize_phantom<'de, D, T>(d: D) -> Result<PhantomData<T>, D::Error>
where
    D: Deserializer<'de>,
    T: TryFrom<String>,
{
    let unit_type_str: String = Deserialize::deserialize(d)?;
    let _unit_type: T = unit_type_str
        .try_into()
        .map_err(|_| serde::de::Error::custom("Parse Error".to_string()))?;

    Ok(PhantomData)
}

serdeのアトリビュートは以下のようにします.関数でシリアライズ・デシリアライズを行うため,型パラメーターのトレイト境界ももとのSerialize, Deserializeからその関数に合うように変更します.

#[derive(Debug, Serialize, Deserialize, PartialEq)]
struct Video<T> {
    #[serde(
        serialize_with = "serialize_phantom",
        deserialize_with = "deserialize_phantom"
    )]
    #[serde(bound(serialize = "T: Into<String> + Default"))]
    #[serde(bound(deserialize = "T: TryFrom<String>"))]
    file_type: PhantomData<T>,
    file_name: String,
}

これによって,以下のように文字列としてシリアライズ・デシリアライズされます.

let video = Video::<Mp4>::new("my video 1".to_string());
assert_eq!(
	serde_json::to_string(&video).unwrap(),
	r#"{"file_type":"Mp4","file_name":"my video 1"}"#
);

let video_json = r#"{"file_type":"Avi","file_name":"my video 2"}"#;
assert_eq!(
	serde_json::from_str::<Video<Avi>>(video_json).unwrap(),
	Video::<Avi>::new("my video 2".to_string())
)

NewTypeパターンを使う

PhantomData型をラップした新しい型SerdePhantomを定義し,SerdePhantomに文字列との変換用のトレイトを実装します.serde(try_from = "FromType")serde(into = "IntoType")コンテナアトリビュートを指定することでシリアライズの前とデシリアライズの後に別の型を経由するようになります.serde(into = "IntoType")を利用するにはCloneトレイトが必要となります.

#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
#[serde(try_from = "String", into = "String")]
#[serde(bound(serialize = "T: Default + Into<String> + Clone"))]
#[serde(bound(deserialize = "T: TryFrom<String>"))]
struct SerdePhantomData<T>(PhantomData<T>);

impl<T> TryFrom<String> for SerdePhantomData<T>
where
    T: TryFrom<String>,
{
    type Error = String;
    fn try_from(value: String) -> Result<Self, Self::Error> {
        let _unit_type: T = value.try_into().map_err(|_| "Parse Error".to_string())?;
        Ok(SerdePhantomData(PhantomData))
    }
}

impl<T> From<SerdePhantomData<T>> for String
where
    T: Default + Into<String>,
{
    fn from(_value: SerdePhantomData<T>) -> Self {
        T::default().into()
    }
}

Video側でも型パラメーターのトレイト境界を変更します.

#[derive(Debug, PartialEq, Serialize, Deserialize)]
struct Video<T> {
    #[serde(bound(serialize = "T: Default + Into<String> + Clone"))]
    #[serde(bound(deserialize = "T: TryFrom<String>"))]
    file_type: SerdePhantomData<T>,
    file_name: String,
}

impl<T> Video<T> {
    fn new(file_name: String) -> Video<T> {
        Video {
            file_name,
            file_type: SerdePhantomData(PhantomData),
        }
    }
}

jsonとの変換は前者と全く同じように利用できます.

let video = Video::<Mp4>::new("my video 1".to_string());
assert_eq!(
	serde_json::to_string(&video).unwrap(),
	r#"{"file_type":"Mp4","file_name":"my video 1"}"#
);

let video_json = r#"{"file_type":"Avi","file_name":"my video 2"}"#;
assert_eq!(
	serde_json::from_str::<Video<Avi>>(video_json).unwrap(),
	Video::<Avi>::new("my video 2".to_string())
)
4
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
4
2