mirror of
https://github.com/gin-gonic/gin.git
synced 2026-09-04 22:53:34 +08:00
The time_format struct tag works for form/query binding but is completely ignored when binding via JSON (ShouldBindJSON). This commit adds support for time_format, time_utc, and time_location tags in the JSON binding path. When a struct has time.Time fields with time_format tags, the JSON decoder falls back to mappingByPtr with a jsonSource setter that uses setTimeField to parse the string value with the custom format. For structs without time_format tags, the original fast path (direct JSON unmarshal) is used, preserving full backward compatibility. Fixes #2170
170 lines
4.9 KiB
Go
170 lines
4.9 KiB
Go
// Copyright 2014 Manu Martinez-Almeida. All rights reserved.
|
|
// Use of this source code is governed by a MIT style
|
|
// license that can be found in the LICENSE file.
|
|
|
|
package binding
|
|
|
|
import (
|
|
"bytes"
|
|
"encoding/json"
|
|
"errors"
|
|
"io"
|
|
"net/http"
|
|
"reflect"
|
|
"strconv"
|
|
"time"
|
|
|
|
ginjson "github.com/gin-gonic/gin/codec/json"
|
|
)
|
|
|
|
// EnableDecoderUseNumber is used to call the UseNumber method on the JSON
|
|
// Decoder instance. UseNumber causes the Decoder to unmarshal a number into an
|
|
// any as a Number instead of as a float64.
|
|
var EnableDecoderUseNumber = false
|
|
|
|
// EnableDecoderDisallowUnknownFields is used to call the DisallowUnknownFields method
|
|
// on the JSON Decoder instance. DisallowUnknownFields causes the Decoder to
|
|
// return an error when the destination is a struct and the input contains object
|
|
// keys which do not match any non-ignored, exported fields in the destination.
|
|
var EnableDecoderDisallowUnknownFields = false
|
|
|
|
type jsonBinding struct{}
|
|
|
|
func (jsonBinding) Name() string {
|
|
return "json"
|
|
}
|
|
|
|
func (jsonBinding) Bind(req *http.Request, obj any) error {
|
|
if req == nil || req.Body == nil {
|
|
return errors.New("invalid request")
|
|
}
|
|
return decodeJSON(req.Body, obj)
|
|
}
|
|
|
|
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 {
|
|
// 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()
|
|
}
|
|
if EnableDecoderDisallowUnknownFields {
|
|
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
|
|
} |