36

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?

2

4 Answers 4

41

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)
}
Sign up to request clarification or add additional context in comments.

9 Comments

Sure, when you have the custom un/marshal functions, you should try to cover every case that's possible.
What is JB in the *j = JB(t) line?
Since you have a type alias, you need to cast it. Read this, and the corresponding links to the documentation: stackoverflow.com/questions/19577423/…
The MarshalJSON() method causes a stack overflow panic, as it indirectly calls itself.
@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
|
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"`
}

Comments

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.

Comments

2

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)
            }
        })
    }
}

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.