Go で JSON の日付フォーマットを制御する:time.Time のカスタム処理
time.Time を JSON に変換すると、デフォルトでは RFC 3339 形式("2025-01-15T09:30:00Z")になります。これは国際標準として広く採用されていますが、外部 API やフロントエンドが異なるフォーマットを要求するケースは珍しくありません。
デフォルトの挙動
まず何もカスタマイズしない場合の出力を確認しておきましょう。
type Event struct {
Name string `json:"name"`
Date time.Time `json:"date"`
}
func main() {
event := Event{
Name: "会議",
Date: time.Date(2025, 6, 15, 14, 30, 0, 0, time.UTC),
}
data, _ := json.Marshal(event)
fmt.Println(string(data))
// {"name":"会議","date":"2025-06-15T14:30:00Z"}
}RFC 3339 はタイムゾーン情報を含み、多くの API やデータベースでネイティブにサポートされています。特に理由がなければこのまま使うのがベストです。
ただし "2025/06/15" のような形式や、Unix タイムスタンプで扱う必要がある場合は、カスタム処理を書くことになります。
Go の日付フォーマットの仕組み
カスタム処理に入る前に、Go 独特のフォーマット指定方法を理解する必要があります。Go では 2006-01-02 15:04:05 という特定の参照日時を書式文字列として使います。
| 2006 | 年(4 桁) |
| 01 | 月(2 桁ゼロ埋め) |
| 02 | 日(2 桁ゼロ埋め) |
| 15 | 時(24 時間制) |
| 04 | 分 |
| 05 | 秒 |
この参照日時は「Mon Jan 2 15:04:05 MST 2006」で、各要素が 1, 2, 3, 4, 5, 6, 7 に対応するよう設計されています。他の言語の YYYY-MM-DD HH:mm:ss に慣れていると最初は戸惑いますが、Go では避けて通れない仕様です。
カスタム日付型を作る
time.Time をラップした型を定義し、MarshalJSON と UnmarshalJSON を実装するのが標準的なやり方です。
type DateOnly struct {
time.Time
}
const dateFormat = "2006-01-02"
func (d DateOnly) MarshalJSON() ([]byte, error) {
return json.Marshal(d.Format(dateFormat))
}
func (d *DateOnly) UnmarshalJSON(data []byte) error {
var s string
if err := json.Unmarshal(data, &s); err != nil {
return err
}
t, err := time.Parse(dateFormat, s)
if err != nil {
return fmt.Errorf("日付のパースに失敗: %w", err)
}
d.Time = t
return nil
}time.Time を埋め込んでいるので、Format や After、Before などのメソッドはそのまま使えます。
type Event struct {
Name string `json:"name"`
Date DateOnly `json:"date"`
}
func main() {
event := Event{
Name: "会議",
Date: DateOnly{time.Date(2025, 6, 15, 0, 0, 0, 0, time.UTC)},
}
data, _ := json.Marshal(event)
fmt.Println(string(data))
// {"name":"会議","date":"2025-06-15"}
var parsed Event
json.Unmarshal([]byte(`{"name":"面談","date":"2025-07-20"}`), &parsed)
fmt.Println(parsed.Date.Format("2006年1月2日"))
// 2025年7月20日
}Unix タイムスタンプ型
フロントエンドやモバイルアプリでは、Unix タイムスタンプ(秒単位の整数値)で日時をやりとりするケースも多いです。
type UnixTime struct {
time.Time
}
func (u UnixTime) MarshalJSON() ([]byte, error) {
return json.Marshal(u.Unix())
}
func (u *UnixTime) UnmarshalJSON(data []byte) error {
var ts int64
if err := json.Unmarshal(data, &ts); err != nil {
return err
}
u.Time = time.Unix(ts, 0)
return nil
}type LogEntry struct {
Message string `json:"message"`
Timestamp UnixTime `json:"timestamp"`
}
func main() {
entry := LogEntry{
Message: "サーバー起動",
Timestamp: UnixTime{time.Now()},
}
data, _ := json.Marshal(entry)
fmt.Println(string(data))
// {"message":"サーバー起動","timestamp":1750000000}
}人間が読みやすく、タイムゾーン情報を含む。API 間のやりとりで推奨される標準フォーマット。
数値なのでソートや比較が高速。JavaScript の Date との相互変換が容易。ただし人間には読めない。
ミリ秒対応の Unix タイムスタンプ
JavaScript の Date.now() はミリ秒を返すため、フロントエンドとの連携ではミリ秒単位が求められることもあります。
type UnixMilliTime struct {
time.Time
}
func (u UnixMilliTime) MarshalJSON() ([]byte, error) {
return json.Marshal(u.UnixMilli())
}
func (u *UnixMilliTime) UnmarshalJSON(data []byte) error {
var ms int64
if err := json.Unmarshal(data, &ms); err != nil {
return err
}
u.Time = time.UnixMilli(ms)
return nil
}time.UnixMilli は Go 1.17 で追加されました。それ以前のバージョンでは time.Unix(ms/1000, (ms%1000)*1e6) のように手計算が必要です。
複数フォーマットを受け付ける
外部 API のレスポンスでフォーマットが統一されていない場合、複数の形式を順番に試す方法が現実的です。
type FlexTime struct {
time.Time
}
var formats = []string{
"2006-01-02T15:04:05Z07:00",
"2006-01-02T15:04:05",
"2006-01-02",
"2006/01/02",
}
func (f *FlexTime) UnmarshalJSON(data []byte) error {
var s string
if err := json.Unmarshal(data, &s); err != nil {
return err
}
for _, format := range formats {
if t, err := time.Parse(format, s); err == nil {
f.Time = t
return nil
}
}
return fmt.Errorf("対応していない日付形式: %s", s)
}フォーマットのスライスを優先度の高い順に並べ、最初にパースできたものを採用します。すべて失敗したらエラーです。
受け入れ側は柔軟にしつつ、出力側は一つの形式に固定しておくのがトラブルを減らすコツです。いわゆる「寛容に受け入れ、厳密に出力する」の原則で、日付フォーマットの不一致に起因するバグを大幅に減らせます。










