nazo6 noteblog

SerdeのDeserializerを実装する(Part1)

作成:2023/09/10

更新:2023/12/18

この記事は、Zennにも投稿しています。

概要

Serdeで任意の形式のファイルなどをデシリアライズする際にはDeserializerを書く必要があります。この記事では基本的なDeserializerの書き方を解説します。
正直自分もあまり理解していない部分が多々あるのですが世に出ている情報が少ないので書くことにしました。

コードの概観

serdeのDeserializerを実装するというのはつまり、「Deserializerトレイトを実装した構造体を用意する」ということです。ある文字列をデシリアライズするための関数from_strの概略コードは以下のようになります(あくまでイメージです)。
use serde::de;

struct MyDeserializer;

impl<'de, 'a> de::Deserializer<'de> for &'a mut MyDeserializer<'de> {
(ここにDeserializeの実装)
...
}

pub fn from_str<T: Deserialize>(input: &str) -> Result<T, Error> {
let deserializer = MyDeserializer::new();
T::deserialize(deserializer)?
}
ここで、MyDeserializerがデシリアライザ本体、T: Deserialize#[derive(Deserialize)]などによりserde::de::Deserializeが実装された型のことですね。

デシリアライズの基本を理解する

この記事では、超基本的なデシリアライザを実装して流れを理解していくことを目指します。
今回実装するのは、「"true""false"の文字列を受けとり、それをbool型にデシリアライズするだけ」のデシリアライザです。

プロジェクトの作成

次のコマンドで、プロジェクトを作成し必要になるクレートを追加します。
$ cargo new --lib deserializer-example
$ cd deserializer-example
$ cargo add serde thiserror

Errorの作成

まずはDeserializerのエラー型を作ります。これはDeserializerの関連型として必須で、serde::de::Errorを実装している必要があります。
Deserializerを実装するのはライブラリであることが多いと思うので今回はthiserrorを使います。
error.rs
use serde::de;
use std::fmt::Display;
use thiserror::Error;

#[derive(Debug, Error)]
pub enum DeserializeError {
#[error("Failed to parse: {0}")]
Parse(String),
#[error("Unsupported: {0}")]
Unsupported(String),
#[error("Error: {0}")]
Message(String),
}

impl de::Error for DeserializeError {
fn custom<T: Display>(msg: T) -> Self {
DeserializeError::Message(msg.to_string())
}
}

Deserializerの実装

BoolDeserializerという構造体にDeserializerを適切に実装したものが以下になります。
lib.rs
mod error;

use serde::{
de::{self, Visitor},
forward_to_deserialize_any,
};

use error::DeserializeError;

pub struct BoolDeserializer<'de> {
input: &'de str,
}

impl<'de, 'a> de::Deserializer<'de> for &'a mut BoolDeserializer<'de> {
type Error = DeserializeError;

fn deserialize_any<V>(self, visitor: V) -> Result<V::Value, Self::Error>
where
V: Visitor<'de>,
{
Err(DeserializeError::Unsupported(
"Unsupported type".to_string(),
))
}

fn deserialize_bool<V>(self, visitor: V) -> Result<V::Value, Self::Error>
where
V: Visitor<'de>,
{
if self.input == "true" {
visitor.visit_bool(true)
} else if self.input == "false" {
visitor.visit_bool(false)
} else {
Err(DeserializeError::Parse("Invalid boolean value".to_string()))
}
}

forward_to_deserialize_any! {
i8 i16 i32 i64 i128 u8 u16 u32 u64 u128 f32 f64 char str string
bytes byte_buf option unit unit_struct newtype_struct seq tuple
tuple_struct map struct enum identifier ignored_any
}
}

実行してみる

なにはともあれ、実行してみましょう。以下のようなコードとテストを追加します。
lib.rs
fn from_str<'de, T: Deserialize<'de>>(input: &'de str) -> Result<T, DeserializeError> {
let mut deserializer = BoolDeserializer { input };
T::deserialize(&mut deserializer)
}

#[cfg(test)]
mod test {
#[test]
fn deserialize_true() {
let value: bool = super::from_str("true").unwrap();
assert!(value);
}

#[test]
fn deserialize_error() {
let value: String = super::from_str("true").unwrap();
assert_eq!(value, "true");
}
}
cargo testで実行すると以下のような出力が得られるはずです。
running 2 tests
test test::deserialize_error ... FAILED
test test::deserialize_true ... ok

failures:

---- test::deserialize_error stdout ----
thread 'test::deserialize_error' panicked at 'called `Result::unwrap()` on an `Err` value: Unsupported("Unsupported type")', src/lib.rs:61:53
"true"という文字列が正常にtrueにデシリアライズされ、さらに"true"という文字列であっても型がStringであればUnsupportedのエラーが発生していることがわかります。

解説

Deserializeを実装するには上で書いたエラー型、そしてdeserialize_で始まる、一連の型をデシリアライズするためのメソッドが必要です。deserialize_メソッドは対応する型がserdeに判別されて呼ばれます。全てのメソッドは以下のページで確認できます。

Deserializer in serde - Rust

docs.rs

ただし、forward_to_deserialize_anyマクロを使用することで、それらをdeserialize_anyメソッドに飛ばすことが可能です。
今回はboolのみをデシリアイズするDeserializerであるため、bool以外の全ての型をdeserialize_anyに飛ばしています。そして実際にdeserialize_anyで行われる処理はサポートされていないというエラーメッセージを返すことだけです。
そして重要なのがbool型をデシリアライズするdeserialize_boolメソッドです。今回は受け取った文字列が"true"であればtrue"false"であればfalseを返す処理にしたいわけですが、上の例では単に値を返すのではなく、何やら引数として受け取ったvisitor: Visitorvisit_bool関数で処理をしたものを返しています。
これは何かというと、型側(つまりDeserializetrait)での処理の柔軟性を高めるためのserdeの機構だと思います。次にこのVisitorについて解説します。

Visitorについて

Visitorについて説明するために、まずは今回デシリアライズしたbool型へのDeserializeトレイトの実装(github)を見てみましょう。
struct BoolVisitor;

impl<'de> Visitor<'de> for BoolVisitor {
type Value = bool;

fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
formatter.write_str("a boolean")
}

fn visit_bool<E>(self, v: bool) -> Result<Self::Value, E>
where
E: Error,
{
Ok(v)
}
}

impl<'de> Deserialize<'de> for bool {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
deserializer.deserialize_bool(BoolVisitor)
}
}
このコードを見ればDeserialize::deserialize -> Deserializer::deserialize_bool -> Visitor::visit_boolの順で処理されていることがよりわかりやすいと思います。
また、先程のコードで実際にdeserialize_boolに渡されていたVisitorBoolVisitorだったということがわかります。そして、BoolVisitorにはexpectingvisit_boolの二つのメソッドが実装されています。詳しくはdocs.rsに解説がありますが、expectingはエラーメッセージに使われ、visit_boolはDeserializerで処理されたbool値を受けとり、Self::Value型を返しています。その他のメソッドは実装されていませんが、デフォルト実装でエラーが返るようになっています。
bool型の実装を見ても当然bool型をそのまま返しているだけなので「これって何の意味があるんだろう」と感じるかもしれません。しかし、例えばbool型からNewType構造体にデシリアライズされてほしいような型があるときにVisitorが役に立ちそうです。先程のテストコードに以下を追加してみます。
#[test]
fn deserialize_newtype() {
use serde::de::{Error, Visitor};
use std::fmt;

#[derive(Debug, PartialEq)]
struct NewType(bool);
struct NewTypeVisitor;

impl<'de> Visitor<'de> for NewTypeVisitor {
type Value = NewType;

fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
formatter.write_str("a boolean")
}

fn visit_bool<E>(self, v: bool) -> Result<Self::Value, E>
where
E: Error,
{
Ok(NewType(v))
}
}

impl<'de> Deserialize<'de> for NewType {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
deserializer.deserialize_bool(NewTypeVisitor)
}
}

let value: NewType = super::from_str("true").unwrap();
assert_eq!(value, NewType(true));
}
ここで重要なのは、Deserializerには一切手を加えていないということです。つまり、Deserializerはserdeによって決められた基本的なインターフェースさえ実装すればDeserializeを実装するあらゆる型をデシリアライズできるのです。

最後に

この記事では、簡単なDeserializerを実装してserdeでのデシリアライズの流れを追ってみました。参考になれば幸いです。
また、最初にも書きましたが、自分自身も(特にVisitor周りなど)これで理解が正しいのか曖昧な部分があります。間違いなどがありましたらコメントなどで教えていただけると嬉しいです

参考にさせていただいたサイト

Implementing a Deserializer · Serde

serde.rs

Deserializerの実装方法についての公式ドキュメント

serdeマニュアル -Deserializerについて- | 株式会社RICOS

serdeマニュアル -Deserializerについて-のページです。

www.ricos.co.jp

Serde入門 (1) Derive属性の裏側をちょっと覗く - Crieit

Serde入門 趣旨 Bayardという全文検索エンジンがあり保存されているドキュメントはJSON形式で取得できる. serde_jsonを使えば良いのだが, serde_jsonではhierarchical_facetというデー...

crieit.net

Why are there 2 types for deserializing in serde?

To expand a bit on @RustyYato's notes about the serde data model: serde has sort of a two-phase process to deserialization. One part deals with generic data formats, and the other deals with specific data structures. The job of the Deserializer is to deal with the "general-purpose" data format. It parses input and converts it into the basic serde data model. It's kind of like if you took all of your inputs in whatever format (TOML, CBOR, etc.) and converted them all to JSON so that the rest of ...

users.rust-lang.org

Visitorの理解を助けてくれました