先日、json2goというツールを作ったが、構造体のフィールドの順番がJSONと同じにならないのがいやで、なんとかならないかと調べてみた。 ObjectのanyへのUnmarshalがmap[string]anyに決め打ちされているのが原因で、ルートのObjectの型をなんとかすることはできても不定型なObjectの末端の子要素まで型を変えるのは難しそうだった。
そうなるとJSONのパーサを書くしかなさそうで「runeでデータを取り出さないと」「lexer書くのめんどくさい」「Unicodeエスケープシーケンスどうしよう」などと考えていたが、json.DecoderにToken()というメソッドがあってJSONのトークンを順次返してくれるので、これをlexerとしてパーサにトークンを渡せば、そこそこの品質のJSONパーサが楽にかけそうだったので 書いてみた。
パーサのライブラリには 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パッケージを改善した - プログラムモグモグ
追記
パーサだけ別ライブラリに切り出した。