厳格なJSONパーザーの作り方

僕の作ったJSONパーザーのDynaJsonはとても厳格にできている。RFC 8259に準拠しているものはすべて受理するし、そうでないものは二つの例外を除きすべて受理しない。

受理する例外は、ケツカンマと02-02のような0で始まる数字である。前者を受理するのは実用性のためであり、後者を受理するのはDynamicJsonとの互換性のためである。

JSONの文法はとても単純だが、不用意にパーザーを実装するとRFCに準拠したJSONを受理しなかったり、非準拠のものをエラーにできなかったりする。パーザーを実装する際の地雷ポイントはParsing JSON is a Minefieldによくまとまっている。

たとえば、nullのようにリテラルだけだと受理しないパーザーがある。[[][]]のような空の配列だけの配列や、1e0のように指数部が0の数字を受理しないものもある。{"a":1,"a":2}のようにキーが一意でないものを受理しないパーザーは多い。

ほとんどのパーザーが、さまざまな非準拠のJSONを受理する。.2-.1+1のような数字や、文字列にコントロール文字が直接入っているものや、[0]]のように後ろのごみがあるものなど。

DynaJsonのために最初に書いたJSONパーザーは、思い込みと不注意からかなりの地雷を踏んでいた。最終的には準拠しているものはすべて受理し、準拠しないものは、入力の破損を検出するために二つの例外以外はエラーにするようにした。

Parsing JSON is a Minefieldの著者の作成したJSON Parsing Test Suiteを利用すると、このようなコーナーケースの動作確認ができる。.NETに対応した6つのJSONパーザーについて実行できるようにしたものをGitHubに置いた。その実行結果を示す。

左側がテストしたJSONのファイル名、真ん中が結果、右側がそのJSONである。ファイル名のうちy_で始まるのは受理すべきもの、n_は却下すべきもの、i_はどちらでもよいもの。結果の色については左上の説明の通り。

.NET Core 3.0から入ったSystem.Text.Jsonはとても厳格であり、準拠のものをすべて受理し、非準拠のものをすべてエラーにする。ただし、オプションで指定すればケツカンマを受理する。この中でケツカンマを受理しないのはUtf8Jsonだけである。

パーザーごとに受理する非準拠のJSONに特徴がある。Json.NETはキーが文字列でないオブジェクトを受理し、Utf8Jsonは末尾のごみを許容する。DynamicJsonはオブジェクトが}で閉じていないものを受理する。Jilは数値の指数部に寛容である。

一番下の赤になっている部分は、10万回ネストしたJSONをパーズした結果である。DynamicJsonは5秒で終わらずタイムアウトし、JilとUtf8Jsonはスタックオーバーフローでクラッシュしている。

Json.NETはデフォルトでメモリの許す限りネストが可能である。System.Text.Jsonはデフォルトで64までだがオプションで変更可能である。DynaJsonはデフォルトで512までだがオプションで変更できる。

コメントを残す

メールアドレスが公開されることはありません。 が付いている欄は必須項目です