LoginSignup
1
0

Goで様々なフォーマットの時刻JSONをtime.Timeとして扱う

Posted at

はじめに

始めまして。最近業務でGoを書いているハセガワカンタです。
先日、みなさんも日常的に使っているであろうtime.Timeの仕様で困ったことがありました。使用しているAPIの関係で以下のようなJSONをtime.Timeとして扱わなければいけなくなったのです。

{
    "date": "2024-01-01"
}
type Example struct {
	Date time.Time `json:"date"`
}

実はこれ普通にUnmarshalしても上手くいきません

type Example struct {
	Date time.Time `json:"date"`
}

func main() {
	j := `{"date": "2024-01-01"}`
	var e Example
	json.Unmarshal([]byte(j), &e)
	fmt.Println("date is", e.Date)
}

date is 0001-01-01 00:00:00 +0000 UTC

time.TimeのMarshalerとUnmarshaler

なぜこれがUnmarshal出来ないのかpkg.go.devを読んでみました。

MarshalJSON implements the json.Marshaler interface. The time is a quoted string in the RFC 3339 format with sub-second precision. If the timestamp cannot be represented as valid RFC 3339 (e.g., the year is out of range), then an error is reported.

UnmarshalJSON implements the json.Unmarshaler interface. The time must be a quoted string in the RFC 3339 format.

// MarshalJSON implements the json.Marshaler interface.
// The time is a quoted string in the RFC 3339 format with sub-second precision.
// If the timestamp cannot be represented as valid RFC 3339
// (e.g., the year is out of range), then an error is reported.
func (t Time) MarshalJSON() ([]byte, error) {
	b := make([]byte, 0, len(RFC3339Nano)+len(`""`))
	b = append(b, '"')
	b, err := t.appendStrictRFC3339(b)
	b = append(b, '"')
	if err != nil {
		return nil, errors.New("Time.MarshalJSON: " + err.Error())
	}
	return b, nil
}

// UnmarshalJSON implements the json.Unmarshaler interface.
// The time must be a quoted string in the RFC 3339 format.
func (t *Time) UnmarshalJSON(data []byte) error {
	if string(data) == "null" {
		return nil
	}
	// TODO(https://go.dev/issue/47353): Properly unescape a JSON string.
	if len(data) < 2 || data[0] != '"' || data[len(data)-1] != '"' {
		return errors.New("Time.UnmarshalJSON: input is not a JSON string")
	}
	data = data[len(`"`) : len(data)-len(`"`)]
	var err error
	*t, err = parseStrictRFC3339(data)
	return err
}

どうやらRFC3339というフォーマットにしか対応していないようなので、MarshalerとUnmarshalerを弄って他のフォーマットにも対応させる必要があります。

実装

このように特定のフォーマットで決め打ちしても出来ますが、今回は汎用性を持たせるために別の実装にしました

type YYYYMMDD time.Time

func (d *YYYYMMDD) UnmarshalJSON(b []byte) error {
	s := string(b)
	t, err := time.Parse(`"2006-01-02"`, s)
	if err != nil {
		return err
	}
	*d = YYYYMMDD(t)
	return nil
}

func (d YYYYMMDD) MarshalJSON() ([]byte, error) {
	return []byte(time.Time(d).Format(`"2006-01-02"`)), nil
}

MultiFormatTimeという様々なフォーマットに対応できる型を作成し、Unmarshal時に採用したフォーマット形式を保持してMarshal時にも対応できるようにしています。

// NOTE: Formatフィールドを持つことでUnMarshal時にどのフォーマットでパースされたかを保持し、それを元にMarshal時に元のフォーマットで出力する
type MultiFormatTime struct {
	time.Time
	Format string
}

// NOTE: 一致するフォーマットが存在しない場合はこのフォーマットで出力する
const DEFAULT_FORMAT = time.RFC3339

// NOTE: 必要になったらフォーマットを追加する
var formats = []string{
	time.RFC3339,
	time.DateOnly,
}

func (mt *MultiFormatTime) UnmarshalJSON(b []byte) error {
	s := string(b)
	var err error
	var t time.Time
	for _, format := range formats {
		t, err = time.Parse(`"`+format+`"`, s)
		if err == nil {
			mt.Time = t
			mt.Format = format
			return nil
		}
	}

	return err
}

func (mt MultiFormatTime) MarshalJSON() ([]byte, error) {
	if mt.Format != "" {
		return json.Marshal(mt.Time.Format(mt.Format))
	}
	return json.Marshal(mt.Time.Format(DEFAULT_FORMAT))
}

ライブラリ

timeパッケージに定義されているフォーマット群にあらかじめ対応したものをパッケージとして公開しているのよければ使ってください。
https://github.com/KantaHasegawa/multi_format_time

1
0
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
0