この記事は、Zennにも投稿しています。
この記事はRust Advent Calendar 2023 シリーズ3の19日目の記事です。
前回のSerdeのDeserializerを実装する(Part1)の続きとして、この記事ではserdeの公式ドキュメントにある「Implementing a Deserializer」というページで実装されているjsonのデシリアライザ(を一部改変したもの)についてチュートリアル風に解説します。逆に言えばこのページの内容を理解していればこの記事を読む必要はないかもしれません…)。
と言っても全て解説すると分量が多すぎなので基本的なところのみを紹介します。
このページで実装されているデシリアイザは結構実践的で、このページの内容が理解できればserdeで割と思い通りのDeserializerを実装できるんじゃないかと思います。
と言っても全て解説すると分量が多すぎなので基本的なところのみを紹介します。
このページで実装されているデシリアイザは結構実践的で、このページの内容が理解できればserdeで割と思い通りのDeserializerを実装できるんじゃないかと思います。
この記事のコードは大体
example-format/src/de.rs at master · serde-rs/example-format
An example Serializer and Deserializer data format for Serde - serde-rs/example-format
github.com
からのコピペですが若干違うところもあるので
GitHub - nazo6/serde-deserializer-example
Contribute to nazo6/serde-deserializer-example development by creating an account on GitHub.
github.com
こちらのリポジトリに記事に沿ったソースコードを載せておきます。それぞれのソースコードには
serde-rs/example-format
のソースコードのリンクを貼っておくのでserdeのドキュメントと見比べていただくとより理解しやすいのではないかと思います。雛形の作成
Deserializerの雛形を作ったものが以下になります。詳細についてはPart1の記事を参考にしてください。
この状態では何を入力されてもエラーを返す、意味のないDeserializerになっています。
ユーティリティの作成
これから
deserialize_*
メソッドを実装するにあたり、あると便利なメソッドがあるので実装しておきます。ref: L54
これから実装するデシリアライザでは文字列を一文字づつ進めながら解析を行います。そのため、文字列を消費して次の文字を得る
next_char
と進めずに文字を得るpeek_char
メソッドが実装されています。serdeの
これは個人的な感想なのですが、Rustでは可変参照の取り扱いの難しさからこのように状態を変化させながら処理を行うことは少ないような気がしていて、頭の切り替えが必要でした。また、そのようなプログラムをここまで大規模に作ったserde、もといdtolnayさんはやはりすごいな…と感じました。
Deserializer
を実装する上で頭に入れておくと良いことは、Deserializer内部の状態をどんどん変化させながらパースを進めていくということです。これは個人的な感想なのですが、Rustでは可変参照の取り扱いの難しさからこのように状態を変化させながら処理を行うことは少ないような気がしていて、頭の切り替えが必要でした。また、そのようなプログラムをここまで大規模に作ったserde、もといdtolnayさんはやはりすごいな…と感じました。
ついでにエラーを簡単に返すためのマクロも作っておきます。これは記事の都合上エラー処理を簡単にするためです。実際にはちゃんとErrorのEnumにバリアントを追加しましょう。
deserialize_any
について
前回の記事では、
false
かtrue
の文字列のみをDeserializeできるDeserializeを作成しましたが、その際、deserialize_any
メソッドを実装することでbool
型以外をエラーにしました。しかし、deserialize_any
はそれ以外に重要な用途があり、「Self-describingなデータ」をデシリアライズする際に、デシリアイズするべき構造体の情報を見ずにデシリアライズを行うことができます。"Self-describing"なデータ形式というのは、データ自体に型の情報を含むデータのことです。例えばjsonはSelf-describingな形式であり、データ中に現れる文字を見ることでデータ構造を決定できます。例えば「
これによりボイラープレートを削減できる他、
{
」はマップ構造、「"
」は文字列を意味する、などです。これによりボイラープレートを削減できる他、
serde_json
ではserde_json::Value
型を使うことで型を決めなくても柔軟にjsonを処理できますが、これにもdeserialize_any
が活用されているようです。しかしながら、上に挙げたserdeのドキュメントのサンプルには「The code below implements every method explicitly for documentation purposes but there is no advantage to that.」とあり、
deserialize_any
メソッドは実装されているもの他のdeserialize_*
メソッドも全て実装されており、forward_to_deserialize_any!
が使われていないため実行されることはありません。ですが、せっかくなので今回はdeserialize_*
メソッドは必要な場所のみに実装して可能な限りdeserialize_any
を使うようにしたいと思います。deserialize_any
の実装
そんなわけで、先程作成した
JsonDeserializer
にdeserialize_any
を実装しました。デシリアイズごとに次のデシリアライズ単位まで文字列を消費するのでこのように最初の一文字を見れば型をある程度決定できます。とりあえず実装が簡単な
bool
と()
型はデシリアライザを書いておきました。それ以外のデシリアライズについて以下で解説します。
数字のパース
まず数字からです。
今回はu64とi64に決め打ちしていますが実際に実装するときはきちんと切り替えられるようにしたほうがいいでしょう。
文字列のパース
文字列のパースは他とは違い、ライフタイムのことを考えなければいけません。デシリアライザのライフタイムについては詳しくserdeのドキュメントに書いてあります。とは言っても今回は呪文のように
'de
を付けるだけです。また、エスケープのことを考えなければなりません。これは難しいのでとりあえず考えないことにしましょう(serdeのサンプルコードもそう言ってる)。
ref: L123
配列のパース
配列は若干特殊で、visitorに
serdeのドキュメントの例では
SeqAccess
traitを実装した構造体をvisitor.visit_seq
に与えることでデシリアライズを行います。このトレイトで実装しなければならないのはnext_element_seed
メソッドのみです。serdeのドキュメントの例では
CommaSeparated
という構造体に実装されています。CommaSeparated
にはDeserializer
への参照が保持されており、そのライフタイムは'de: 'a
、つまり'de
より短い間有効です。これは直感的に納得できるのではないかと思います。next_element_seed
は配列の各要素をデシリアライズするために呼ばれます。seed: DeserializeSeed
という値が渡されるため、このseed.deserialize
に文字列を与えることでデシリアライズされた要素の中身を得ることができます。構造体のパース
一つのマップ状要素(Key-Value)のそれぞれについて
next_key_seed → next_value_seed
の順に呼ばれます。serdeの例では、先程の配列のパースの時に
CommaSeparated
という概念として構造体を抽象化したのでそれを再利用しています。賢いkeyとvalueそれぞれでシリアライズするということ以外は配列の時と大体同じです。
試してみる
では実際にこれがちゃんと動くか試してみましょう。
正しそうですね!
最後に
スペースとか改行とか考えなければならないことはまだまだありますが、これでserdeの
Deserializer
を書く上で主となる要素はある程度カバーできたのではないかと思います。何か参考になれば幸いです。また、自分としても正直理解が怪しいところもあり、間違った解釈をしている可能性があるのでそういう箇所があれば是非教えていただけると助かります。