Solution

Implementing the MarshalJSON Interface

type Log struct {
  Id        int64     `json:"id"`
  UserId    string    `json:"user_id"`
  Operation string    `json:"operation"`
  Result    string    `json:"result"`
  CreatedAt time.Time `json:"created_at"`
}

func (log *Log) MarshalJSON() ([]byte, error) {
  type Alias Log
  return json.Marshal(&struct {
    Alias
    CreatedAt string `json:"created_at"`
  }{
    Alias:     Alias(*log),
    CreatedAt: log.CreatedAt.Format("2006-01-02 15:04:05"),
  })
}

This approach is relatively simple, but if you have many structs that contain time fields, you’ll need to implement MarshalJSON for each struct, which can be cumbersome. Additionally, there’s an issue with reverse serialization where deserialization fails if created_at is null.

As for why the time format is 2006-01-02 15:04:05, you can refer to the constants defined in the format.go file of the time package.

Defining a Custom Time Data Type

package utils

import (
  "time"
)

type (
  Time time.Time
)

const DefaultTimeFormat = "2006-01-02 15:04:05"

func (t *Time) UnmarshalJSON(data []byte) error {
  now, err := time.ParseInLocation(`"`+DefaultTimeFormat+`"`, string(data), time.Local)
  *t = Time(now)
  return err
}

func (t *Time) MarshalJSON() ([]byte, error) {
  if time.Time(*t).IsZero() {
    return []byte("null"), nil
  }
  b := make([]byte, 0, len(DefaultTimeFormat)+2)
  b = append(b, '"')
  b = time.Time(*t).AppendFormat(b, DefaultTimeFormat)
  b = append(b, '"')
  return b, nil
}

func (t Time) String() string {
  return time.Time(t).Format(DefaultTimeFormat)
}

func (t *Time) IsZero() bool {
  return time.Time(*t).Second() == 0 && time.Time(*t).Nanosecond() == 0
}

After defining the time type, use this type as the time type in other structs.

Node struct {
  Id          string               `json:"id"`
  Name        string               `json:"name"`
  CreatedAt   utils.Time           `json:"created_at"`
  UpdatedAt   utils.Time           `json:"updated_at"`
}

This way, when Node is serialized and deserialized, it will automatically call the custom MarshalJSON and UnmarshalJSON methods of utils.Time.

Note: In the MarshalJSON method, if your utils.Time is empty, it will be converted to null instead of 0001-01-01 00:00:00 +0000 UTC.

I hope this is helpful, Happy hacking…