1
1

More than 3 years have passed since last update.

Goのencoding/json.Decoderが構造体フィールドにjson:"-"タグがついていたら無視する処理を探してみた

Posted at

コンテキスト

  • Goの構造体のフィールドには"tag_name:tag_value"というようにタグをつけることができる。
  • jsonのエンコードやデコードで使われるjsonタグというものがある。
  • json.Marshalで構造体->jsonのエンコードする場合に、構造体のフィールドのjsonタグに紐づいた文字列がjsonにおける文字列となる。
  • jsonタグに"-"という値を紐付けると、jsonには出力されないフィールドとなる。
type Struct struct {
    Name        string `json:"name"`
    Age         int
    Credential  string  `json:"-"`
}

上の構造体は以下のようなjsonに変換されてエンコードされる。

{"name": "Tomori Yu", "Age": 21}

疑問

  • エンコードに関する挙動は以上に見た通りだが、デコードの挙動はどうなっているのだろう。

試してみる

package main

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

type person struct {
    Name       string `json:"name"`
    Age        int
    Credential string `json:"-"`
}

func main() {
    p1 := person{"yu", 21, "AKIi9854u4t8394j8gf"}
    bs1, _ := json.Marshal(p1)
    fmt.Printf("marshaled person: %v\n", string(bs1))
    // -> marshaled person: {"name":"yu","Age":21}

    var p2 person
    _ = json.Unmarshal(bs, &p2)
    fmt.Printf("unmarshaled person: %v\n", p2)
    // -> unmarshaled person: {yu 21 }

    bs2 := []byte(`{"name": "yu", "Age": 21, "Credential": "AKIi9854u4t8394j8gf"}`)
    decoder := json.NewDecoder(bytes.NewBuffer(bs2))
    var p3 person
    _ = decoder.Decode(&p3)
    fmt.Printf("decoded person: %v\n", p3)
    // -> decoded person: {yu 21 }
}
  • json.MarshalではAgeがアウトプットに残るが、json.Unmarshal&json.DecodeではAgeが残らない。
  • 構造体->jsonの場合と同じように、json->構造体のデコードにおいても構造体のフィールドにjson:"-"というタグが付いていれば無視される。

json.Decoderに関して構造体のフィールドにjson:"-"というタグが付いていたら無視する処理を探してみた

encoding/json.Decoder

decoder := json.NewDecoder(buffer)

json.NewDecoderはDecoder構造体を返す。

Decoder

type Decoder struct {
    r       io.Reader
    buf     []byte
    d       decodeState
    scanp   int   // start of unread data in buf
    scanned int64 // amount of data already scanned
    scan    scanner
    err     error

    tokenState int
    tokenStack []int
}

Decoder構造体のポインタ型に対するDecodeメソッドは引数として渡した変数にデコードした結果を返す。
Decoder.Decode

func (dec *Decoder) Decode(v interface{}) error {
    if dec.err != nil {
        return dec.err
    }

    if err := dec.tokenPrepareForDecode(); err != nil {
        return err
    }

    if !dec.tokenValueAllowed() {
        return &SyntaxError{msg: "not at beginning of value", Offset: dec.InputOffset()}
    }

    // Read whole value into buffer.
    n, err := dec.readValue()
    if err != nil {
        return err
    }
    dec.d.init(dec.buf[dec.scanp : dec.scanp+n])
    dec.scanp += n

    // Don't save err from unmarshal into dec.err:
    // the connection is still usable since we read a complete JSON
    // object from it before the error happened.
    err = dec.d.unmarshal(v)

    // fixup token streaming state
    dec.tokenValueEnd()

    return err
}

dec.dは、Decoder構造体のフィールドであり、decodeState構造体のインスタンスである。
dec.d.initでdecodeState.dataにまだ読まれてない[]byteを初期化している。
そして、err = dec.d.unmarshal(v)に、デコードの処理がありそうだ。
decodeState

type decodeState struct {
    data         []byte
    off          int // next read offset in data
    opcode       int // last read result
    scan         scanner
    errorContext struct { // provides context for type errors
        Struct     reflect.Type
        FieldStack []string
    }
    savedError            error
    useNumber             bool
    disallowUnknownFields bool
}

decodeState.unmarshal

func (d *decodeState) unmarshal(v interface{}) error {
    rv := reflect.ValueOf(v)
    if rv.Kind() != reflect.Ptr || rv.IsNil() {
        return &InvalidUnmarshalError{reflect.TypeOf(v)}
    }

    d.scan.reset()
    d.scanWhile(scanSkipSpace)
    // We decode rv not rv.Elem because the Unmarshaler interface
    // test must be applied at the top level of the value.
    err := d.value(rv)
    if err != nil {
        return d.addErrorContext(err)
    }
    return d.savedError
}

d.scanは、decodeState構造体のscanフィールドであり、scanner構造体のフィールドである。
scannerはjsonをパースする処理におけるステートマシンとして利用される。
d.scan.reset()でdecodeStateのフィールドであるscannerインスタンスが初期化される。
d.value(rv)でデコード処理が行われていそうだ。
scanner

// A scanner is a JSON scanning state machine.
// Callers call scan.reset and then pass bytes in one at a time
// by calling scan.step(&scan, c) for each byte.
// The return value, referred to as an opcode, tells the
// caller about significant parsing events like beginning
// and ending literals, objects, and arrays, so that the
// caller can follow along if it wishes.
// The return value scanEnd indicates that a single top-level
// JSON value has been completed, *before* the byte that
// just got passed in.  (The indication must be delayed in order
// to recognize the end of numbers: is 123 a whole value or
// the beginning of 12345e+6?).
type scanner struct {
    // The step is a func to be called to execute the next transition.
    // Also tried using an integer constant and a single func
    // with a switch, but using the func directly was 10% faster
    // on a 64-bit Mac Mini, and it's nicer to read.
    step func(*scanner, byte) int

    // Reached end of top-level value.
    endTop bool

    // Stack of what we're in the middle of - array values, object keys, object values.
    parseState []int

    // Error that happened, if any.
    err error

    // total bytes consumed, updated by decoder.Decode (and deliberately
    // not set to zero by scan.reset)
    bytes int64
}

decodeState.value

// value consumes a JSON value from d.data[d.off-1:], decoding into v, and
// reads the following byte ahead. If v is invalid, the value is discarded.
// The first byte of the value has been read already.
func (d *decodeState) value(v reflect.Value) error {
    switch d.opcode {
    default:
        panic(phasePanicMsg)

    case scanBeginArray:
        if v.IsValid() {
            if err := d.array(v); err != nil {
                return err
            }
        } else {
            d.skip()
        }
        d.scanNext()

    case scanBeginObject:
        if v.IsValid() {
            if err := d.object(v); err != nil {
                return err
            }
        } else {
            d.skip()
        }
        d.scanNext()

    case scanBeginLiteral:
        // All bytes inside literal return scanContinue op code.
        start := d.readIndex()
        d.rescanLiteral()

        if v.IsValid() {
            if err := d.literalStore(d.data[start:d.readIndex()], v, false); err != nil {
                return err
            }
        }
    }
    return nil
}

v.IsValid()はvがゼロ値でなければtrueを返します。
構造体を引数に渡した場合は、d.object(v)が呼ばれるっぽいです。

decodeState.object()を読むと、switch文で引数のunderlyingな型がStructな時の処理が書かれてあります。

    case reflect.Struct:
        fields = cachedTypeFields(t)
        // ok

fieldsはstructFields構造体のインスタンスです。
structFields

type structFields struct {
    list      []field
    nameIndex map[string]int
}

cachedTypeFields関数は、typeFields関数にキャッシュ機能を足した薄いラッパーとなっています。
typeFieldsは以下のようになっています。
https://github.com/golang/go/blob/master/src/encoding/json/encode.go#L1209-L1391

// typeFields returns a list of fields that JSON should recognize for the given type.
// The algorithm is breadth-first search over the set of structs to include - the top struct
// and then any reachable anonymous structs.
func typeFields(t reflect.Type) structFields {
    // Anonymous fields to explore at the current level and the next.
    current := []field{}
    next := []field{{typ: t}}
[省略]
                tag := sf.Tag.Get("json")
                if tag == "-" {
                    continue
                }
[省略]

ありました。sfは構造体のフィールドです。
forの中でcontinueされなかったフィールドはforブロックの後の処理でfields = append(fields, field)という感じでフィールドとして登録されます。
構造体のフィールドのうちjsonタグに"-"が付いているものはcontinueされているので、無視することになっているみたいです。

先ほど見たdecodeState.object()ではfields = cachedTypeFields(t)という感じで構造体の有効なフィールドだけが処理されることになります。
その後にfields変数に関して、以下の処理が書かれています。
https://github.com/golang/go/blob/master/src/encoding/json/decode.go#L696-L709

            if i, ok := fields.nameIndex[string(key)]; ok {
                // Found an exact name match.
                f = &fields.list[i]
            } else {
                // Fall back to the expensive case-insensitive
                // linear search.
                for i := range fields.list {
                    ff := &fields.list[i]
                    if ff.equalFold(ff.nameBytes, key) {
                        f = ff
                        break
                    }
                }
            }

jsonのkeyにマッチする有効なStructのフィールドが見つかればfという変数に入れられて後の処理に続くようです。

まとめ

  • json.Decodeもjson:"-"タグのついた構造体のフィールドは無視する。
1
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
1