📖

Go のリフレクションの練習のために JSON パーサーを作った

2025/03/25に公開

筆者は Go を書き始めて間もないですが、HTTP サーバーなどの実装に Go を選択することがあります。とあるライブラリを使っているときにオブジェクト構造を変換するユーティリティが欲しくなったのですが、任意のデータを Go の構造にパースする方法や Struct Tag の参照方法などを知らなかったためリフレクションに入門することにしました。今回は簡単な練習として JSON デコーダ・エンコーダを作成することにしました。

Go でリフレクションを使うためには reflect パッケージを使用します。
https://pkg.go.dev/reflect

成果物

成果物は以下の GitHub レポジトリにあります。
https://github.com/Tsukina-7mochi/go-json/

最終的に次のようにオブジェクトのパース・文字列化ができるようになりました。

type Address struct {
    Street string
    City   string
    State  string
    Zip    string `json:"zip_code"`
}

type User struct {
    ID        int `json:"id"`
    Name      string
    Email     string
  Addresses []Address
}

func main() {
    input := `{
        "id": 1,
        "name": "John Doe",
        "email": "johndoe@example.com",
        "addresses": [
            {
                "street": "123 Main St",
                "city": "Springfield",
                "state": "IL",
                "zip_code": "62701"
            },
            {
                "street": "456 Elm St",
                "city": "Springfield",
                "state": "IL",
                "zip_code": "62701"
            }
        ]
    }`

    // デコード
    var user User
    err := json.Decode(input, &user)
    if err != nil {
        panic(err)
    }

    // エンコード
    encoded, err := json.Encode(user)
    if err != nil {
        panic(err)
    }

    println(encoded)
}

実装方針

デコーダ

デコーダはトークナイザと (トークン列に対する) デコーダの 2 つの部品により実装しました。トークナイザ (Tokenizer) は []byte を持ち、Next() メソッドにより入力を消費してトークン (終端記号) を返します。デコーダは Tokenizer とパース先変数へのポインタを受け取る関数として実装し、Tokenizer.Next の返すトークンを消費して対象変数にパースします。
すなわち今回の実装では AST を経由せずトークン列を直接オブジェクトに展開しています。

エンコーダ

エンコーダはオブジェクトを直接文字列にエンコードするように実装しました。トークン列に変換するほうがより一般性のある実装になりますが練習のため簡単に実装できるようにしました。

デコーダの実装

トークナイザの実装

トークナイザの実装は本題から逸れるため折りたたんでおきます。

トークナイザの実装

トークンは Kind に型を、Value に実際の値を持つ構造体として定義しました。

type TokenKind string

const (
    EOFToken            = TokenKind("EOF")
    BeginArrayToken     = TokenKind("BeginArray")
    EndArrayToken       = TokenKind("EndArray")
    BeginObjectToken    = TokenKind("BeginObject")
    EndObjectToken      = TokenKind("EndObject")
    NameSeparatorToken  = TokenKind("NameSeparator")
    ValueSeparatorToken = TokenKind("ValueSeparator")
    NullToken           = TokenKind("Null")
    BooleanToken        = TokenKind("Boolean")
    NumberToken         = TokenKind("Number")
    StringToken         = TokenKind("String")
)

type Token struct {
    Kind  TokenKind
    Value interface{}
}

トークナイザは単純に読んでいる文字列の先頭から順にトークンとして解釈しています。

type Tokenizer struct {
    input []byte
    index int
}

func (t *Tokenizer) Next() (*Token, error) {
    t.skipWhitespace()

    if t.index >= len(t.input) {
        return &Token{Kind: EOFToken}, nil
    }

    switch t.input[t.index] {
    case '[':
        t.index += 1
        return &Token{Kind: BeginArrayToken}, nil
    case ']':
        t.index += 1
        return &Token{Kind: EndArrayToken}, nil
    case '{':
        t.index += 1
        return &Token{Kind: BeginObjectToken}, nil
    case '}':
        t.index += 1
        return &Token{Kind: EndObjectToken}, nil
    case ':':
        t.index += 1
        return &Token{Kind: NameSeparatorToken}, nil
    case ',':
        t.index += 1
        return &Token{Kind: ValueSeparatorToken}, nil
    }

    if t.matchesHeadingIdentifier([]byte("null")) {
        t.index += 4
        return &Token{Kind: NullToken}, nil
    }
    if t.matchesHeadingIdentifier([]byte("true")) {
        t.index += 4
        return &Token{Kind: BooleanToken, Value: true}, nil
    }
    if t.matchesHeadingIdentifier([]byte("false")) {
        t.index += 5
        return &Token{Kind: BooleanToken, Value: false}, nil
    }

    stringToken, err := t.takeString()
    if err != nil {
        return nil, err
    }
    if stringToken != nil {
        return stringToken, nil
    }

    numberToken := t.takeNumber()
    if numberToken != nil {
        return numberToken, nil
    }

    return nil, ErrUnexpectedCharacter
}

デコーダの実装

デコーダは Tokenizer とパース先のオブジェクトへのポインタを受け取る関数として実装します。オブジェクトの型は reflect.TypeOf() により reflect.Type として取得できます。これがポインタや Slice などの場合は reflect.Type.Elem() により参照先・アイテムの型を取得できます。今回の場合パース先の型は reflect.TypeOf(out).Elem() のようにして取得します。

func decode(t *tokenizer.Tokenizer, out any) error {
    // out が *T であるとする

    // out がポインタであることを確認
    outTypePtr := reflect.TypeOf(out)
    if outTypePtr.Kind() != reflect.Ptr {
        panic("out must be a pointer")
    }

    // outType は T
    outType := outTypePtr.Elem()

    // ...
}

outType によって分岐して型ごとにパースの処理を書いていきます。たとえば真偽値をパースする場合は単純にトークンから bool として値を取り出し out に格納します。文字列や数値の場合も同様です。

switch outType.Kind() {
case reflect.Bool:
    return decodeBoolean(t, out.(*bool))
}
func decodeBoolean(t *tokenizer.Tokenizer, out *bool) error {
    token, err := t.Next()
    if err != nil {
        return err
    }
    if token.Kind == tokenizer.BooleanToken {
        *out = token.BoolValue()
    } else {
        return ErrUnexpectedTokenType
    }
    return nil
}

配列・オブジェクトのパースは多少複雑になります。reflect.Net() を使ってアイテムの型の変数を作成し、その変数に対して再帰的に decode() を呼び出します。

配列の場合は reflect.Append() を使ってパースしたアイテムをスライスに追加し、reflect.Set() を使って out に格納し直します。

func decodeArray(t *tokenizer.Tokenizer, out any) error {
    // out が *[]T とする

    // out がポインタであることを確認
    outTypePtr := reflect.TypeOf(out)
    if outTypePtr.Kind() != reflect.Ptr {
        panic("out must be a pointer")
    }

    // outType, outValue は []T
    outType := outTypePtr.Elem()
    if outType.Kind() != reflect.Slice {
        panic("out must be a slice")
    }
    outValue := reflect.ValueOf(out).Elem()

    // itemType は T
    itemType := outType.Elem()

    // '[' があることを確認
    if _, err := expectToken(t, tokenizer.BeginArrayToken); err != nil {
        return err
    }

    for {
        // item は *T
        // 再帰的に decode を呼び出して item にパースする
        item := reflect.New(itemType).Interface()
        if err := decode(t, item); err != nil {
            return err
        }

        // *out = append(*out, *item) のようなことをやっている
        outValue.Set(reflect.Append(outValue, reflect.ValueOf(item).Elem()))

        // 次のトークンが ']' ならループを抜ける
        // ']' でも ',' でもなかったらエラー
        token, err := t.Next()
        if err != nil {
            return err
        }
        if token.Kind == tokenizer.EndArrayToken {
            break
        } else if token.Kind != tokenizer.ValueSeparatorToken {
            return ErrUnexpectedTokenType
        }
    }

    return nil
}

オブジェクトをパースする場合はさらにフィールドの名前を扱う必要があります。reflect.Type.NumField() でフィールドの数を知ることができ、実際のフィールドは reflect.Type.Field(i)reflect.StructureField としてアクセスすることができます。Structure Tag は reflect.StructureField.Tag.Get() により取得できます。
今回の実装では簡単のため JSON に存在するフィールドが Go の構造体に存在しない場合にはエラーとなるようにしています。逆に Go の構造体に存在するフィールドが JSON に存在しない場合は値は設定されません。

func decodeObject(t *tokenizer.Tokenizer, out any) error {
    // out がポインタであることを確認
    outTypePtr := reflect.TypeOf(out)
    if outTypePtr.Kind() != reflect.Ptr {
        panic("out must be a pointer")
    }
    outValue := reflect.ValueOf(out).Elem()

    // out が構造体のポインタであることを確認
    outType := outTypePtr.Elem()
    if outType.Kind() != reflect.Struct {
        panic("out must be a struct")
    }

    // 構造体タグを収集してタグに指定された名前→フィールド名の map を作る
    tagFieldNames := map[string]string{}
    numFields := outType.NumField()
    for i := 0; i < numFields; i++ {
        field := outType.Field(i)
        tag := field.Tag.Get("json")
        if tag != "" {
            tagFieldNames[tag] = field.Name
        }
    }

    // '{' があることを確認
    if _, err := expectToken(t, tokenizer.BeginObjectToken); err != nil {
        return err
    }

    for {
        // フィールド名 (文字列トークン) を期待
        nameToken, err := expectToken(t, tokenizer.StringToken)
        if err != nil {
            return err
        }

        // フィールド名が Structure Tag に指定されていたらその値を使い、
        // 指定されていない場合フィールド名を Upper Camel Case にして使う
        name := nameToken.StringValue()
        fieldName := tagFieldNames[name]
        if fieldName == "" {
            fieldName = SnakeCaseToUpperCamelCase(name)
        }

        // ':' があることを確認
        if _, err := expectToken(t, tokenizer.NameSeparatorToken); err != nil {
            return err
        }

        // fieldName の名前のフィールドを探し、その値に対して decode を再帰的に呼び出す
        field := outValue.FieldByName(fieldName)
        if !field.IsValid() {
            return ErrUnexpectedTargetType
        }
        if err = decode(t, field.Addr().Interface()); err != nil {
            return err
        }

        // 次のトークンが '}' ならループを抜ける
        // '}' でも ',' でもなかったらエラー
        token, err := t.Next()
        if err != nil {
            return err
        }
        if token.Kind == tokenizer.EndObjectToken {
            break
        } else if token.Kind != tokenizer.ValueSeparatorToken {
            return ErrUnexpectedTokenType
        }
    }

    return nil
}

あとは []byte を入力とするメソッドを作成して完成です。

func Decode(input []byte, out any) error {
    t := tokenizer.NewTokenizer(input)
    return decode(t, out)
}

エンコーダの作成

エンコーダはデコーダに比べてかなりシンプルです。reflect.TypeOf で取得した方に応じて分岐し、内容をそのまま strings.Builder に突っ込んでいます。配列・構造体のエンコードでは各アイテムに対して再帰的に encode() を呼び出しています。構造体のエンコードでは Decoder の際と同様にリフレクションを用いてフィールド上をイテレートすることで文字列化しています。

func encode(v any, sb *strings.Builder) error {
    if v == nil {
        sb.WriteString("null")
        return nil
    }

    tv := reflect.TypeOf(v)
    rv := reflect.ValueOf(v)

    switch tv.Kind() {
    case reflect.Bool:
        sb.WriteString(strconv.FormatBool(rv.Bool()))

    // 文字列, 数値に対しても同様

    case reflect.Array, reflect.Slice:
        sb.WriteByte('[')
        for i := 0; i < rv.Len(); i++ {
            if i > 0 {
                sb.WriteByte(',')
            }
            if err := encode(rv.Index(i).Interface(), sb); err != nil {
                return err
            }
        }
        sb.WriteByte(']')

    case reflect.Struct:
        numFields := rv.NumField()
        sb.WriteByte('{')
        for i := 0; i < numFields; i++ {
            if i > 0 {
                sb.WriteByte(',')
            }
            field := rv.Field(i)
            fieldName := rv.Type().Field(i).Tag.Get("json")
            if fieldName == "" {
                fieldName = CamelCaseToSnakeCase(rv.Type().Field(i).Name)
            }

            sb.WriteByte('"')
            sb.Write(EscapeString([]byte(fieldName)))
            sb.WriteByte('"')
            sb.WriteByte(':')

            if err := encode(field.Interface(), sb); err != nil {
                return err
            }
        }
        sb.WriteByte('}')
    }

    return nil
}

参考記事

https://datatracker.ietf.org/doc/html/rfc8259
https://go.dev/blog/laws-of-reflection
https://qiita.com/s9i/items/b835634d84bba5574d0a
(2025/3/25 時点)

Discussion