GolangでそこそこのJSONパーサを楽に自作する

先日、json2goというツールを作ったが、構造体のフィールドの順番がJSONと同じにならないのがいやで、なんとかならないかと調べてみた。 ObjectのanyへのUnmarshalがmap[string]anyに決め打ちされているのが原因で、ルートのObjectの型をなんとかすることはできても不定型なObjectの末端の子要素まで型を変えるのは難しそうだった。

そうなるとJSONのパーサを書くしかなさそうで「runeでデータを取り出さないと」「lexer書くのめんどくさい」「Unicodeエスケープシーケンスどうしよう」などと考えていたが、json.DecoderToken()というメソッドがあってJSONトークンを順次返してくれるので、これをlexerとしてパーサにトークンを渡せば、そこそこの品質のJSONパーサが楽にかけそうだったので 書いてみた。

github.com

パーサのライブラリには participle を使っている。

パーサ部分だけ取り出すとこんな感じ。

// lexer.go
package parser

import (
    "bytes"
    "encoding/json"
    "fmt"
    "io"

    "github.com/alecthomas/participle/v2/lexer"
)

const (
    TokenTypeDelim  lexer.TokenType = iota // '[',']','{','}'
    TokenTypeFalse                         // false
    TokenTypeNull                          // null
    TokenTypeTrue                          // true
    TokenTypeNumber                        // number
    TokenTypeString                        // string
)

var jsonSymbols = map[string]lexer.TokenType{
    "[":      TokenTypeDelim,
    "]":      TokenTypeDelim,
    "{":      TokenTypeDelim,
    "}":      TokenTypeDelim,
    "false":  TokenTypeFalse,
    "null":   TokenTypeNull,
    "true":   TokenTypeTrue,
    "number": TokenTypeNumber,
    "string": TokenTypeString,
}

type JsonDefinition struct {
}

func (l *JsonDefinition) Symbols() map[string]lexer.TokenType {
    return jsonSymbols
}

func (l *JsonDefinition) Lex(filename string, r io.Reader) (lexer.Lexer, error) {
    buf := &bytes.Buffer{}
    decoder := json.NewDecoder(io.TeeReader(r, buf))
    decoder.UseNumber()

    lex := &JsonLexer{
        decoder: decoder,
        buf:     buf,
        pos: lexer.Position{
            Filename: filename,
            Line:     1,
            Column:   1,
        },
    }

    return lex, nil
}

type JsonLexer struct {
    decoder *json.Decoder
    buf     *bytes.Buffer
    pos     lexer.Position
}

func (l *JsonLexer) Next() (lexer.Token, error) {
    startOffset := l.decoder.InputOffset()
    rawTok, err := l.decoder.Token()
    span := make([]byte, l.decoder.InputOffset()-startOffset)
    tok := lexer.Token{}

    if _, err := l.buf.Read(span); err != nil {
        return tok, err
    }

    tok.Pos = l.pos
    l.pos.Advance(string(span))

    if err == io.EOF {
        tok.Type = lexer.EOF
        return tok, nil
    } else if err != nil {
        return tok, fmt.Errorf("%d:%d: %w", tok.Pos.Line, tok.Pos.Column, err)
    }

    switch v := rawTok.(type) {
    case json.Delim:
        tok.Type = TokenTypeDelim
        tok.Value = v.String()
    case bool:
        if v {
            tok.Type = TokenTypeTrue
            tok.Value = "true"
        } else {
            tok.Type = TokenTypeFalse
            tok.Value = "false"
        }
    case nil:
        tok.Type = TokenTypeNull
        tok.Value = "null"
    case json.Number:
        tok.Type = TokenTypeNumber
        tok.Value = v.String()
    case string:
        tok.Type = TokenTypeString
        tok.Value = v
    }

    return tok, nil
}
// parser.go
package parser

import "github.com/alecthomas/participle/v2"

var (
    jsonParser = participle.MustBuild[JsonValue](
        participle.Lexer(&JsonDefinition{}),
    )
)

type JsonValue struct {
    False  *string     `parser:"@false |"`
    Null   *string     `parser:"@null |"`
    True   *string     `parser:"@true |"`
    Object *JsonObject `parser:"@@ |"`
    Array  *JsonArray  `parser:"@@ |"`
    Number *string     `parser:"@number |"`
    String *string     `parser:"@string"`
}

type JsonObject struct {
    Members []*JsonObjectMember `parser:"'{' @@* '}'"`
}

type JsonObjectMember struct {
    Key   string     `parser:"@string"`
    Value *JsonValue `parser:"@@"`
}

type JsonArray struct {
    Elements []*JsonValue `parser:"'[' @@* ']'"`
}

func ParseJSON(filename string, src []byte) (*JsonValue, error) {
    v, err := jsonParser.ParseBytes(filename, src)

    if err != nil {
        return nil, err
    }
    return v, nil
}

参考: Go言語のorderedmapパッケージを改善した - プログラムモグモグ

追記

パーサだけ別ライブラリに切り出した。

github.com