Golden Road

好きだという熱こそが最低限で最高の希望

【Go】structをJSONにする時、ゼロ値を含む/含まないを切り替える方法と注意点 #golang

inabaです。

Go言語でstructをJSONにする時、ゼロ値を含まない場合はタグにomitemptyをつけますよね。

type stampCard struct {
    Stamp int `json:"stamp,omitempty"`
}

しかし、同じstructを使っている時にゼロ値を含む/含まないを切り替えたい時があります。
具体的には下記のようなstructです。

type stampCard struct {
    InStampRally bool `json:"in_stamp_rally"`
    Stamp        int  `json:"stamp,omitempty"`
}

上記のstructではInStampRallyがtrueの時だけStampの値をJSONに含めたいです。
しかし、このstructのままだとInStampRallyがtrueでもStampが0なら含まれません。

package main

import (
    "encoding/json"
    "fmt"

    log "github.com/Sirupsen/logrus"
)

type stampCard struct {
    InStampRally bool `json:"in_stamp_rally"`
    Stamp        int  `json:"stamp,omitempty"`
}

func main() {
    sc := stampCard{InStampRally: true, Stamp: 0}
    jsc, err := json.Marshal(pc)
    if err != nil {
        log.Error(err)
    }

    fmt.Println(string(jsc)) // {"in_stamp_rally":true}
}

解決策

解決策はゼロ値を含む/含まないを切り替えたい値をポインタ化します。

type stampCard struct {
    InStampRally bool `json:"in_stamp_rally"`
    Stamp        *int `json:"stamp,omitempty"`
}

これで

  • nilでないならJSONに値を含める
  • nilならJSONに値を含めない

という切り替えができます。

package main

import (
    "encoding/json"
    "fmt"

    log "github.com/Sirupsen/logrus"
)

type stampCard struct {
    InStampRally bool `json:"in_stamp_rally"`
    Stamp        *int `json:"stamp,omitempty"`
}

func main() {
    scs := []stampCard{
        {InStampRally: false},
        {InStampRally: true},
    }

    for i, sc := range scs {
        if sc.InStampRally {
            scs[i].Stamp = intPointer(0)
        }
    }

    jscs, err := json.Marshal(scs)
    if err != nil {
        log.Error(err)
    }

    fmt.Println(string(jscs)) // [{"in_stamp_rally":false},{"in_stamp_rally":true,"stamp":0}]
}

func intPointer(v int) *int {
    return &v
}

上記サンプルコードではstampCardのスライスをループさせてInStampRallyがtrueなら値が0のintポインターStampに代入しています。
こうすることで下記のようにomitemptyが指定されていてもゼロ値をJSONに出力することができます。

[
  {
    "in_stamp_rally": false
  },
  {
    "in_stamp_rally": true,
    "stamp": 0
  }
]

注意点

rangeループのvalueからポインターを取ると、ループでvalueの値が変わってもポインターの値は同じなので、注意しないと意図しない値が入ってしまいます。
(ポインターの値が同じ理由は、ループのたびに新しいメモリを確保しないためでしょう。)

DBから取得した値をループさせてJSON化する時等、注意が必要です。

例として、下記は順番に0, 1, 2, 3のスタンプ数のスタンプカードができる事を期待しているコードです。

package main

import (
    "encoding/json"
    "fmt"

    log "github.com/Sirupsen/logrus"
)

type stampCard struct {
    InStampRally bool `json:"in_stamp_rally"`
    Stamp        *int `json:"stamp,omitempty"`
}

func main() {
    stamps := []int{0, 1, 2, 3}

    var scs []stampCard
    for _, stamp := range stamps {
        log.Infof("%p", &stamp)
        sc := stampCard{InStampRally: true, Stamp: &stamp}
        scs = append(scs, sc)
    }

    jscs, err := json.Marshal(scs)
    if err != nil {
        log.Error(err)
    }

    fmt.Println(string(jscs))
}

しかし、上記の結果は下記になります。

$ go run main.go | jq
INFO[0000] 0xc42000a3d0
INFO[0000] 0xc42000a3d0
INFO[0000] 0xc42000a3d0
INFO[0000] 0xc42000a3d0
[
  {
    "in_stamp_rally": true,
    "stamp": 3
  },
  {
    "in_stamp_rally": true,
    "stamp": 3
  },
  {
    "in_stamp_rally": true,
    "stamp": 3
  },
  {
    "in_stamp_rally": true,
    "stamp": 3
  }
]

まず、ループ中のvalueのポインター値を表示していますが、すべて同じになっています。
その後のJSONは全てのstamp3になってしまいました。
全ての値が最後の値に書き換わってしまっています。

修正後のコード

ループのvalueを一旦新しい変数に入れてその変数のポインターを取れば正確な値をJSONに出力できます。

package main

import (
    "encoding/json"
    "fmt"

    log "github.com/Sirupsen/logrus"
)

type stampCard struct {
    InStampRally bool `json:"in_stamp_rally"`
    Stamp        *int `json:"stamp,omitempty"`
}

func main() {
    stamps := []int{0, 1, 2, 3}

    var scs []stampCard
    for _, stamp := range stamps {
        tmp := stamp // important!!
        sc := stampCard{InStampRally: true, Stamp: &tmp}
        scs = append(scs, sc)
    }

    jscs, err := json.Marshal(scs)
    if err != nil {
        log.Error(err)
    }

    fmt.Println(string(jscs))
}

結果:

[
  {
    "in_stamp_rally": true,
    "stamp": 0
  },
  {
    "in_stamp_rally": true,
    "stamp": 1
  },
  {
    "in_stamp_rally": true,
    "stamp": 2
  },
  {
    "in_stamp_rally": true,
    "stamp": 3
  }
]

今日はここまで。