本記事でやること
-
encoding/json/v2のUnmarshal関数の内部実装を読み解く - JSON値全体の構文を検証する仕組みを理解する
対象読者
-
encoding/json(v1, v2)を使ったことがある方 - Go標準ライブラリの内部実装に興味がある方
はじめに
本記事はGo1.25で実験的に追加されたencoding/json/v2パッケージのUnmarshal関数の内部実装を追っていくシリーズの第2回です。
- 第1回: パッケージ設計とメモリ効率編
- 第2回: JSONの構文検証編(本記事)
今回は、v2のv1互換モードで実行される「JSONの構文検証」の仕組みを見ていきます。
前回のおさらい
前回の記事では、Unmarshal関数が以下のような流れで処理を進めることを確認しました。
今回はunmarshalDecode関数の内部で行われる構文検証を追っていきます。
v2のv1互換モードとは
v2では、GOEXPERIMENT=jsonv2を有効にすると、encoding/jsonパッケージが内部的にv2の実装を使うようになります。
このとき、既存のv1コードとの互換性を保つため、「v1互換モード」が用意されています。
v1の構文検証
以下は、v1のUnmarshal関数はcheckValid関数で事前にJSONの構文検証を実施します。
v2のv1互換モード
v2でも同様に事前にJSONの構文検証を行うために、ReportErrorsWithLegacySemanticsオプションが用意されています。
このオプションは、GOEXPERIMENT=jsonv2環境下で内部的に呼ばれるv2のUnmarshal関数の入力引数に設定されているDefaultOptionsV1()によって自動的に有効化されます。
CheckNextValue関数によるJSON値全体の構文検証
CheckNextValueは、JSON値全体の構文を検証しますが、読み取り位置は進めません。
decodeBuffer構造体のおさらい
decodeBuffer構造体は以下のようなJSONを読み進めていくための読み取り位置の情報などを保持する構造体です。詳しい内容は第1回の記事を参照してください。
-
peekPos: JSONのtokenを先読みした位置 -
peekErr: 先読みのエラー
PeekKind関数による読み取り位置の特定
PeekKindはJSONの空白や区切り文字をスキップして、読み取る対象のJSONの値の開始位置をdecodeBufferのpeekPosに設定します。
peekPosのクリアによる後続処理への準備
PeekKindによる読み取り開始位置をpos変数にコピーしたあと、peekPosをクリアします。これは、後続に控えているunmarshal処理で間違った位置が使われないようにするためです。
consumeValue関数によるJSON値の種類判定
JSONの読み取り開始位置が決まった後にconsumeValue関数が呼ばれます。
consumeValue関数は、JSON値の種類を判定し、それぞれに応じた処理を行います。
ここからは、具体的なJSON{"name":"Alice"}を例に処理内容を追っていきたいと思います。
{"name":"Alice"}の場合
読み取り開始位置pos=0の文字は{なので、以下のcase文に入り、consumeObject関数が実行されます。
consumeObject関数によるオブジェクトの構造検証
consumeObjectはJSONの構造を検証し、値の部分ではconsumeValue関数を再帰的に呼び出す処理になっています。
ステップ1: 読み取り開始位置を1つ進める
上記のconsumeValue関数で{は処理済みなので、開始位置posを1つ進めます。
ステップ2: キー("name")を読み取る
キーの文字列部分(引用符を含めた)の長さを返すConsumeSimpleString関数に d.buf[1:] = "name":"Alice"}を入力し、キー("name")の長さ(6)を得ます。
その後、エラーがなければ読み取り開始をキーの長さ分だけ進めます。
-
pos += n=1 + 6 = 7 -
quotedName = d.buf[1:7]="name"
ステップ3: :(コロン)を確認
キーの値の後に:(コロン)が存在することを確認します。その後、読み取り開始位置を進めます
ステップ4: 値("Alice") を消費(再帰呼び出し)
次に値の部分を読み取ることになります。ここで、consumeValue関数が再帰的に呼ばれます。
現在の読み取り開始位置は:(コロン)の直後の"なので、consumeValue関数のswitch/case文では以下に入ることなります。
先で説明した通り、ConsumeSimpleString関数は引用符を含む値の長さを返してくれるので、nは"Alice"の引用符を含む7を返します。
その後、読み取り開始位置posをn分("Alice")だけ進めてconsumeValue関数を抜けます。
ステップ5: }を確認する
再帰的に呼ばれたconsumeValueを抜けた後、consumeObject関数に戻ります。現在の読み取り開始位置は}になっているので、以下のcase文に入り、posを1つ進めて終了します。
再帰構造の意味
consumeValueとconsumeObjectは相互に再帰呼び出しを行います。このような再帰構造により、ネストされたJSONも処理できるようになっていると考えます。
consumeValue(pos=0)
├─ '{' を検出
└─ consumeObject を呼ぶ
├─ キー "name" を処理
└─ 値の部分で consumeValue(pos=8) を呼ぶ
├─ '"' を検出
└─ "Alice" を処理
まとめ
JSON値全体の構文検証の流れ
-
CheckNextValue: JSON値全体を検証するが、読み取り位置は進めない -
consumeValue: 値の種類を判定し、適切な処理に振り分ける -
consumeObject: キーと値のペアを処理し、値の部分でconsumeValueを再帰的に呼び出す
{"name":"Alice"}の処理まとめ
| 位置 | 処理 | pos |
|---|---|---|
| 0 |
consumeValue: { を検出 |
0 |
| 0 |
consumeObject: { をスキップ |
1 |
| 1~7 | キー "name" を消費 |
7 |
| 7 |
: を確認してスキップ |
8 |
| 8~15 |
consumeValue: "Alice" を消費 |
15 |
| 15 |
} を確認してスキップ |
16 |
設計の特徴
- 再帰構造:
consumeValue⇄consumeObjectの相互呼び出し - 位置管理:
posを受け取り、処理後の位置を返す
次回予告
次回は、構文検証が完了した後、実際に値をGoの型にunmarshalする処理を見ていきます。