Merge 04a7275c7123117c773611de54ef031895ed2c37 into dcaa4296d111981ffb31ac3eba90bb63e1eb5ab9

This commit is contained in:
water 2026-08-19 23:09:57 -07:00 committed by GitHub
commit 32d829a399
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 169 additions and 2 deletions

View File

@ -6,11 +6,15 @@ package binding
import ( import (
"bytes" "bytes"
"encoding/json"
"errors" "errors"
"io" "io"
"net/http" "net/http"
"reflect"
"strconv"
"time"
"github.com/gin-gonic/gin/codec/json" ginjson "github.com/gin-gonic/gin/codec/json"
) )
// EnableDecoderUseNumber is used to call the UseNumber method on the JSON // EnableDecoderUseNumber is used to call the UseNumber method on the JSON
@ -41,8 +45,62 @@ func (jsonBinding) BindBody(body []byte, obj any) error {
return decodeJSON(bytes.NewReader(body), obj) return decodeJSON(bytes.NewReader(body), obj)
} }
// jsonSource implements setter for JSON binding, allowing time_format
// and time_utc tags to work with JSON binding (ShouldBindJSON).
// For time.Time fields with time_format tags, it uses setTimeField
// to parse the string value. For all other fields, it uses standard
// JSON unmarshaling.
type jsonSource map[string]json.RawMessage
var _ setter = jsonSource(nil)
// timeTimeType is the reflect.Type for time.Time, used for comparison.
var timeTimeType = reflect.TypeOf(time.Time{})
// TrySet tries to set a value from a JSON source.
func (j jsonSource) TrySet(value reflect.Value, field reflect.StructField, key string, opt setOptions) (isSet bool, err error) {
raw, ok := j[key]
if !ok {
return false, nil
}
// For time.Time fields with a time_format tag, use setTimeField
// to parse the string value using the custom format.
if value.Type() == timeTimeType && field.Tag.Get("time_format") != "" {
// Unmarshal the raw JSON value as a string
var s string
if err := ginjson.API.Unmarshal(raw, &s); err != nil {
// If the raw value is a number (unix timestamp), try that too
var n int64
if err2 := ginjson.API.Unmarshal(raw, &n); err2 != nil {
return false, err
}
// Convert number to string for setTimeField
s = strconv.FormatInt(n, 10)
}
return true, setTimeField(s, field, value)
}
// For all other types, use standard JSON unmarshaling.
if err := ginjson.API.Unmarshal(raw, value.Addr().Interface()); err != nil {
return false, err
}
return true, nil
}
func decodeJSON(r io.Reader, obj any) error { func decodeJSON(r io.Reader, obj any) error {
decoder := json.API.NewDecoder(r) // Read the full body first so we can retry if needed
body, err := io.ReadAll(r)
if err != nil {
return err
}
if len(body) == 0 {
return errors.New("empty JSON body")
}
// Fast path: try the original decoder approach first (preserves UseNumber,
// DisallowUnknownFields, and works with any JSON codec backend).
decoder := ginjson.API.NewDecoder(bytes.NewReader(body))
if EnableDecoderUseNumber { if EnableDecoderUseNumber {
decoder.UseNumber() decoder.UseNumber()
} }
@ -50,7 +108,63 @@ func decodeJSON(r io.Reader, obj any) error {
decoder.DisallowUnknownFields() decoder.DisallowUnknownFields()
} }
if err := decoder.Decode(obj); err != nil { if err := decoder.Decode(obj); err != nil {
// If the struct has time_format tags, the decode may have failed because
// a time.Time field used a non-RFC3339 format. Fall back to the jsonSource
// approach which respects the time_format tag.
if hasTimeFormatTag(obj) {
if err2 := decodeJSONWithTimeFormat(body, obj); err2 != nil {
return err // return the original decode error, which is more descriptive
}
return validate(obj)
}
return err return err
} }
return validate(obj) return validate(obj)
} }
// decodeJSONWithTimeFormat decodes JSON using mappingByPtr with a jsonSource,
// which respects time_format tags on time.Time fields.
func decodeJSONWithTimeFormat(body []byte, obj any) error {
var rawMap map[string]json.RawMessage
if err := ginjson.API.Unmarshal(body, &rawMap); err != nil {
return err
}
if err := mappingByPtr(obj, jsonSource(rawMap), "json"); err != nil {
return err
}
return nil
}
// hasTimeFormatTag checks if the given struct (or any nested struct) has
// any time.Time fields with a time_format tag.
func hasTimeFormatTag(obj any) bool {
v := reflect.ValueOf(obj)
if v.Kind() == reflect.Ptr {
v = v.Elem()
}
if v.Kind() != reflect.Struct {
return false
}
return hasTimeFormatTagRecursive(v.Type())
}
func hasTimeFormatTagRecursive(t reflect.Type) bool {
for i := range t.NumField() {
f := t.Field(i)
if f.Type.Kind() == reflect.Struct {
if f.Type == timeTimeType {
if f.Tag.Get("time_format") != "" {
return true
}
} else if f.Anonymous {
if hasTimeFormatTagRecursive(f.Type) {
return true
}
}
}
}
return false
}

View File

@ -37,6 +37,59 @@ func TestJSONBindingBindBodyMap(t *testing.T) {
assert.Equal(t, "world", s["hello"]) assert.Equal(t, "world", s["hello"])
} }
func TestJSONBindingWithTimeFormat(t *testing.T) {
// Test that time_format tag works with JSON binding (issue #2170)
type TimeFormatStruct struct {
Timestamp time.Time `json:"Timestamp" time_format:"2006-01-02 15:04:05"`
UnixTime time.Time `json:"UnixTime" time_format:"unix"`
UnixNano time.Time `json:"UnixNano" time_format:"unixNano"`
}
var s TimeFormatStruct
err := jsonBinding{}.BindBody([]byte(`{"Timestamp": "2001-11-11 11:11:11", "UnixTime": 1575528300, "UnixNano": 1575528300000000000}`), &s)
require.NoError(t, err)
assert.Equal(t, time.Date(2001, 11, 11, 11, 11, 11, 0, time.Local), s.Timestamp)
assert.Equal(t, time.Unix(1575528300, 0), s.UnixTime)
assert.Equal(t, time.Unix(0, 1575528300000000000), s.UnixNano)
}
func TestJSONBindingWithTimeFormatAndUTC(t *testing.T) {
// Test that time_format works with time_utc tag in JSON binding
type TimeUTCStruct struct {
LocalTime time.Time `json:"local_time" time_format:"2006-01-02" time_utc:"1"`
}
var s TimeUTCStruct
err := jsonBinding{}.BindBody([]byte(`{"local_time": "2001-11-11"}`), &s)
require.NoError(t, err)
assert.Equal(t, time.Date(2001, 11, 11, 0, 0, 0, 0, time.UTC), s.LocalTime)
}
func TestJSONBindingWithTimeFormatAndLocation(t *testing.T) {
// Test that time_format works with time_location tag in JSON binding
type TimeLocationStruct struct {
LocalTime time.Time `json:"local_time" time_format:"2006-01-02" time_location:"Asia/Shanghai"`
}
var s TimeLocationStruct
err := jsonBinding{}.BindBody([]byte(`{"local_time": "2001-11-11"}`), &s)
require.NoError(t, err)
loc, _ := time.LoadLocation("Asia/Shanghai")
assert.Equal(t, time.Date(2001, 11, 11, 0, 0, 0, 0, loc), s.LocalTime)
}
func TestJSONBindingWithoutTimeFormat(t *testing.T) {
// Test that RFC3339 format still works (no time_format tag)
type NoTimeFormatStruct struct {
Timestamp time.Time `json:"timestamp"`
}
var s NoTimeFormatStruct
err := jsonBinding{}.BindBody([]byte(`{"timestamp": "2001-11-11T11:11:11Z"}`), &s)
require.NoError(t, err)
assert.Equal(t, time.Date(2001, 11, 11, 11, 11, 11, 0, time.UTC), s.Timestamp)
}
func TestCustomJsonCodec(t *testing.T) { func TestCustomJsonCodec(t *testing.T) {
// Restore json encoding configuration after testing // Restore json encoding configuration after testing
oldMarshal := json.API oldMarshal := json.API