gin-gonic / gin

Gin is a HTTP web framework written in Go (Golang). It features a Martini-like API with much better performance -- up to 40 times faster. If you need smashing performance, get yourself some Gin.
https://gin-gonic.com/
MIT License
77.99k stars 7.97k forks source link

Custom time.Time type can not use bindQuery get a value 2 #3919

Closed feiyangbeyond closed 5 months ago

feiyangbeyond commented 5 months ago

I have a new type MyTime, and it has implements encoding/json.Unmarshaler.


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

type MyTime time.Time

func (t *MyTime) UnmarshalJSON(data []byte) error {
    var v interface{}
    if err := json.Unmarshal(data, &v); err != nil {
        return err
    }
    switch value := v.(type) {
    case string:
        var err error
        tt, err := time.ParseInLocation(defaultTimeLayout, value, time.Local)
        if err != nil {
            return err
        }
        *t = MyTime(tt)
        return nil
    default:
        return errors.New("invalid time format")
    }
}

func (t MyTime) MarshalJSON() ([]byte, error) {
    return json.Marshal(time.Time(t).Format(defaultTimeLayout))
}

And my struct define:

type MyReq struct {
    Time MyTime `json:"time" form:"time" binding:"required" time_format:"2006-01-02 15:04:05"`
}

When I use http get query , the same error occurred. And after changing MyReq.Time type to time.Time, it works.

I hope it works with MyTime.

Did I use it improperly?

Originally posted by @feiyangbeyond in https://github.com/gin-gonic/gin/issues/3895#issuecomment-2033774740

--

Why I use MyTime?

I wish to control the type of time in json serialization/formatting, I use MyTime in get/post request to keep it consistent

RedCrazyGhost commented 5 months ago

I don't think this issue is about gin, but you can refer to the source code of Time.time and write a custom type of JSON serialization and deserialization method.

// 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
}
feiyangbeyond commented 5 months ago

@RedCrazyGhost

I'm not sure if it's a gin error. Let me tell you the results of the investigation.

The program has thrown an error before it reaches the custom time type I wrote.

This error only occurs when making a get request and using gin to bind query. The error is invalid character '-' after top-level value

My struct:

type Foo struct {
    Bar string `form:"bar"`
    Time MyTime `form:"time"` // use MyTime 
}

When I send get request like /foo?bar=some&time=2024-04-17 13:21:45 , gin uses json.Unmarshal to convert 2024-04-17 13:21:45 to MyTime,

Since 2024-04-17 13:21:45 is not a normal json string, using json.Unmarshal will definitely report an error.

I think the error root cause is binding single parameter value as json when binding parameters on get request.

I can add "" before and after the time field when sending a get request, this way you can avoid this error.

dreamlu commented 4 weeks ago

I meet the same problem: my custom time type:

type CTime time.Time

func (t CTime) MarshalJSON() ([]byte, error) {
    return marshalJSON[CTime](t)
}

func (t *CTime) UnmarshalJSON(b []byte) error {
    s := strings.Trim(string(b), `"`)
    if s == "" {
        return nil
    }
    if len(s) <= 10 {
        s = fmt.Sprintf("%s 00:00:00", s)
    }
    ti, err := parse(Layout, s)
    if err != nil {
        return err
    }
    *t = CTime(ti)
    return nil
}

this problem append in the source code: bind query.go and the method func mapForm()

in this function about custom time type error handle image image

this is not ptr

maybe like gorm handle custom type?

feiyangbeyond commented 4 weeks ago

This problem is very annoying.

Later, I used unix timestamp instead of time.Time in http request and response.