【ITニュース解説】Rust: A JSON parser
2025年09月28日に「Dev.to」が公開したITニュース「Rust: A JSON parser」について初心者にもわかりやすく解説しています。
ITニュース概要
RustでJSONパーサーを自作する方法を解説。JSONの様々なデータ型をRustのEnumで表現し、JSON文字列から各要素を識別し、対応するRustの型へ変換するパースロジックを構築する。エラー処理も考慮されている。
ITニュース解説
JSON(JavaScript Object Notation)は、Webアプリケーションなどで広く使われているデータのやり取り形式だ。人間にとっても読みやすく、コンピューターにとっても解析しやすい構造をしている。例えば、設定ファイルやAPIからのデータ取得などでよく利用される。このニュース記事では、Rustというプログラミング言語を使って、このJSON形式の文字列データを、プログラムが扱える形に変換する「JSONパーサー」の作り方が解説されている。Rustは安全性や並行処理に強みを持つシステムプログラミング言語で、JSONパーサーのような基盤となるツールを作るのに非常に適している。
JSONが扱えるデータの種類はいくつか決まっている。具体的には、真偽値(true/false)、数値(整数や小数)、文字列、配列(複数の要素のリスト)、オブジェクト(キーと値のペアの集まり)の5種類だ。このパーサーを作る上で最初の課題は、これらのJSONのデータ型をRustのプログラム内でどう表現するか、ということになる。
記事では、Rustの「enum(列挙型)」という機能を使ってこれを解決している。enumは、複数の選択肢の中から一つを選ぶような型を定義するのに便利だが、Rustのenumはさらに強力で、それぞれの選択肢が別のデータ型を「ラップ(包み込む)」することができる。
具体的には、JsonTypeというenumが定義されている。Object(HashMap<String, JsonType>)はJSONオブジェクトを表現し、キー(文字列)と値のペアを効率的に管理するRustのHashMap(ハッシュマップ)を使う。ここで値の型がJsonType自身である点がポイントで、オブジェクトの中に別のオブジェクトや配列など、あらゆるJSONの型を入れ子にできる。Array(Vec<JsonType>)はJSON配列を表現し、可変長配列であるVec(ベクター)を使う。これも要素の型がJsonTypeなので、配列の中に様々なJSONの型を入れられる。JSON文字列はRustのString型で、JSON真偽値はRustのbool型でそのまま表現される。JSON数値は整数と小数を区別するため、整数はNumber(i64)(64ビット整数)、小数(浮動小数点数)はDecimal(f64)(64ビット浮動小数点数)として分けて表現される。このようにJsonTypeという一つのenumで、あらゆるJSONデータを表現できる柔軟な構造が作られている。
パーサーは、不正なJSON形式の入力を受け取った場合や、予期しない記号が現れた場合にエラーを適切に処理する必要がある。記事では、パーシング中に発生する可能性のある様々なエラーを表現するために、ParserErrorという別のenumを定義している。
UnexpectedToken(String)は予期しない文字が現れた場合のエラー、InvalidSyntax(String)はJSONの文法自体が不正な場合のエラー、MissingToken(String)は必要な文字が欠けている場合のエラー、EmptyInputは入力が空だった場合のエラー、NotSupported(String)はパーサーが特定の機能をサポートしていない場合のエラーだ。これらのエラー型を使うことで、どのような問題が起きたのかを具体的にプログラムに伝えることができる。Rustでは、このような成功と失敗の両方の可能性を表現するためにResultという標準のenumがよく使われる。Result<T, E>は、成功した場合はOk(T)(Tは成功時の値)、失敗した場合はErr(E)(Eはエラー情報)という形で結果を返す。これにより、呼び出し側はパーシングが成功したのか失敗したのかを明確に判断し、適切な処理を行うことができるのだ。
JSONパーサーの実際の処理は、parse_jsonという公開関数から始まる。この関数は、JSON文字列を受け取り、解析結果としてJsonTypeのデータ、またはエラー情報ParserErrorを返すResult型を返す。
まず、入力文字列が空でないかをチェックする。空であればEmptyInputエラーを返す。次に、入力文字列の先頭にある空白文字を全て取り除く。
そして、JSONデータは必ずオブジェクト({で始まる)か配列([で始まる)のどちらかで始まる、というJSONのルールを利用する。parse_json関数は、入力文字列の最初の文字を見て、それが{ならJSONオブジェクトのパーシングを開始するparse_object関数を呼び出し、[ならJSON配列のパーシングを開始するparse_array関数を呼び出す。どちらでもない場合は、UnexpectedTokenエラーを返して処理を終了する。このように、トップレベルのデータ型を最初に判断し、適切な専用のパーシング関数に処理を委ねるのが、このパーサーの基本的なアプローチだ。
記事では、JSONオブジェクトを解析するparse_object関数の詳細な実装が説明されている。この関数もまた、解析が成功した場合はResult型で、解析されたHashMapと、解析されずに残った入力文字列の一部を返す。これは非常に重要な設計パターンだ。各パーシング関数が「自分が解析した部分」を処理した後、「残りの部分」を次の関数に渡すことで、複雑な構造を持つJSONデータでも段階的に解析を進めることができる。
parse_object関数はまず、受け取った文字列が{で始まっているかを確認し、そうでなければInvalidSyntaxエラーを返す。問題なければ、その{をスキップし、残りの文字列から空白を取り除く。
次に、オブジェクトのキーと値のペアを繰り返し解析するためのループに入る。ループの各段階で、現在の入力文字列の最初の文字をチェックする。
もし最初の文字が}であれば、これは空のオブジェクトか、全てのキーと値のペアの解析が終わり、オブジェクトの終わりに到達したことを意味する。この場合、解析済みのHashMapと残りの文字列(}の後の部分)を返してループを抜ける。
そうでなければ、最初の文字はキーの始まり、つまり"であるはずだ。JSONのキーは必ず文字列なので、parse_string関数を呼び出してキーを解析する。
キーの解析が成功したら、次に:(コロン)が来ることを期待する。:がなければMissingTokenエラーとなる。:をスキップし、残りの文字列から空白を取り除く。
次に、値の解析に進む。値は様々な型(オブジェクト、配列、文字列、真偽値、数値)のいずれかを取りうるため、入力文字列の最初の文字を見て、どの型のパーシング関数を呼び出すかを判断する。例えば、{で始まればparse_objectを、[で始まればparse_arrayを呼び出すといった具合だ。各パーシング関数は、その型のデータをJsonTypeとして返し、さらに解析されずに残った文字列も返す。この残りの文字列が、次の解析ステップの入力となる。
解析されたキーと値は、HashMapにinsert(追加)される。
最後に、次のキーと値のペアがあるか、それともオブジェクトの終わりかを判断するため、カンマ,または閉じブレース}をチェックする。,があれば次のペアの解析を続けるためにループの先頭に戻り、}があればオブジェクトの解析を終了する。どちらでもない場合はUnexpectedTokenエラーとなる。
この一連のステップを繰り返すことで、JSONオブジェクトの全てのキーと値のペアが正しく解析され、RustのHashMap<String, JsonType>というデータ構造としてプログラム内で扱えるようになる。この再帰的な呼び出し(オブジェクトの中にオブジェクトを解析するparse_objectが呼び出される、配列の中にオブジェクトを解析するparse_objectが呼び出される)が、複雑なJSON構造を解析する上で非常に重要な仕組みとなっている。
このJSONパーサーは、JSONという汎用的なデータ形式を、Rustの強力な型システムとenum、HashMap、Vecといったデータ構造を駆使して、安全かつ効率的にプログラム内で扱えるように変換する仕組みを示している。特に、JsonType enumによる柔軟な型表現、Result enumによる堅牢なエラーハンドリング、そして各パーシング関数が「解析された部分」と「残りの入力文字列」を返すことで複雑な構造を段階的に解析していく再帰的なアプローチが、このパーサーの核となるアイデアだ。システムエンジニアにとって、このように外部のデータをプログラム内部のデータ構造に変換する「パース」の概念は非常に重要であり、この解説を通じてその基本的な考え方と実装の一端を理解できるだろう。