📦

gotipでencoding/json/v2を試す

に公開

gotipgo1.25rc1でencoding/json/v2を試す

長いことdiscussionにあったencoding/json/v2ですが2025-01-31に以下のproposalに移行しました。

https://github.com/golang/go/issues/71497

2025-04-18にGOEXPERIMENT=jsonv2下で利用できるようになるCLがマージされました。

https://go-review.googlesource.com/c/go/+/665796

すでにgo1.25rc1GOEXPERIMENT付きで試せるようになっています。
iteratorのように問題なく進めばGo 1.26で正式実装になるでしょう。

筆者は以下の記事などで何度かencoding/json/v2について触れていますが

https://zenn.dev/ngicks/articles/go-json-undefined-or-null-v2

何が変わったかなどをついてこの記事で述べていきたいと思います。この記事は結構焼き増しです!!!

以後特にほかに述べられないときv1とはencoding/jsonのことをさし、v2とはencoding/json/v2のことをさします。

EDIT LOG

edit logs

2025-06-12

go1.25rc1がリリースされたのでそれに合わせて変更しました。

2025-05-23

2025-05-14

サンプル

以下に上がります。

https://github.com/ngicks/go-play-encoding-json-v2

環境

$ go version
go version go1.25rc1 linux/amd64

(いらなくなったので省略)gotipで開発できるようにする

go 1.25rc1がリリースされて不要になりました。

gotipで開発できるようにする
go install golang.org/dl/gotip@latest
go download 665796
export PATH=$(gotip env GOROOT)/bin/:$PATH
export GOEXPERIMENT=jsonv2

gotipを入れて前述のCLをダウンロードします。マージされていますので別にmasterの先頭を入れてもいいと思います。

PATHをいじってgoコマンドがgotipのものになるようにします。
Go 1.21からGOTOOLCHAINという概念が追加されています。
それまでのGogoコマンドのバージョンが今コンパイルの対象となっているGo moduleのそれより低かろうがコンパイルを試みるような挙動だったようですが、
gotoolchain追加後は現在呼び出されたgoコマンドよりもgo.modの内容が新しければ自動的にtoolchainをダウンロードする挙動になっています。

ここで不都合なのがgotipで最新を落としてgotip mod initするとgo.modには最新の次のバージョンで記載されます。つまり今回だとgo 1.25です。
このtoolchainは現在存在しませんからダウンロードしようとするところでエラーになります。

最後にGOEXPERIMENTを設定しておきます。

プロジェクトを作成

$ GOTOOLCHAIN=go1.25rc1 go mod init github.com/ngicks/go-play-encoding-json-v2
go: creating new go.mod: module github.com/ngicks/go-play-encoding-json-v2
$ ls
go.mod
$ cat go.mod
module github.com/ngicks/go-play-encoding-json-v2

go 1.25rc1

プロジェクトを実行したり、goplsを起動するには若干面倒ですが下記の手順が必要です

$ export GOEXPERIMENT=
$ export PATH=$(GOTOOLCHAIN=go1.25rc1 go env GOROOT)/bin:$PATH
$ export GOEXPERIMENT=jsonv2
$ go version
go version go1.25rc1 linux/amd64

こうしないとgoがパニックします。多分ですがgo commandがビルドされた時点で存在していたGOEXPERIMENT以外が存在していることが条件です。
PATHで指定されるgogo1.24.4とかだとGOEXPERIMENT=jsonv2が存在しません。

既存のプロジェクトに追加して遊ぶ場合は

//go:build (go1.25 && goexperiment.jsonv2) || go1.26

をファイルの先頭に付け足します。このbuild constraintによって、go1.25かつGOEXPERIMENT=jsonv2のときのみコンパイル対象になります。
go1.26以降にGOEXPERIMENTなしになっているのは希望的観測なのですが多分大丈夫でしょう。

経緯

詳しくは前述のdiscussionに書かれているのでそこを読んでほしいのですが、適当にまとめ直すと

encoding/jsonにはいろいろ問題がありました。

  • 機能が欠けている
    • time.Timeのフォーマットを指定できない
    • 特定の値をomitできない(e.g. zero valueのtime.Time)
    • slice([]T)やmap(map[K]V)がnilのとき必ずnullが出力され、[]{}を出力する方法がない
    • 型をembedする以外に値をinlineする方法がない
    • strcutなどに向けてunmarshalするとき、想定されないフィールドが含まれるときにそれらをどこかにfallbackする機能がない
    • etc, etc.
  • APIが変
    • json.NewDecoder(r).Decode(v)がよくされるが、r io.Readerから1つのJSON Valueを取り出すだけでそのあとにゴミデータがあった場合などにエラーにならない。
    • Decoder, EncoderにOptionを設定する方法があるが(SetEscapeHTMLとかDisallowUnknownFieldsとかのこと)、json.Marhsal, json.Unmarshalにはない
    • json.Compact, json.Indent, json.HTMLEscapeなどがbytes.Bufferを使っていること。より柔軟な[]byte, io.Writerなどを使わないこと。
  • パフォーマンスが悪い
    • MarshalJSONのinterfaceが[]byteを返すものであり、毎回allocationが要求される
    • UnmarshalJSONのinterfaceが[]byteを受け取るものであり、encoding/jsonはJSON Valueの終端までいったん解析し、json.Compactをかけてから渡していた。また、UnmarshalJSONの実装側でももう1度JSON Valueの解析が行われることになる。
    • json.MarshalIndentを呼び出すとindentのついていないjsonを書きだした後に再度解析を行ってindentを挿入するような処理になっている。
  • streaming APIが存在しない。
    • Encoder.EncodeToken proposalは通過したが、実装はされていない(#40127)
    • json.Tokenはinterfaceであるため、JSON numberやstringをbox化するときにallocateしてしまう。
    • 理屈上最も大きなJSON Tokenのみバッファーすればよいはずだが、Encoder.EncodeおよびDecoder.DecodeはJSON Value単位でバッファーしまてしまう。
  • 挙動が変
    • 準拠しているRFCが古い。下記の時間とともに以下のようにRFCが更新されて行っている。encoding/jsonが準拠するのはRFC 7159v2が準拠するのはRFC 8259
    • array([n]T)の長さが不一致でもunmarshalが成功する
    • json:",string"オプションが数値以外にも適用される、また、[]intなどにrecursiveに適用されない。
    • Unmarshal時のGo structフィールド名とJSON objectのキー名とのマッチングがcase-insensitive。
      • json:"name"で指定していたとしてもcase-insensitiveでした。
    • underlying typeがnon-addressableである型のMarshalJSON / UnmarshalJSONが呼ばれない。
      • method receiverがpointerでjson.Marshalに渡された値がnon-pointerだった時のことをさしています。
    • unmarshalのターゲットにnon-zeroな値を渡したときの挙動に一貫性がない
      • sliceはunmarshal後の長さが短くなる場合、s[len(s):cap(s)]の区間はzero化されていませんでした。
    • エラーに一貫性がなく、io error, syntax error, semantic errorが構造化されずにごっちゃに返されている。

破壊的変更を避けながらこれらを解消することはできるかもしれないが、デフォルトの挙動をRFC 8259に準拠させるなどしたほうが良いと思われるのでv2として破壊的変更を加えよう、みたいな感じです。

v1からの顕著な変更

encoding/json/jsontextとencoding/json/v2に分かれる

proposalに貼られていますが、以下のように構造が変化します。

encoding/json/jsontextを追加し、ここでJSONの文法を処理します。
encoding/json/v2を追加し、ここでjsontextで処理されたトークン情報を用いて、v1と同様にreflectなどを使用してGoの値との相互的なやり取りを実現します。

encoding/json/v2

json.Marshal, json.Unmarshalとほぼ同等なものに加えて、io.Reader/io.Writer, *jsontext.Encoder/*jsontext.Decoderを引数に取る新しいAPIが追加されます。

https://github.com/golang/go/blob/0e17905793cb5e0acc323a0cdf3733199d93976a/src/encoding/json/v2/arshal.go#L167

https://github.com/golang/go/blob/0e17905793cb5e0acc323a0cdf3733199d93976a/src/encoding/json/v2/arshal.go#L183

https://github.com/golang/go/blob/0e17905793cb5e0acc323a0cdf3733199d93976a/src/encoding/json/v2/arshal.go#L203

https://github.com/golang/go/blob/0e17905793cb5e0acc323a0cdf3733199d93976a/src/encoding/json/v2/arshal.go#L398

https://github.com/golang/go/blob/0e17905793cb5e0acc323a0cdf3733199d93976a/src/encoding/json/v2/arshal.go#L415

https://github.com/golang/go/blob/0e17905793cb5e0acc323a0cdf3733199d93976a/src/encoding/json/v2/arshal.go#L448

見てのとおり、Optionsという型でoptionを受け付けられるようになっており、ユーザー側で柔軟な挙動の変更が可能です。

encoding/json/jsontext

*json.Encoder/*json.Decoderに当たるものとして*jsontext.Encoder/*jsontext.Decoderが実装されます。
それぞれio.Writer/io.Readerを引数にとって初期化します。

https://github.com/golang/go/blob/0e17905793cb5e0acc323a0cdf3733199d93976a/src/encoding/json/jsontext/encode.go#L84-L95

https://github.com/golang/go/blob/0e17905793cb5e0acc323a0cdf3733199d93976a/src/encoding/json/jsontext/decode.go#L117-L126

JSONのToken単位での読み書きをするWrite/ReadTokenと,JSON Value単位("foo"のようなstring literalや{"foo":"bar"}のような1つのJSON Objectなど)での読み書きをするWrite/ReadValueがあり、v1に比べるとよりレキシカルな操作が可能です。

また、*json.Decoder.Moreの代わりにPeekKindがあり、これによって次のtokenの種類(kind)をしることができます。

https://github.com/golang/go/blob/0e17905793cb5e0acc323a0cdf3733199d93976a/src/encoding/json/jsontext/decode.go#L302-L309

(v1では読んじゃったらUnreadできないからすごい困ってたんですよ)

*jsontext.Encoder/*jsontext.Decoderともに、StackDepth, StackIndex, StackPointerを実装しており、現在のnestの深さ、ある深さの開始Token({なのか[なのか)、現在の位置のJSON Pointer(RFC 6901)をそれぞれ知ることができます。

https://github.com/golang/go/blob/0e17905793cb5e0acc323a0cdf3733199d93976a/src/encoding/json/jsontext/decode.go#L1126-L1134

https://github.com/golang/go/blob/0e17905793cb5e0acc323a0cdf3733199d93976a/src/encoding/json/jsontext/decode.go#L1136-L1158

https://github.com/golang/go/blob/0e17905793cb5e0acc323a0cdf3733199d93976a/src/encoding/json/jsontext/decode.go#L1160-L1163

jsontext.Tokenは構造体、jsontext.Value[]byteであり、interfaceではないので不要なbox化が起きなくなっています。

https://github.com/golang/go/blob/0e17905793cb5e0acc323a0cdf3733199d93976a/src/encoding/json/jsontext/token.go#L32-L90

https://github.com/golang/go/blob/0e17905793cb5e0acc323a0cdf3733199d93976a/src/encoding/json/jsontext/value.go#L38-L47

Options

多くのv2 APIがうけつけるOptions型によって、encoder単位、呼び出し単位でふるまいのカスタマイズが可能になっています。

https://github.com/golang/go/blob/0e17905793cb5e0acc323a0cdf3733199d93976a/src/encoding/json/internal/jsonopts/options.go#L7-L18

Optionsはinterfaceですが上記のとおり現状encoding/json以下のpackageからしか定義ができないようになっています。
逆に言うとこのハックによってencoding/json/v2, encoding/json/jsontextのそれぞれにふさわしいpackageでOptionsが定義されています。

https://github.com/golang/go/blob/0e17905793cb5e0acc323a0cdf3733199d93976a/src/encoding/json/jsontext/options.go#L17-L45

上記のようにいろいろあります。

関連APIがOptionsを受け取るほか、*jsontext.Encoder/*jsontext.Decoderがこれを引きまわすようになっているため、MarshalJSONTo/UnmarshalJSONFromの実装もこれらを受け取ることができます。

例えばv2v1json.MarshalIndent相当のことをするにはjsontext.WithIdentv2.Marshal*系APIに渡します。

https://github.com/golang/go/blob/0e17905793cb5e0acc323a0cdf3733199d93976a/src/encoding/json/jsontext/options.go#L221-L255

*jsontext.EncoderはこのOptionsが渡されていた場合、streamへの書き出し時にObjectやArrayがnestするたびにStackDepthに応じたインデントを書きだします。v1では一旦Marshalをして[]byteを出力し、これを解析してインデントをつけなおすという遠回りな処理をしていましたが、v2ではOptionsがencoderについて回ることで処理がずいぶんシンプルになっています。

encoder単位、呼び出し単位でのふるまいの変更ができるOptionsで顕著なものはMarshalToFunc[T any]/UnmarshalFunc[T any]です。
これらは、関数をあたることで特定の型Tのmarshal, unmarshalのふるまいを変えることができます。
v1まではjson.Marshaler/json.Unmarshalerを型に実装させるしかありませんでしたが、v2では呼び出しごとに変更することができるほか、closureを渡すことができるので、例えばある型が見つかるたびchannelに送信するとか、カウントをインクリメントするとかもできます。

https://github.com/golang/go/blob/0e17905793cb5e0acc323a0cdf3733199d93976a/src/encoding/json/v2/arshal_funcs.go#L204-L251

https://github.com/golang/go/blob/0e17905793cb5e0acc323a0cdf3733199d93976a/src/encoding/json/v2/arshal_funcs.go#L287-L336

見てのとおりそれぞれencoder/decoderを受け取るため、StackDepth, StackIndex, StackPointerを用いて階層情報を取得しそれに基づいてふるまいを変えることもできるようになっています。

struct tag

json:"" struct tagも大幅な変更があります。

https://github.com/golang/go/blob/0e17905793cb5e0acc323a0cdf3733199d93976a/src/encoding/json/v2/fields.go#L469-L555

特に顕著なのは

  • case:value, format:valueなどでフォーマットを指定できるように(のちにサンプルを示す)
    • 前述のtime.TimeRFC3339以外でmarshal/unmarshalできなかった問題が解決します。
  • inlineで型をembedしなくてもembedしてた時みたいにmarshal/unmarshalされます
  • unknownでstrcut fieldなどで指定していない(=不明な)フィールドをまとめて格納することができます。(のちにサンプルを示す)
  • json:"name"のname部分をsingle quotation mark(')でescape可能に
    • json:"',\"'"と設定すれば、",\""というフィールドが出力されます。

discussion版からの顕著な変更

https://github.com/golang/go/issues/71497#issuecomment-2626483666

顕著な変更(主観)は

  • MarshalJSONV2/UnmarshalJSONV2 -> MarshalJSONTo/UnmarshalJSONFromに改名
  • MarshalJSONTo/UnmarshalJSONFromがoptionsを受け取らなくなった。
    • 代わりにjsontext.Encoder/jsontext.DeocderOptionsを返すことができるように。
  • jsontext.Encoder/jsontext.DecoderStackPointerstringの代わりにjsontext.Pointerを返すように。
  • jsontext.ObjectStart/jsontext.ObjectEnd -> jsontext.BeginObject/jsontext.EndObjectに変更

その他いろいろ追加されています。

使ってみる

v2.Marshal / v2.Unmarshal

Marshal/Unmarshalの使用感はあまり変わりません。

ちょっとしたコツとして、v2.Marshalに渡す値は必ずaddressableなもの、つまりポインターにします。
しない場合、v2は一旦値をコピーしてポインターに変換しなおします。これはnon-addressableな値だとmethod receiverがpointerだとreflect経由では呼び出しができないためです。

こうしておくほうが若干パフォーマンスが良いです。

//go:build (go1.25 && goexperiment.jsonv2) || go1.26

package main

import (
    "encoding/json/jsontext"
    "encoding/json/v2"
    "fmt"
    "time"
)

type A struct {
    Foo string    `json:"foo,omitzero"`
    Bar int       `json:"int,omitzero"`
    T   time.Time `json:"t,omitzero,format:RFC3339"`
    U   string    `json:"',\"'"`
}

func main() {
    a := A{
        Foo: "foo",
        Bar: 123,
        T:   time.Now(),
        U:   "um",
    }

    bin, err := json.Marshal(&a, jsontext.WithIndent("    "))
    if err != nil {
        panic(err)
    }
    fmt.Println(string(bin))
    /*
       {
           "foo": "foo",
           "int": 123,
           "t": "2025-05-23T21:47:23+09:00",
           ",\"": "um"
       }
    */
    a = *new(A)
    err = json.Unmarshal(bin, &a)
    if err != nil {
        panic(err)
    }
    fmt.Printf("%#v\n", a)
    // main.A{Foo:"foo", Bar:123, T:time.Date(2025, time.May, 23, 21, 47, 23, 0, time.Local), U:"um"}
}

jsontext.Encoder

*jsontext.Encoder*json.Encoderに当たるもので、言葉通りJSONのTokenやValueをio.Writerに書き込むことができます。

WriteTokenWriteValueで値を書き込みますが、内部のステートマシンが状態を覚えているので:とか,とかを手動で書き込む必要はないです。これはいいデザインですね。

StackDepth,StackIndex,StackPointerjsontext.Decoderと共通なmethodでそれぞれ、

  • 現在のJSON ObjectやJSON Arrayのnest回数
  • i番目の階層の開始token: 0, {, [のどれか
  • JSON Pointer(RFC 6901)

が取得できます。

UnusedBufferencoderStateに紐づくバッファーが利用できるので、これを利用するとよいというAPIのようです。内部のコメントを見るとencoderStateのバッファーの未使用の部分をsliceで返すような実装をしていたけどやめたようなことがコメントで書かれています。見た限りずっとこのコメントが残されています。proposalになる時点でもこのmethod名が変わらなかったので実装されるときもこのままかもしれないですね。

sninnet

import (
    "bytes"
    "encoding/json/jsontext"
    "testing"
)

func TestEncoder(t *testing.T) {
    buf := new(bytes.Buffer)
    enc := jsontext.NewEncoder(buf, jsontext.WithIndent("    "))

    var err error
    bufErr := func(e error) {
        if err != nil {
            return
        }
        err = e
    }

    assertDepth := func(enc *jsontext.Encoder, depth int) {
        if enc.StackDepth() != depth {
            t.Errorf("wrong depth: expected = %d, actual = %d", depth, enc.StackDepth())
        }
    }

    assertDepth(enc, 0)
    bufErr(enc.WriteToken(jsontext.BeginObject))
    assertDepth(enc, 1)

    bufErr(enc.WriteToken(jsontext.String("foo")))
    bufErr(enc.WriteToken(jsontext.Null))
    bufErr(enc.WriteToken(jsontext.String("baz")))

    bufErr(enc.WriteToken(jsontext.BeginArray))
    assertDepth(enc, 2)

    bufErr(enc.WriteToken(jsontext.String("qux")))
    bufErr(enc.WriteToken(jsontext.Int(123)))
    bufErr(enc.WriteToken(jsontext.String("quux")))
    if enc.OutputOffset() == int64(buf.Len()) {
        t.Errorf("immediately flushed at %d", enc.OutputOffset())
    }
    v := enc.UnusedBuffer()
    v = append(v, []byte(`[`)...)
    v = append(v, []byte(`{"corge":null}`)...)
    v = append(v, []byte(`]`)...)
    assertDepth(enc, 2)
    bufErr(enc.WriteValue(v))
    assertDepth(enc, 2)

    t.Log(enc.StackIndex(0)) // encoder_test.go:52: <invalid jsontext.Kind: '\x00'> 1
    t.Log(enc.StackIndex(1)) // encoder_test.go:53: { 4
    t.Log(enc.StackIndex(2)) // encoder_test.go:54: [ 4

    bufErr(enc.WriteToken(jsontext.EndArray))
    assertDepth(enc, 1)
    bufErr(enc.WriteToken(jsontext.EndObject))
    assertDepth(enc, 0)

    if err != nil {
        panic(err)
    }
    expected := `{
    "foo": null,
    "baz": [
        "qux",
        123,
        "quux",
        [
            {
                "corge": null
            }
        ]
    ]
}
`
    if buf.String() != expected {
        t.Fatalf("not equal:\nexpected = %s\nactual  = %s", expected, buf.String())
    }
}

jsontext.Decoder

*jsontext.Decoder*json.Decoderに当たるもので、言葉通りJSON TokenやValueをio.Readerから読み込むことができます。

PeekKindで値を消費せずにjsontext.Kindを取得し、ReadToken, ReadValueで値を読み込みます。

StackDepth,StackIndex,StackPointerjsontext.Encoderと共通なmethodでそれぞれ、

  • 現在のJSON ObjectやJSON Arrayのnest回数
  • i番目の階層の開始token: 0, {, [のどれか
  • JSON Pointer(RFC 6901)

が取得できます。

snippet

import (
    "bytes"
    "encoding/json"
    "encoding/json/jsontext"
    "io"
    "strings"
    "testing"
)

func TestDecoder(t *testing.T) {
    const input = `{
    "foo": null,
    "baz": [
        "qux",
        123,
        "quux",
        [
            {
                "corge": null
            }
        ]
    ]
}
`
    dec := jsontext.NewDecoder(strings.NewReader(input))

    expected := []any{
        jsontext.BeginObject,
        jsontext.String("foo"),
        jsontext.Null,
        jsontext.String("baz"),
        "peek",
        jsontext.BeginArray,
        jsontext.String("qux"),
        jsontext.Int(123),
        "peek",
        jsontext.String("quux"),
        jsontext.Value(`[{"corge":null}]`),
        jsontext.EndArray,
        jsontext.EndObject,
    }

    for _, tokenOrValue := range expected {
        idxKind, valueLen := dec.StackIndex(dec.StackDepth())
        t.Logf("depth = %d, index kind = %s, len at index = %d, stack pointer = %q", dec.StackDepth(), idxKind, valueLen, dec.StackPointer())
        /*
           decoder_test.go:46: depth = 0, index kind = <invalid jsontext.Kind: '\x00'>, len at index = 0, stack pointer = ""
           decoder_test.go:46: depth = 1, index kind = {, len at index = 0, stack pointer = ""
           decoder_test.go:46: depth = 1, index kind = {, len at index = 1, stack pointer = "/foo"
           decoder_test.go:46: depth = 1, index kind = {, len at index = 2, stack pointer = "/foo"
           decoder_test.go:46: depth = 1, index kind = {, len at index = 3, stack pointer = "/baz"
           decoder_test.go:46: depth = 1, index kind = {, len at index = 3, stack pointer = "/baz"
           decoder_test.go:46: depth = 2, index kind = [, len at index = 0, stack pointer = "/baz"
           decoder_test.go:46: depth = 2, index kind = [, len at index = 1, stack pointer = "/baz/0"
           decoder_test.go:46: depth = 2, index kind = [, len at index = 2, stack pointer = "/baz/1"
           decoder_test.go:46: depth = 2, index kind = [, len at index = 2, stack pointer = "/baz/1"
           decoder_test.go:46: depth = 2, index kind = [, len at index = 3, stack pointer = "/baz/2"
           decoder_test.go:46: depth = 2, index kind = [, len at index = 4, stack pointer = "/baz/3"
           decoder_test.go:46: depth = 1, index kind = {, len at index = 4, stack pointer = "/baz"
        */
        switch x := tokenOrValue.(type) {
        case string:
            t.Logf("peek = %s", dec.PeekKind())
        /*
           decoder_test.go:62: peek = [
           decoder_test.go:62: peek = string
        */
        case jsontext.Token:
            tok, err := dec.ReadToken()
            if err != nil && err != io.EOF {
                panic(err)
            }
            if tok.Kind() != x.Kind() {
                t.Errorf("not equal: expected(%v) != actual(%v)", x, tok)
            }
            switch tok.Kind() {
            case 'n': // null
            case 'f': // false
            case 't': // true
            case '"', '0': // string literal, number literal
                if tok.String() != x.String() {
                    t.Errorf("not equal: expected(%s) != actual(%s)", x, tok)
                }
            case '{': // end object
            case '}': // end object
            case '[': // begin array
            case ']': // end array
            }
        case jsontext.Value:
            val, err := dec.ReadValue()
            if err != nil && err != io.EOF {
                panic(err)
            }

            if !bytes.Equal(mustCompact(val), mustCompact(x)) {
                t.Errorf("not equal: expected(%q) != actual(%q)", string(x), string(val))
            }
        }
    }
}

func mustCompact(v jsontext.Value) jsontext.Value {
    err := v.Compact()
    if err != nil {
        panic(err)
    }
    return v
}

jsontext.Decoder.StackPointer

jsontext.Deocder.StackPointerを活用すれば特定のJSON Pointerまで値を読み捨ててdecodeをするみたいなこともできます。

snippet

import (
    "bytes"
    "encoding/json/jsontext"
    "encoding/json/v2"
    "errors"
    "fmt"
    "io"
    "iter"
    "strconv"
    "strings"
    "testing"
)

var ErrNotFound = errors.New("not found")

func ReadJSONAt(dec *jsontext.Decoder, pointer jsontext.Pointer, read func(dec *jsontext.Decoder) error) (err error) {
    lastToken := pointer.LastToken()
    var idx int64 = -1
    if len(lastToken) > 0 && strings.TrimLeftFunc(lastToken, func(r rune) bool { return '0' <= r && r <= '9' }) == "" {
        idx, err = strconv.ParseInt(lastToken, 10, 64)
        if err == nil {
            pointer = pointer[:len(pointer)-len(lastToken)-1]
        } else {
            // I'm not really super sure this could happen.
            idx = -1
        }
    }

    currentDepth := 0
    for {
        _, err = dec.ReadToken()
        if errors.Is(err, io.EOF) {
            break
        }
        if err != nil {
            return err
        }
        p := dec.StackPointer()
        if pointer == p {
            if idx >= 0 {
                if dec.PeekKind() != '[' {
                    return ErrNotFound
                }
                // skip '['
                _, err = dec.ReadToken()
                if err != nil {
                    return err
                }
                for ; idx > 0; idx-- {
                    err := dec.SkipValue()
                    if err != nil {
                        return err
                    }
                }
            }
            if dec.PeekKind() == ']' {
                return ErrNotFound
            }
            return read(dec)
        }
        nextDepth := commonSegment(p, pointer)
        if nextDepth < currentDepth {
            // search depth should only increase
            break
        }
        currentDepth = nextDepth
    }
    return ErrNotFound
}

func commonSegment(target, pointer jsontext.Pointer) int {
    if pointer.Contains(target) {
        return strings.Count(string(target), "/") + 1
    }
    next, stop := iter.Pull(target.Tokens())
    defer stop()
    common := 0
    for p := range pointer.Tokens() {
        t, ok := next()
        if !ok {
            break
        }
        if t != p {
            break
        }
        common++
    }
    return common
}

func TestDecoder_Pointer(t *testing.T) {
    jsonBuf := []byte(`{"yay":"yay","nay":[{"boo":"boo"},{"bobo":"bobo"}],"foo":{"bar":{"baz":"baz"}}}`)

    type Boo struct {
        Boo string `json:"boo"`
    }
    type Bobo struct {
        Bobo string `json:"bobo"`
    }
    type Baz struct {
        Baz string `json:"baz"`
    }

    type testCase struct {
        pointer    jsontext.Pointer
        readTarget any
        expected   any
    }
    for _, tc := range []testCase{
        {"/foo/bar", Baz{}, Baz{"baz"}},
        {"/nay/0", Boo{}, Boo{"boo"}},
        {"/nay/1", Bobo{}, Bobo{"bobo"}},
        {"/yay/2", nil, nil},
        {"/foo/bar/baz/qux", nil, nil},
        {"/nay/2", nil, nil},
    } {
        t.Run(string(tc.pointer), func(t *testing.T) {
            err := ReadJSONAt(
                jsontext.NewDecoder(bytes.NewBuffer(jsonBuf)),
                tc.pointer,
                func(dec *jsontext.Decoder) error {
                    return json.UnmarshalDecode(dec, &tc.readTarget)
                },
            )
            if tc.readTarget == nil {
                if err != ErrNotFound {
                    t.Errorf("should be ErrNotFound, but is %q", err)
                }
                return
            }
            if err != nil && err != io.EOF {
                panic(err)
            }
            expected := fmt.Sprintf("%#v", tc.expected)
            read := fmt.Sprintf("%#v", tc.readTarget)
            if expected != read {
                t.Errorf("read not as expected: expected(%q) != actual(%q)", expected, read)
            }
        })
    }
}

実用しようと思うならたとえば/foo/bar/bazが与えられた時に/foo/barを読み終わって/foo/otherに移行したときに探索をやめるようなコードが必要ですが、例なので実装していません。

EDIT(2025-05-14): 実用レベルかはともかく要素が見つからないと確定したら早々にreturnするようにしました。

v2.MarshalerTo / v2.UnmarshalerFrom

何度もやってるundefined | null | Tを表現できる型をv2.MarshalerTo/v2.UnmarshalerFromで実装してみます。

snippet

import (
    "encoding/json/jsontext"
    "encoding/json/v2"
    "testing"
)

var (
    _ json.MarshalerTo     = Option[any]{}
    _ json.UnmarshalerFrom = (*Option[any])(nil)
    _ json.MarshalerTo     = Und[any]{}
    _ json.UnmarshalerFrom = (*Und[any])(nil)
)

type Option[V any] struct {
    some bool
    v    V
}

func None[V any]() Option[V] {
    return Option[V]{}
}

func Some[V any](v V) Option[V] {
    return Option[V]{some: true, v: v}
}

func (o Option[V]) IsZero() bool {
    return o.IsNone()
}

func (o Option[V]) IsNone() bool {
    return !o.some
}

func (o Option[V]) IsSome() bool {
    return o.some
}

func (o Option[V]) Value() V {
    return o.v
}

func (o Option[V]) MarshalJSONTo(enc *jsontext.Encoder) error {
    if o.IsNone() {
        return enc.WriteToken(jsontext.Null)
    }
    return json.MarshalEncode(enc, o.Value())
}

func (o *Option[V]) UnmarshalJSONFrom(dec *jsontext.Decoder) error {
    if k := dec.PeekKind(); k == 'n' {
        err := dec.SkipValue()
        if err != nil {
            return err
        }
        o.some = false
        o.v = *new(V)
        return nil
    }
    var v V
    err := json.UnmarshalDecode(dec, &v)
    if err != nil {
        return err
    }
    // preventing half-baked value left in-case of error in middle of decode
    // sacrificing performance
    o.some = true
    o.v = v
    return nil
}

type Und[V any] struct {
    opt Option[Option[V]]
}

func Undefined[V any]() Und[V] {
    return Und[V]{}
}

func Null[V any]() Und[V] {
    return Und[V]{opt: Some(None[V]())}
}

func Defined[V any](v V) Und[V] {
    return Und[V]{opt: Some(Some(v))}
}

func (u Und[V]) IsZero() bool {
    return u.IsUndefined()
}

func (u Und[V]) IsUndefined() bool {
    return u.opt.IsNone()
}

func (u Und[V]) IsNull() bool {
    return u.opt.IsSome() && u.opt.Value().IsNone()
}

func (u Und[V]) IsDefined() bool {
    return u.opt.IsSome() && u.opt.Value().IsSome()
}

func (u Und[V]) Value() V {
    return u.opt.Value().Value()
}

func (u Und[V]) MarshalJSONTo(enc *jsontext.Encoder) error {
    if !u.IsDefined() {
        return enc.WriteToken(jsontext.Null)
    }
    return json.MarshalEncode(enc, u.Value())
}

func (u *Und[V]) UnmarshalJSONFrom(dec *jsontext.Decoder) error {
    // should be with omitzero which handles absence of field.
    if k := dec.PeekKind(); k == 'n' {
        err := dec.SkipValue()
        if err != nil {
            return err
        }
        *u = Null[V]()
        return nil
    }
    var v V
    err := json.UnmarshalDecode(dec, &v)
    if err != nil {
        return err
    }
    *u = Defined(v)
    return nil
}

func TestArshalerToFrom(t *testing.T) {
    type sample struct {
        // null or string
        Foo Option[string]
        // undefined or string
        Bar Option[int] `json:",omitzero"`
        // undefined | null | bool
        Baz Und[bool] `json:",omitzero"`
    }

    type testCase struct {
        in        sample
        marshaled string
    }
    for _, tc := range []testCase{
        {sample{}, `{"Foo":null}`},
        {sample{Some(""), Some(0), Null[bool]()}, `{"Foo":"","Bar":0,"Baz":null}`},
        {sample{Some("foo"), Some(5), Defined(false)}, `{"Foo":"foo","Bar":5,"Baz":false}`},
        {sample{None[string](), None[int](), Defined(true)}, `{"Foo":null,"Baz":true}`},
    } {
        t.Run(tc.marshaled, func(t *testing.T) {
            bin, err := json.Marshal(tc.in)
            if err != nil {
                panic(err)
            }
            if string(bin) != tc.marshaled {
                t.Errorf("not equal: expected(%q) != actual(%q)", tc.marshaled, string(bin))
            }
            var unmarshaled sample
            err = json.Unmarshal(bin, &unmarshaled)
            if err != nil {
                panic(err)
            }
            if unmarshaled != tc.in {
                t.Errorf("not euql:\nexpected(%#v)\n!=\nactual(%#v)", tc.in, unmarshaled)
            }
        })
    }
}

json:",format:value"

json:",format:value"でフォーマットを指定できます。現状は組み込まれた特定のものしか指定できません。
ユーザー指定のformatを持てるようにしようというのは別proposalになっています。最初の実装時には入ってこないかもしれないです。

snippet

import (
    "encoding/json/v2"
    "testing"
    "time"
)

func TestArshalerFormat(t *testing.T) {
    type sample struct {
        Foo map[string]string `json:",format:emitempty"`
        Bar []byte            `json:",format:array"`
        Baz time.Duration     `json:",format:units"`
        Qux time.Time         `json:",format:'2006-01-02'"`
    }

    s := sample{
        Foo: nil,
        Bar: []byte(`bar`),
        Baz: time.Minute,
        Qux: time.Date(2025, 0o5, 12, 22, 23, 22, 123456789, time.UTC),
    }

    bin, err := json.Marshal(s)
    if err != nil {
        panic(err)
    }
    expected := `{"Foo":{},"Bar":[98,97,114],"Baz":"1m0s","Qux":"2025-05-12"}`
    if string(bin) != expected {
        t.Errorf("not equal:\n%s\n!=\n%s", expected, string(bin))
    }
}

json:",unknown"でフォールバック

json:",unknown"でstruct fieldなどで指定されていない(=不明な)フィールドをすべて格納できます。これが欲しかった。

json.DiscardUnknownMembers(true)で無視、json.RejectUnknownMembers(true)でエラーに挙動を変更できます。

snippet

import (
    "encoding/json/v2"
    "maps"
    "testing"
)

func TestTagUnknown(t *testing.T) {
    type sample struct {
        X   map[string]any `json:",unknown"`
        Foo string
        Bar int
        Baz bool
    }

    input := []byte(`{"Foo":"foo","Bar":12,"Baz":true,"Qux":"qux","Quux":"what!?"}`)
    var s sample
    err := json.Unmarshal(input, &s)
    if err != nil {
        panic(err)
    }
    expected := map[string]any{"Qux": "qux", "Quux": "what!?"}
    if !maps.Equal(s.X, expected) {
        t.Errorf("not equal:\n%#v\n!=\n%#v", expected, s.X)
    }

    err = json.Unmarshal(input, &s, json.RejectUnknownMembers(true))
    if err == nil {
        t.Error("should cause an error")
    } else {
        t.Logf("%v", err)
        // tag_unknown_test.go:32: json: cannot unmarshal JSON string into Go play.sample: unknown object member name "Qux"
    }
}

streaming decode

v1でやっていたようにstreaming deocdeも可能です。

snippet

import (
    "bytes"
    "encoding/json/jsontext"
    "encoding/json/v2"
    "io"
    "reflect"
    "testing"
)

const streamDecodeInput = `{
    "foo": null,
    "bar": {
            "baz": [
                {"foo":"foo1"},
                {"foo":"foo2"},
                {"foo":"foo3"}
            ]
        }
}
`

func TestStreamingDecode(t *testing.T) {
    dec := jsontext.NewDecoder(bytes.NewReader([]byte(streamDecodeInput)))
    for dec.StackPointer() != jsontext.Pointer("/bar/baz") {
        _, err := dec.ReadToken()
        if err != nil {
            if err != io.EOF {
                panic(err)
            }
            break
        }
    }

    if dec.PeekKind() != '[' {
        panic("not array")
    }
    // discard '['
    _, err := dec.ReadToken()
    if err != nil {
        panic(err)
    }
    type sample struct {
        Foo string `json:"foo"`
    }
    var decoded []sample
    for dec.PeekKind() != ']' {
        var s sample
        err := json.UnmarshalDecode(dec, &s)
        if err != nil {
            panic(err)
        }
        decoded = append(decoded, s)
    }
    expected := []sample{{"foo1"}, {"foo2"}, {"foo3"}}
    if !reflect.DeepEqual(expected, decoded) {
        t.Errorf("not equal:\nexpected(%#v)\n!=\nactual(%#v)", expected, decoded)
    } else {
        t.Logf("decoded = %#v", decoded)
        // streaming_decode_test.go:60: decoded = []play.sample{play.sample{Foo:"foo1"}, play.sample{Foo:"foo2"}, play.sample{Foo:"foo3"}}
    }
}

streaming deocde 2

v2.WithUnmarshalers(v2.UnmarshalFromFunc(func (...) {...}))で型ごとにunmarshalerを変更できます。
これを利用すればもっと簡単に(と言いつつコードはごちゃごちゃしますが)streaming decodeを行うことができます。

snippet

import (
    "bytes"
    "encoding/json/jsontext"
    "encoding/json/v2"
    "io"
    "reflect"
    "testing"
)

const streamDecodeInput = `{
    "foo": null,
    "bar": {
            "baz": [
                {"foo":"foo1"},
                {"foo":"foo2"},
                {"foo":"foo3"}
            ]
        }
}
`

func TestStreamingDecode2(t *testing.T) {
    type Data struct {
        Foo string `json:"foo"`
    }

    type Bar struct {
        Baz []Data `json:"baz"`
    }

    type sample struct {
        Foo *int `json:"foo"`
        Bar Bar  `json:"bar"`
    }

    dataChan := make(chan Data)
    unmarshaler := json.WithUnmarshalers(json.UnmarshalFromFunc(func(dec *jsontext.Decoder, d *Data) error {
        type plain Data
        var p plain
        err := json.UnmarshalDecode(dec, &p)
        if err == nil {
            dataChan <- Data(p)
        }
        *d = Data(p)
        return err
    }))

    resultCh := make(chan []Data)
    go func() {
        var result []Data
        for d := range dataChan {
            result = append(result, d)
        }
        resultCh <- result
    }()

    var s sample
    err := json.Unmarshal([]byte(streamDecodeInput), &s, unmarshaler)
    if err != nil {
        panic(err)
    }
    close(dataChan)
    result := <-resultCh

    expected := []Data{{"foo1"}, {"foo2"}, {"foo3"}}
    if !reflect.DeepEqual(expected, result) {
        t.Errorf("not equal:\nexpected(%#v)\n!=\nactual(%#v)", expected, result)
    } else {
        t.Logf("decoded = %#v", result)
        // streaming_decode_test.go:111: decoded = []play.Data{play.Data{Foo:"foo1"}, play.Data{Foo:"foo2"}, play.Data{Foo:"foo3"}}
    }
}

まだ足りてなさそうなところ

token列からのunmarshalは簡単じゃなさそう。

encoding/xmlにはxml.NewTokenDecoderがありますが、encoding/json/v2にはこういったtoken readerがないため効率的なtee-ingができないかもしれないです。

ということで下記のEither[L, R]を例に出します。jsonからunmarshalするとき、左で成功すれば左、だめなら右でunmarshal、どっちかで成功すればよいというものです。tokenのtee-ingができないと一旦JSON Valueをバッファーする必要があり、これはv1json.Unmarshalerと同様にパフォーマンスが悪そうに思えます。こちらはv2Optionsを伝搬できる違いがあり、実装が無意味というわけでもないです。

とはいえ下記のようにjsontext.Encoder/jsontext.Decoderがinterfaceになることはないでしょうから当面(もしくはずっと)token列からのunmarshalはできないと思われます。

  • Make Encoder and Decoder an interface: The json.MarshalerTo and json.UnmarshalerFrom interfaces reference a concrete jsontext.Encoder and jsontext.Decoder implementation, which prevents use of a customer encoder or decoder. We considered making these an interface, but the performance cost of constantly calling a virtual method was expensive when a vast majority of usages are for the standard implementation.

追記(2025-05-13): ちょっと考えるとteeingもできたので追記しました。

Value-buffer版

snippet

import (
    "encoding/json/jsontext"
    "encoding/json/v2"
    "fmt"
    "testing"
)

var (
    _ json.MarshalerTo     = Either[any, any]{}
    _ json.UnmarshalerFrom = (*Either[any, any])(nil)
)

// zero value is zero left.
type Either[L, R any] struct {
    isRight bool
    l       L
    r       R
}

func (e Either[L, R]) IsLeft() bool {
    return !e.isRight
}

func (e Either[L, R]) MarshalJSONTo(enc *jsontext.Encoder) error {
    if e.IsLeft() {
        return json.MarshalEncode(enc, e.Left())
    }
    return json.MarshalEncode(enc, e.Right())
}

func (e *Either[L, R]) UnmarshalJSONFrom(dec *jsontext.Decoder) error {
    val, err := dec.ReadValue()
    if err != nil {
        return err
    }

    var l L
    errL := json.Unmarshal(val, &l, dec.Options())
    if errL == nil {
        e.isRight = false
        e.l = l
        e.r = *new(R)
        return nil
    }

    var r R
    errR := json.Unmarshal(val, &r, dec.Options())
    if errR == nil {
        e.isRight = true
        e.l = *new(L)
        e.r = r
        return nil
    }

    return fmt.Errorf("Either[L, R]: unmarshal failed for both L and R: l = (%w), r = (%w)", errL, errR)
}

func TestArshalerEither(t *testing.T) {
    type testCase struct {
        in   string
        fail bool
    }
    for _, tc := range []testCase{
        {"\"foo\"", false},
        {"123", false},
        {"false", true},
    } {
        var e Either[string, int]
        err := json.Unmarshal([]byte(tc.in), &e)
        if (err != nil) != tc.fail {
            t.Errorf("incorrect!")
        }
        t.Logf("err = %v", err)
        /*
           arshaler_either_test.go:122: err = <nil>
           arshaler_either_test.go:122: err = <nil>
           arshaler_either_test.go:122: err = json: cannot unmarshal into Go play.Either[string,int]: Either[L, R]: unmarshal failed for both L and R: l = (json: cannot unmarshal JSON boolean into Go string), r = (json: cannot unmarshal JSON boolean into Go int)
        */
    }
}
残りのmethod
func Left[L, R any](l L) Either[L, R] {
    return Either[L, R]{isRight: false, l: l}
}

func Right[L, R any](r R) Either[L, R] {
    return Either[L, R]{isRight: true, r: r}
}


func (e Either[L, R]) IsRight() bool {
    return e.isRight
}

func (e Either[L, R]) Left() L {
    return e.l
}

func (e Either[L, R]) Right() R {
    return e.r
}

func (e Either[L, R]) Unpack() (L, R) {
    // for ? syntax discussed under https://github.com/golang/go/discussions/71460
    return e.l, e.r
}

func MapLeft[L, R, L2 any](e Either[L, R], mapper func(l L) L2) Either[L2, R] {
    if e.IsLeft() {
        return Left[L2, R](mapper(e.Left()))
    }
    return Right[L2](e.Right())
}

func (e Either[L, R]) MapLeft(mapper func(l L) L) Either[L, R] {
    return MapLeft(e, mapper)
}

func MapRight[L, R, R2 any](e Either[L, R], mapper func(l R) R2) Either[L, R2] {
    if e.IsRight() {
        return Right[L](mapper(e.Right()))
    }
    return Left[L, R2](e.Left())
}

func (e Either[L, R]) MapRight(mapper func(l R) R) Either[L, R] {
    return MapRight(e, mapper)
}

tee-ing版

すごく迂遠なことをすればtokenのtee-ingが実装できます。

  • PeekKindでJSON ObjectかJSON Arrayのときと、それ以外のときで分岐します。
    • null, false, true, ""(String literal), 0(Number literal)のいずれかである場合はどちらにせよValue単位の読み込みになるため、ReadValueを読んでunmarshalしておきます。
  • JSON ObjectかJSON Arrayのとき、まず初めにStackDepthをとっておきます。jsontext.Decoder.ReadTokenすると{[を読むことでdepthが増加するはずですので、元のdepthに戻ったときそののJSON ObjectかJSON Arrayが読み終わったとみなせます。
  • 二つio.Pipeを作り、自家版io.MultiWriterみたいなものでwriter側を結合して1つのio.Writerにし、*jsontext.Encoderを作成します。
  • *jsontext.Decoder.ReadTokenで読み込んだtokenをこのencoderに書き込みます。
    • ReadTokenReadValue,:の情報がドロップしてしまうため、これを完全に再建するためには*jsontext.Encoderの力が必要です。
  • io.MultiWriterをそのまま使えないのは、片方のv2.UnmarshalReadが早期にエラー終了したとき、こちらには書き込まなくてよくなるのでそれをpipeのwriter側に伝えたいが、これにCloseWithError以外のうまい方法がないためです。
  • あとはどこかのgoroutineがpanicした時に備えてrecoverしてre-panicできるように少し考慮を加えて完成です。

ものすごい大きなJSON Objectとかでない限りValue-buffer版のほうが効率いいと思うので微妙な気持ちになりますが、筆者が思いつけるのはここが限界です。
もっと賢い方法があったら教えてほしいです。

snippet

つまり以下のようにTeeDecoderを定義します。

v2.Unmarshalが終わると*jsontext.Decoderv2が持っているキャッシュプールに戻されるため、呼び出しが終わった後もdecoderを保持し続けるとrace conditionが生じます。
TeeDecoderは内部で作ったgoroutineが全部終了するようしっかりsyncをとる必要があります。

type ReadCloseStopper interface {
    io.ReadCloser
    Stop(successful bool) // when called with true, stop tee-ing of both side. Otherwise stops the calling side.
}

type bufReader struct {
    mu     sync.Mutex
    closed bool
    r      *bytes.Reader
}

func (r *bufReader) Read(p []byte) (int, error) {
    r.mu.Lock()
    defer r.mu.Unlock()

    if r.closed {
        return 0, io.EOF
    }
    return r.r.Read(p)
}

func (r *bufReader) Close() error {
    r.mu.Lock()
    defer r.mu.Unlock()
    r.closed = true
    return nil
}

func (r *bufReader) Stop(successful bool) {
    r.mu.Lock()
    defer r.mu.Unlock()
    r.closed = true
}

var (
    errStopped     = errors.New("stopped")
    errFailedEarly = errors.New("failed early")
)

type teeReader struct {
    mu     sync.Mutex
    closed bool
    r      *io.PipeReader
}

func (r *teeReader) Read(p []byte) (int, error) {
    r.mu.Lock()
    defer r.mu.Unlock()

    if r.closed {
        return 0, io.EOF
    }

    return r.r.Read(p)
}

func (r *teeReader) Close() error {
    r.mu.Lock()
    defer r.mu.Unlock()

    if r.closed {
        return nil
    }
    r.closed = true

    return r.r.Close()
}

func (r *teeReader) Stop(successful bool) {
    r.mu.Lock()
    defer r.mu.Unlock()

    if r.closed {
        return
    }
    r.closed = true

    err := errFailedEarly
    if successful {
        err = errStopped
    }
    r.r.CloseWithError(err)
}

type multiPipeWriter struct {
    maskedErr error
    wl, wr    *io.PipeWriter
}

func (w *multiPipeWriter) Write(b []byte) (n int, err error) {
    if w.wl != nil {
        var nl int
        nl, err = w.wl.Write(b)
        if err != nil {
            // failed write = the other side of pipe is closed with error or anything.
            w.wl = nil
            if !errors.Is(err, w.maskedErr) {
                w.wr.CloseWithError(err)
                w.wr = nil
                return
            }
        } else {
            n = nl
        }
        err = nil
    }

    if w.wr != nil {
        var nr int
        nr, err = w.wr.Write(b)
        if err != nil {
            w.wr = nil
            if !errors.Is(err, w.maskedErr) {
                w.wl.CloseWithError(err)
                w.wl = nil
                return
            }
        } else {
            n = nr
        }
        err = nil
    }
    if len(b) != n && err == nil {
        err = io.ErrClosedPipe
    }
    return
}

func (w *multiPipeWriter) CloseWithError(err error) error {
    if w.wl != nil {
        w.wl.CloseWithError(err)
        w.wl = nil
    }
    if w.wr != nil {
        w.wr.CloseWithError(err)
        w.wr = nil
    }
    return nil
}

var errBadDec = errors.New("bad decoder")

func TeeDecoder(dec *jsontext.Decoder, encOptions ...jsontext.Options) (l ReadCloseStopper, r ReadCloseStopper, wait func(), err error) {
    switch dec.PeekKind() {
    default:
        return nil, nil, func() {}, fmt.Errorf("%w: decoder peeked a non starting token %q", errBadDec, dec.PeekKind().String())
    case 'n', 'f', 't', '"', '0':
        val, err := dec.ReadValue()
        if err != nil {
            return nil, nil, func() {}, err
        }
        return &bufReader{r: bytes.NewReader(val)}, &bufReader{r: bytes.NewReader(val)}, func() {}, nil
    case '[', '{':
        prl, pwl := io.Pipe()
        prr, pwr := io.Pipe()

        var (
            wg       sync.WaitGroup
            panicVal any
        )
        wg.Add(1)
        go func() {
            defer wg.Done()
            var err error
            mw := &multiPipeWriter{errFailedEarly, pwl, pwr}
            defer func() {
                // it's possible that reading dec panicks
                if rec := recover(); rec != nil {
                    panicVal = rec
                    err = fmt.Errorf("panicked: %v", rec)
                }
                mw.CloseWithError(err)
            }()

            enc := jsontext.NewEncoder(mw, encOptions...)

            depth := dec.StackDepth()
            var tok jsontext.Token
            for {
                tok, err = dec.ReadToken()
                if err != nil {
                    return
                }

                err = enc.WriteToken(tok)
                if err != nil {
                    return
                }

                if dec.StackDepth() == depth {
                    break
                }
            }
        }()

        wait = func() {
            wg.Wait()
            if panicVal != nil {
                panic(panicVal)
            }
        }
        return &teeReader{r: prl}, &teeReader{r: prr}, wait, nil
    }
}

上記のTeeDecoderを用いるとUnmarshalJSONFromは下記のようになります。

func (e *Either[L, R]) UnmarshalJSONFrom(dec *jsontext.Decoder) error {
    eitherErr := func(errL, errR error) error {
        return fmt.Errorf("Either[L, R]: unmarshal failed for both L and R: l = (%w), r = (%w)", errL, errR)
    }
    switch dec.PeekKind() {
    case 'n', 'f', 't', '"', '0':
        val, err := dec.ReadValue()
        if err != nil {
            return err
        }

        var l L
        errL := json.Unmarshal(val, &l, dec.Options())
        if errL == nil {
            e.isRight = false
            e.l = l
            e.r = *new(R)
            return nil
        }

        var r R
        errR := json.Unmarshal(val, &r, dec.Options())
        if errR == nil {
            e.isRight = true
            e.l = *new(L)
            e.r = r
            return nil
        }

        return eitherErr(errL, errR)
    case '{', '[': // maybe deep and large
        var wg sync.WaitGroup
        defer wg.Wait() // in case of panic

        var err error

        rl, rr, wait, err := TeeDecoder(dec)
        if err != nil {
            return err
        }
        defer func() {
            rl.Stop(false)
            rr.Stop(false)
            wait()
        }()

        var (
            l          L
            r          R
            errL, errR error
            panicVal   any
        )

        wg.Add(1)
        go func() {
            defer func() {
                if rec := recover(); rec != nil {
                    panicVal = rec
                }
                rr.Stop(false)
                wg.Done()
            }()
            errR = json.UnmarshalRead(rr, &r, dec.Options())
        }()

        errL = json.UnmarshalRead(rl, &l, dec.Options())
        rl.Stop(errL == nil)

        wg.Wait()
        if panicVal != nil {
            panic(panicVal)
        }

        if errL == nil {
            e.isRight = false
            e.l = l
            e.r = *new(R)
            return nil
        }

        if errR == nil {
            e.isRight = true
            e.l = *new(L)
            e.r = r
            return nil
        }

        return eitherErr(errL, errR)
    default: // invalid, '}',    ']'
        // syntax error
        _, err := dec.ReadValue()
        return err
    }
}

下記のようにテストを定義して挙動を確かめます。
go test -count=100 -timeout=5s -race ./play/either_teeing/してみていますがPASSしているのできちんとsyncできているようです。

func TestArshalerEither(t *testing.T) {
    type testCase struct {
        in   string
        fail bool
    }
    for _, tc := range []testCase{
        {"\"foo\"", false},
        {"123", false},
        {"false", true},
        {"{\"foo\": false}", true},
    } {
        t.Run(tc.in, func(t *testing.T) {
            var e Either[string, int]
            err := json.Unmarshal([]byte(tc.in), &e)
            if (err != nil) != tc.fail {
                t.Errorf("incorrect!")
            }
            t.Logf("err = %v", err)
            /*
               === RUN   TestArshalerEither
               === RUN   TestArshalerEither/"foo"
                   arshaler_either_test.go:402: err = <nil>
               === RUN   TestArshalerEither/123
                   arshaler_either_test.go:402: err = <nil>
               === RUN   TestArshalerEither/false
                   arshaler_either_test.go:402: err = json: cannot unmarshal into Go play.Either[string,int]: Either[L, R]: unmarshal failed for both L and R: l = (json: cannot unmarshal JSON boolean into Go string), r = (json: cannot unmarshal JSON boolean into Go int)
               === RUN   TestArshalerEither/{"foo":_false}
                   arshaler_either_test.go:402: err = json: cannot unmarshal into Go play.Either[string,int] after offset 13: Either[L, R]: unmarshal failed for both L and R: l = (json: cannot unmarshal JSON object into Go string), r = (json: cannot unmarshal JSON object into Go int)
            */
        })
    }

    type sampleL struct {
        Foo []int
    }
    type sampleR struct {
        Bar map[string]string
    }
    for _, tc := range []testCase{
        {"\"foo\"", true},
        {"123", true},
        {"false", true},
        {"{\"foo\": false}", true},
        {"{\"Foo\": false}", true},
        {"{\"Foo\": [1,2,3]}", false},
        {"{\"Bar\": {\"foo\":\"foofoo\",\"bar\":\"barbar\"}}", false},
        {"{\"Foo\": [1,2,3}", true},     // syntax error
        {"{\"Bar\": {\"foo\":}}", true}, // syntax error
    } {
        t.Run(tc.in, func(t *testing.T) {
            var e Either[sampleL, sampleR]
            err := json.Unmarshal([]byte(tc.in), &e, json.RejectUnknownMembers(true))
            if (err != nil) != tc.fail {
                t.Errorf("incorrect!")
            }
            t.Logf("err = %v", err)
            /*
               === RUN   TestArshalerEither/"foo"#01
                   arshaler_either_test.go:440: err = json: cannot unmarshal into Go struct: Either[L, R]: unmarshal failed for both L and R: l = (json: cannot unmarshal JSON string into Go play.sampleL), r = (json: cannot unmarshal JSON string into Go play.sampleR)
               === RUN   TestArshalerEither/123#01
                   arshaler_either_test.go:440: err = json: cannot unmarshal into Go struct: Either[L, R]: unmarshal failed for both L and R: l = (json: cannot unmarshal JSON number into Go play.sampleL), r = (json: cannot unmarshal JSON number into Go play.sampleR)
               === RUN   TestArshalerEither/false#01
                   arshaler_either_test.go:440: err = json: cannot unmarshal into Go struct: Either[L, R]: unmarshal failed for both L and R: l = (json: cannot unmarshal JSON boolean into Go play.sampleL), r = (json: cannot unmarshal JSON boolean into Go play.sampleR)
               === RUN   TestArshalerEither/{"foo":_false}#01
                   arshaler_either_test.go:440: err = json: cannot unmarshal into Go struct after offset 13: Either[L, R]: unmarshal failed for both L and R: l = (json: cannot unmarshal JSON string into Go play.sampleL: unknown object member name "foo"), r = (json: cannot unmarshal JSON string into Go play.sampleR: unknown object member name "foo")
               === RUN   TestArshalerEither/{"Foo":_false}
                   arshaler_either_test.go:440: err = json: cannot unmarshal into Go struct after offset 13: Either[L, R]: unmarshal failed for both L and R: l = (json: cannot unmarshal JSON boolean into Go []int within "/Foo"), r = (json: cannot unmarshal JSON string into Go play.sampleR: unknown object member name "Foo")
               === RUN   TestArshalerEither/{"Foo":_[1,2,3]}
                   arshaler_either_test.go:440: err = <nil>
               === RUN   TestArshalerEither/{"Bar":_{"foo":"foofoo","bar":"barbar"}}
                   arshaler_either_test.go:440: err = <nil>
               === RUN   TestArshalerEither/{"Foo":_[1,2,3}
                   arshaler_either_test.go:440: err = json: cannot unmarshal into Go struct within "/Foo": Either[L, R]: unmarshal failed for both L and R: l = (jsontext: read error: jsontext: invalid character '}' after array element (expecting ',' or ']') within "/Foo" after offset 14), r = (json: cannot unmarshal JSON string into Go play.sampleR: unknown object member name "Foo")
               === RUN   TestArshalerEither/{"Bar":_{"foo":}}
                   arshaler_either_test.go:440: err = json: cannot unmarshal into Go struct within "/Bar/foo": Either[L, R]: unmarshal failed for both L and R: l = (jsontext: read error: jsontext: missing value after object name within "/Bar/foo" after offset 15), r = (jsontext: read error: jsontext: missing value after object name within "/Bar/foo" after offset 15)
            */
        })
    }
}

panicが伝搬できてるかもテストしておきましょう。

var panicVal any = "panicVal"

type panicReader struct {
    after io.Reader
    val   any
}

func (r *panicReader) Read(p []byte) (int, error) {
    n, err := r.after.Read(p)
    if err == io.EOF {
        panic(r.val)
    }
    return n, err
}

var _ json.UnmarshalerFrom = (*panicDecoder)(nil)

type panicDecoder struct{}

func (d *panicDecoder) UnmarshalJSONFrom(dec *jsontext.Decoder) error {
    panic(panicVal)
}

func TestArshalerEither_panic(t *testing.T) {
    t.Run("reader panics", func(t *testing.T) {
        defer func() {
            rec := recover()
            if rec != panicVal {
                t.Errorf("incorrect panic: want(%v) != got(%v)", panicVal, rec)
            }
        }()
        var e Either[map[int]any, map[string]any]
        json.UnmarshalRead(&panicReader{strings.NewReader(`{"foo":"foo",`), panicVal}, &e)
    })
    t.Run("left panics", func(t *testing.T) {
        defer func() {
            rec := recover()
            if rec != panicVal {
                t.Errorf("incorrect panic: want(%v) != got(%v)", panicVal, rec)
            }
        }()
        var e Either[panicDecoder, map[string]any]
        json.Unmarshal([]byte(`{"foo":"foo","bar":"bar"}`), &e)
    })
    t.Run("right panics", func(t *testing.T) {
        defer func() {
            rec := recover()
            if rec != panicVal {
                t.Errorf("incorrect panic: want(%v) != got(%v)", panicVal, rec)
            }
        }()
        var e Either[map[string]any, panicDecoder]
        json.Unmarshal([]byte(`{"foo":"foo","bar":"bar"}`), &e)
    })
}
前の

snippet

func (e *Either[L, R]) UnmarshalJSONFrom(dec *jsontext.Decoder) error {
    eitherErr := func(errL, errR error) error {
        return fmt.Errorf("Either[L, R]: unmarshal failed for both L and R: l = (%w), r = (%w)", errL, errR)
    }
    switch dec.PeekKind() {
    case 'n', 'f', 't', '"', '0':
        val, err := dec.ReadValue()
        if err != nil {
            return err
        }

        var l L
        errL := json.Unmarshal(val, &l, dec.Options())
        if errL == nil {
            e.isRight = false
            e.l = l
            e.r = *new(R)
            return nil
        }

        var r R
        errR := json.Unmarshal(val, &r, dec.Options())
        if errR == nil {
            e.isRight = true
            e.l = *new(L)
            e.r = r
            return nil
        }

        return eitherErr(errL, errR)
    case '{', '[': // maybe deep and large
        var wg sync.WaitGroup
        defer wg.Wait() // in case of panic

        prl, pwl := io.Pipe()
        prr, pwr := io.Pipe()
        defer func() {
            prl.Close()
            prr.Close()
        }()

        var (
            panicVal  any
            storeOnce sync.Once
        )
        recoverStoreOnce := func() {
            if rec := recover(); rec != nil {
                storeOnce.Do(func() {
                    panicVal = rec
                })
            }
        }

        errUnmarshalFailedEarly := errors.New("unmarshal failed early")
        errLeftSuccessful := errors.New("left successful")
        wg.Add(1)
        go func() {
            var err error
            defer func() {
                recoverStoreOnce()
                pwl.CloseWithError(err)
                pwr.CloseWithError(err)
                wg.Done()
            }()

            encl := jsontext.NewEncoder(pwl)
            encr := jsontext.NewEncoder(pwr)

            depth := dec.StackDepth()
            for {
                var tok jsontext.Token
                tok, err = dec.ReadToken()
                if err != nil {
                    return
                }

                errL := encl.WriteToken(tok)
                if errL != nil && !errors.Is(errL, errUnmarshalFailedEarly) {
                    err = errL
                    return
                }
                errR := encr.WriteToken(tok)
                if errR != nil && !errors.Is(errR, errUnmarshalFailedEarly) {
                    err = errR
                    return
                }
                if errL != nil && errR != nil {
                    err = errUnmarshalFailedEarly
                    return
                }

                if dec.StackDepth() == depth {
                    break
                }
            }
        }()

        var (
            l          L
            r          R
            errL, errR error
        )

        wg.Add(1)
        go func() {
            closed := false
            defer func() {
                recoverStoreOnce()
                if !closed {
                    prl.Close()
                }
                wg.Done()
            }()

            errL = json.UnmarshalRead(prl, &l, dec.Options())
            if errL != nil { // successful = tokens are fully consumed
                prl.CloseWithError(errUnmarshalFailedEarly)
            } else { // If decoding left succeeded, right is no longer needed.
                prr.CloseWithError(errLeftSuccessful)
            }
            closed = true
        }()

        errR = json.UnmarshalRead(prr, &r, dec.Options())
        if errR != nil && !errors.Is(errR, errLeftSuccessful) {
            prr.CloseWithError(errUnmarshalFailedEarly)
        }

        wg.Wait()
        if panicVal != nil {
            panic(panicVal)
        }

        if errL == nil {
            e.isRight = false
            e.l = l
            e.r = *new(R)
            return nil
        }

        if errR == nil {
            e.isRight = true
            e.l = *new(L)
            e.r = r
            return nil
        }

        return eitherErr(errL, errR)
    default: // invalid, '}',    ']'
        // syntax error
        _, err := dec.ReadValue()
        return err
    }
}
func TestArshalerEither(t *testing.T) {
    type testCase struct {
        in   string
        fail bool
    }
    for _, tc := range []testCase{
        {"\"foo\"", false},
        {"123", false},
        {"false", true},
        {"{\"foo\": false}", true},
    } {
        var e Either[string, int]
        err := json.Unmarshal([]byte(tc.in), &e)
        if (err != nil) != tc.fail {
            t.Errorf("incorrect!")
        }
        t.Logf("err = %v", err)
        /*
           arshaler_either_test.go:254: err = <nil>
           arshaler_either_test.go:254: err = <nil>
           arshaler_either_test.go:254: err = json: cannot unmarshal into Go play.Either[string,int]: Either[L, R]: unmarshal failed for both L and R: l = (json: cannot unmarshal JSON boolean into Go string), r = (json: cannot unmarshal JSON boolean into Go int)
           arshaler_either_test.go:254: err = json: cannot unmarshal into Go play.Either[string,int] after offset 13: Either[L, R]: unmarshal failed for both L and R: l = (json: cannot unmarshal JSON object into Go string), r = (json: cannot unmarshal JSON object into Go int)
        */
    }

    type sampleL struct {
        Foo []int
    }
    type sampleR struct {
        Bar map[string]string
    }
    for _, tc := range []testCase{
        {"\"foo\"", true},
        {"123", true},
        {"false", true},
        {"{\"foo\": false}", true},
        {"{\"Foo\": false}", true},
        {"{\"Foo\": [1,2,3]}", false},
        {"{\"Bar\": {\"foo\":\"foofoo\",\"bar\":\"barbar\"}}", false},
        {"{\"Foo\": [1,2,3}", true},     // syntax error
        {"{\"Bar\": {\"foo\":}}", true}, // syntax error
    } {
        t.Run(tc.in, func(t *testing.T) {
            var e Either[sampleL, sampleR]
            err := json.Unmarshal([]byte(tc.in), &e, json.RejectUnknownMembers(true))
            if (err != nil) != tc.fail {
                t.Errorf("incorrect!")
            }
            t.Logf("err = %v", err)
            /*
                === RUN   TestArshalerEither/"foo"
                    arshaler_either_test.go:286: err = json: cannot unmarshal into Go struct: Either[L, R]: unmarshal failed for both L and R: l = (json: cannot unmarshal JSON string into Go play.sampleL), r = (json: cannot unmarshal JSON string into Go play.sampleR)
                === RUN   TestArshalerEither/123
                    arshaler_either_test.go:286: err = json: cannot unmarshal into Go struct: Either[L, R]: unmarshal failed for both L and R: l = (json: cannot unmarshal JSON number into Go play.sampleL), r = (json: cannot unmarshal JSON number into Go play.sampleR)
                === RUN   TestArshalerEither/false
                    arshaler_either_test.go:286: err = json: cannot unmarshal into Go struct: Either[L, R]: unmarshal failed for both L and R: l = (json: cannot unmarshal JSON boolean into Go play.sampleL), r = (json: cannot unmarshal JSON boolean into Go play.sampleR)
                === RUN   TestArshalerEither/{"foo":_false}
                    arshaler_either_test.go:286: err = json: cannot unmarshal into Go struct after offset 13: Either[L, R]: unmarshal failed for both L and R: l = (json: cannot unmarshal JSON string into Go play.sampleL: unknown object member name "foo"), r = (json: cannot unmarshal JSON string into Go play.sampleR: unknown object member name "foo")
                === RUN   TestArshalerEither/{"Foo":_false}
                    arshaler_either_test.go:286: err = json: cannot unmarshal into Go struct after offset 13: Either[L, R]: unmarshal failed for both L and R: l = (json: cannot unmarshal JSON boolean into Go []int within "/Foo"), r = (json: cannot unmarshal JSON string into Go play.sampleR: unknown object member name "Foo")
                === RUN   TestArshalerEither/{"Foo":_[1,2,3]}
                    arshaler_either_test.go:286: err = <nil>
                === RUN   TestArshalerEither/{"Bar":_{"foo":"foofoo","bar":"barbar"}}
                    arshaler_either_test.go:286: err = <nil>
                === RUN   TestArshalerEither/{"Foo":_[1,2,3}
                    arshaler_either_test.go:286: err = json: cannot unmarshal into Go struct within "/Foo": Either[L, R]: unmarshal failed for both L and R: l = (jsontext: read error: jsontext: invalid character '}' after array element (expecting ',' or ']') within "/Foo" after offset 14), r = (json: cannot unmarshal JSON string into Go play.sampleR: unknown object member name "Foo")
                === RUN   TestArshalerEither/{"Bar":_{"foo":}}
                    arshaler_either_test.go:286: err = json: cannot unmarshal into Go struct within "/Bar/foo": Either[L, R]: unmarshal failed for both L and R: l = (jsontext: read error: jsontext: missing value after object name within "/Bar/foo" after offset 15), r = (jsontext: read error: jsontext: missing value after object name within "/Bar/foo" after offset 15)
            */
        })
    }
}

おわりに

多分使えるようになるのはGo 1.26からでしょうからあと1年とすこし耐えましょう。

GitHubで編集を提案

Discussion