A Day In The Life

とあるプログラマの備忘録

Time型をJSONに変換するとtime.Locationの情報が欠落する場合の対処法

Time 型のフィールドを含む構造体を JSONエンコードJSON からデコードすると特定条件下で Time 型オブジェクトの time.Location の情報が失われることがあります。 以下のプログラムを見てください。

package main

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

type Hoge struct {
    Name string
    CreatedAt time.Time
}

func main() {
    time.Local = time.UTC
 
    // Hoge構造体を生成する
    h1 := Hoge{
        Name: "aaa", 
        CreatedAt: time.Unix(1593077427, 0),
    }

    // Jsonsに変換
    j, _ := json.Marshal(h1)
 
    // Jsonから復元する
    h2 := Hoge{}
    json.Unmarshal(j, &h2)
 
    fmt.Printf("h1の出力結果\n%#v\n", h1)
    fmt.Printf("h2の出力結果\n%#v\n", h2)
}

出力結果を確認すると、JSON 変換前の Hoge オブジェクト(変数名 h1)の CreatedAt フィールドは loc に (*time.Location)(0x5c7a40) がセットされているのに対し、JSON 変換後に復元した Hoge オブジェクト(変数名 h2)の CreatedAt フィールドは loc が nil になっています。

h1の出力結果
main.Hoge{Name:"aaa", CreatedAt:time.Time{wall:0x0, ext:63728674227, loc:(*time.Location)(0x5c7a40)}}
h2の出力結果
main.Hoge{Name:"aaa", CreatedAt:time.Time{wall:0x0, ext:63728674227, loc:(*time.Location)(nil)}}

これ %+v で出力しても違いがわからないので注意が必要です。%#v で出力してみないとわからないです。

time.Localにtime.UTCをセットした場合のみ起きる現象

JSON に変換すると Time 型フィールドの loc 情報が nil になってしまう現象は time.Localにtime.UTCをセットした場合のみ発生します。

func main() {
    time.Local = time.UTC
    ...
    ... その他、処理
    ...
}

以下のように UTC 以外のローカルタイムを設定している場合は発生しません。

func main() {
    loc, _ := time.LoadLocation("Asia/Tokyo")
    time.Local = loc
    ...
    ... その他、処理
    ...
}

ローカル時間をUTCにセットするとreflect.DeepEqualが常にfalse判定されてしまう

time.Location の情報が失われると、Time 型フィールドを含む構造体を reflect.DeepEqual で比較した時に意図通り判定されなくなります。以下のプログラムを見てください。

package main

import (
    "fmt"
    "time"
    "encoding/json"
    "reflect"
)

// Hoge構造体の定義は省略

func main() {
    time.Local = time.UTC
 
    // Hoge構造体を生成する
    h1 := Hoge{
        Name: "aaa", 
        CreatedAt: time.Unix(1593077427, 0),
    }

    // Jsonsに変換
    j, _ := json.Marshal(h1)
 
    // Jsonから復元する
    h2 := Hoge{}
    json.Unmarshal(j, &h2)
 
    result := reflect.DeepEqual(h1, h2)
    fmt.Printf("reflect.DeepEqual: %v", result)
}

result の出力結果は true ではなく false になります。

reflect.DeepEqual: false

対処方法

これに対処する方法はいくつか存在します。

  • Time.Equal メソッドを使って判定する
  • UTC に変換する
  • go-cmp を使って判定する
  • time.Local に nil をセットする

個別に対処方法を見ていきましょう

Time.Equal メソッドを使って判定する

こちらは time パッケージの Godoc に記載されている方法です。

result := h1.CreatedAt.Equal(h2.CreatedAt)
fmt.Printf("Time.Eqaul: %v", result)

出力結果は true になります。

Time.Eqaul: true

ただこの方法だと Time 型のフィールドを含む構造体同士を比較する場合はフィールドを一つずつ比較しないといけなくなります。

UTC に変換する

Time オブジェクトを生成するタイミングで UTC メソッドを使って UTC 変換に変換します。

func main() {
    time.Local = time.UTC
 
    // Hoge構造体を生成する
    h1 := Hoge{
        Name: "aaa", 
        // UTCに変換する
        CreatedAt: time.Unix(1593077427, 0).UTC(),
    }

    // Jsonsに変換
    j, _ := json.Marshal(h1)
 
    // Jsonから復元する
    h2 := Hoge{}
    json.Unmarshal(j, &h2)
 
    result := reflect.DeepEqual(h1, h2)
    fmt.Printf("reflect.DeepEqual: %v", result)
}

Hoge 構造体を生成するときに UTC メソッドを使って変換する必要がありますが結果はちゃんと true になります。

reflect.DeepEqual: true

Equal メソッドを使うよりも良さそうではありますが UTC メソッド呼び出しを忘れるとバグの温床になります。個人的にはあまりおすすめしません。

go-cmp を使って判定する

reflect.DeepEqual を使う代わりに、ユニットテストで構造体を比較する時の定番パッケージ go-cmp を使って比較します。

import (
    "fmt"
    "time"
    "encoding/json"
    "github.com/google/go-cmp/cmp"
    "reflect"
)

// Hoge構造体の定義は省略

func main() {
    time.Local = time.UTC
 
    // Hoge構造体を生成する
    h1 := Hoge{
        Name: "aaa", 
        CreatedAt: time.Unix(1593077427, 0),
    }

    // Jsonsに変換
    j, _ := json.Marshal(h1)
 
    // Jsonから復元する
    h2 := Hoge{}
    json.Unmarshal(j, &h2)
 
    result1 := reflect.DeepEqual(h1, h2)
    fmt.Printf("reflect.DeepEqual: %v\n", result1)
 
    result2 := cmp.Equal(h1, h2)
    fmt.Printf("cmp.Equal: %v", result2)
}

出力結果は以下のようになります。

reflect.DeepEqual: false
cmp.Equal: true

go-cmp は Time 型フィールドを比較する時、内部で Time.Equal メソッドを使って判定してくれるので reflect.DeepEqual が false 判定してしまう場面でも意図通り判定してくれます。

time.Local に nil をセットする

go-cmp も良いのですが Go の標準パッケージだけでなんとかするなら、main 関数で time.Local に nil をセットする方法があります。

package main

import (
    "fmt"
    "time"
    "encoding/json"
    "reflect"
)

// Hoge構造体の定義は省略

func main() {
    // nilをセットする
    time.Local = nil
 
    // Hoge構造体を生成する
    h1 := Hoge{
        Name: "aaa", 
        CreatedAt: time.Unix(1593077427, 0),
    }

    // Jsonsに変換
    j, _ := json.Marshal(h1)
 
    // Jsonから復元する
    h2 := Hoge{}
    json.Unmarshal(j, &h2)
 
    result := reflect.DeepEqual(h1, h2)
    fmt.Printf("reflect.DeepEqual: %v\n", result)
}

出力結果は以下のようになります。意図通り reflect.DeepEqual の結果が true になります。

reflect.DeepEqual: true

参考記事