28

lets say i have the following json

{
    name: "John",
    birth_date: "1996-10-07"
}

and i want to decode it into the following structure

type Person struct {
    Name string `json:"name"`
    BirthDate time.Time `json:"birth_date"`
}

like this

person := Person{}

decoder := json.NewDecoder(req.Body);

if err := decoder.Decode(&person); err != nil {
    log.Println(err)
}

which gives me the error parsing time ""1996-10-07"" as ""2006-01-02T15:04:05Z07:00"": cannot parse """ as "T"

if i were to parse it manually i would do it like this

t, err := time.Parse("2006-01-02", "1996-10-07")

but when the time value is from a json string how do i get the decoder to parse it in the above format?

Jonathan Hall
  • 75,165
  • 16
  • 143
  • 189
zola
  • 5,737
  • 8
  • 33
  • 48
  • 2
    Possible duplicate of [Parsing a json datetime in revel](https://stackoverflow.com/questions/44705817/parsing-a-json-datetime-in-revel) – RickyA Jul 25 '17 at 12:32
  • 2
    Possible duplicate of [Parsing date string in golang](https://stackoverflow.com/questions/25845172/parsing-date-string-in-golang) – Adrian Jul 25 '17 at 13:22

4 Answers4

35

That's a case when you need to implement custom marshal and unmarshal functions.

UnmarshalJSON(b []byte) error { ... }

MarshalJSON() ([]byte, error) { ... }

By following the example in the Golang documentation of json package you get something like:

// First create a type alias
type JsonBirthDate time.Time

// Add that to your struct
type Person struct {
    Name string `json:"name"`
    BirthDate JsonBirthDate `json:"birth_date"`
}

// Implement Marshaler and Unmarshaler interface
func (j *JsonBirthDate) UnmarshalJSON(b []byte) error {
    s := strings.Trim(string(b), "\"")
    t, err := time.Parse("2006-01-02", s)
    if err != nil {
        return err
    }
    *j = JsonBirthDate(t)
    return nil
}
    
func (j JsonBirthDate) MarshalJSON() ([]byte, error) {
    return json.Marshal(time.Time(j))
}

// Maybe a Format function for printing your date
func (j JsonBirthDate) Format(s string) string {
    t := time.Time(j)
    return t.Format(s)
}
AshClarke
  • 2,990
  • 5
  • 21
  • 27
Kiril
  • 6,009
  • 13
  • 57
  • 77
  • Right, and for the `UnmarshalJSON` func, OP could add multiple `time.Parse` attempts based on how many different formats need to be supported. I believe the format for `time.RFC3339` is the default parser and more formats can be found in the [docs](https://golang.org/pkg/time/#pkg-constants) – Jonathan Jul 25 '17 at 13:09
  • 1
    Sure, when you have the custom un/marshal functions, you should try to cover every case that's possible. – Kiril Jul 25 '17 at 13:16
  • Minor nit: it should be `JSONBirthDate`, not `JsonBirthDate`, as per [the code style](https://github.com/golang/go/wiki/CodeReviewComments#initialisms). – Ainar-G Jul 25 '17 at 14:20
  • 4
    What is JB in the `*j = JB(t)` line? – WaltPurvis Jul 26 '17 at 02:52
  • 1
    Since you have a type alias, you need to cast it. Read this, and the corresponding links to the documentation: https://stackoverflow.com/questions/19577423/how-to-cast-to-a-type-alias-in-go – Kiril Jul 26 '17 at 07:29
  • 7
    The `MarshalJSON()` method causes a stack overflow panic, as it indirectly calls itself. – danielcooperxyz Aug 31 '18 at 15:36
  • @danielcooperxyz is right; you need a cast here: `return j.(time.Time).MarshalJSON()` – Leo Alekseyev Apr 06 '21 at 01:20
  • 1
    @Kiril good solution. However, please note that the MarshalJSON() method will return a string in the format of "2023-03-12T00:00:00Z". To make it return a string in the format of "2023-03-12", it needs to be rewritten as follows. `return []byte("\"" + time.Time(j).Format("2006-01-02") + "\""), nil` – Sergei Popinevskii Mar 12 '23 at 11:41
6

If there are lots of struct and you just implement custom marshal und unmarshal functions, that's a lot of work to do so. You can use another lib instead,such as a json-iterator extension jsontime:

import "github.com/liamylian/jsontime"

var json = jsontime.ConfigWithCustomTimeFormat

type Book struct {
    Id        int           `json:"id"`
    UpdatedAt *time.Time    `json:"updated_at" time_format:"sql_date" time_utc:"true"`
    CreatedAt time.Time     `json:"created_at" time_format:"sql_datetime" time_location:"UTC"`
}
Liam Lian
  • 61
  • 1
  • 2
2

I wrote a package for handling yyyy-MM-dd and yyyy-MM-ddThh:mm:ss dates at https://github.com/a-h/date

It uses the type alias approach in the answer above, then implements the MarshalJSON and UnmarshalJSON functions with a few alterations.

// MarshalJSON outputs JSON.
func (d YYYYMMDD) MarshalJSON() ([]byte, error) {
    return []byte("\"" + time.Time(d).Format(formatStringYYYYMMDD) + "\""), nil
}

// UnmarshalJSON handles incoming JSON.
func (d *YYYYMMDD) UnmarshalJSON(b []byte) (err error) {
    if err = checkJSONYYYYMMDD(string(b)); err != nil {
        return
    }
    t, err := time.ParseInLocation(parseJSONYYYYMMDD, string(b), time.UTC)
    if err != nil {
        return
    }
    *d = YYYYMMDD(t)
    return
}

It's important to parse in the correct timezone. My code assumes UTC, but you may wish to use the computer's timezone for some reason.

I also found that solutions which involved using the time.Parse function leaked Go's internal mechanisms as an error message which clients didn't find helpful, for example: cannot parse "sdfdf-01-01" as "2006". That's only useful if you know that the server is written in Go, and that 2006 is the example date format, so I put in more readable error messages.

I also implemented the Stringer interface so that it gets pretty printed in log or debug messages.

a-h
  • 4,244
  • 2
  • 23
  • 29
0

Custom implementation of marshal, unmarshal and string methods.

package json

import (
    "fmt"
    "strings"
    "time"
)

const rfc3339 string = "2006-01-02"

// Date represents a date without a time component, encoded as a string
// in the "YYYY-MM-DD" format.
type Date struct {
    Year  int
    Month time.Month
    Day   int
}

// UnmarshalJSON implements json.Unmarshaler inferface.
func (d *Date) UnmarshalJSON(b []byte) error {
    t, err := time.Parse(rfc3339, strings.Trim(string(b), `"`))
    if err != nil {
        return err
    }
    d.Year, d.Month, d.Day = t.Date()
    return nil
}

// MarshalJSON implements json.Marshaler interface.
func (d Date) MarshalJSON() ([]byte, error) {
    s := fmt.Sprintf(`"%04d-%02d-%02d"`, d.Year, d.Month, d.Day)
    return []byte(s), nil
}

// String defines a string representation.
// It will be called automatically when you try to convert struct instance
// to a string.
func (d Date) String() string {
    return fmt.Sprintf("%04d-%02d-%02d", d.Year, d.Month, d.Day)
}

And tests for them.

package json

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

func TestDate_UnmarshalJSON(t *testing.T) {
    in := `"2022-12-31"`
    want := time.Date(2022, time.December, 31, 0, 0, 0, 0, time.UTC)

    var got Date
    if err := got.UnmarshalJSON([]byte(in)); err != nil {
        t.Fatalf("unexpected error: %v", err)
    }

    if !(got.Year == want.Year() && got.Month == want.Month() && got.Day == want.Day()) {
        t.Errorf("got date = %s, want %s", got, want)
    }
}

func TestDate_UnmarshalJSON_badFormat(t *testing.T) {
    in := `"31 Dec 22"`

    var got Date
    err := got.UnmarshalJSON([]byte(in))

    if err, ok := err.(*time.ParseError); !ok {
        t.Errorf("expected a time parse error, got: %v", err)
    }
}

func TestDate_MarshalJSON(t *testing.T) {
    testcases := map[string]struct {
        in   Date
        want string
    }{
        "without zero padding": {
            in:   Date{2022, time.December, 31},
            want: `"2022-12-31"`,
        },
        "with zero padding": {
            in:   Date{2022, time.July, 1},
            want: `"2022-07-01"`,
        },
        "initial value": {
            in:   Date{},
            want: `"0000-00-00"`,
        },
    }

    for name, tc := range testcases {
        t.Run(name, func(t *testing.T) {
            got, err := json.Marshal(tc.in)
            if err != nil {
                t.Fatalf("unexpected error: %v", err)
            }

            if string(got) != tc.want {
                t.Errorf("got date = %s, want %s", got, tc.want)
            }
        })
    }
}

func TestDate_String(t *testing.T) {
    testcases := map[string]struct {
        in   Date
        want string
    }{
        "without zero padding": {
            in:   Date{2022, time.December, 31},
            want: "2022-12-31",
        },
        "with zero padding": {
            in:   Date{2022, time.July, 1},
            want: "2022-07-01",
        },
        "initial value": {
            in:   Date{},
            want: "0000-00-00",
        },
    }

    for name, tc := range testcases {
        t.Run(name, func(t *testing.T) {
            if got := tc.in.String(); got != tc.want {
                t.Errorf("got %q, want %q", got, tc.want)
            }
        })
    }
}