mirror of
https://github.com/gin-gonic/gin.git
synced 2026-09-04 14:49:27 +08:00
Merge 04a7275c7123117c773611de54ef031895ed2c37 into dcaa4296d111981ffb31ac3eba90bb63e1eb5ab9
This commit is contained in:
commit
32d829a399
118
binding/json.go
118
binding/json.go
@ -6,11 +6,15 @@ package binding
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"io"
|
||||
"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
|
||||
@ -41,8 +45,62 @@ func (jsonBinding) BindBody(body []byte, obj any) error {
|
||||
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 {
|
||||
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 {
|
||||
decoder.UseNumber()
|
||||
}
|
||||
@ -50,7 +108,63 @@ func decodeJSON(r io.Reader, obj any) error {
|
||||
decoder.DisallowUnknownFields()
|
||||
}
|
||||
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 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
|
||||
}
|
||||
@ -37,6 +37,59 @@ func TestJSONBindingBindBodyMap(t *testing.T) {
|
||||
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) {
|
||||
// Restore json encoding configuration after testing
|
||||
oldMarshal := json.API
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user