mirror of
https://github.com/gin-gonic/gin.git
synced 2026-04-29 23:23:18 +08:00
fix(binding): handle non-JSON string values in form binding for custom struct types
When a struct field implements json.Unmarshaler, form binding passes the raw form value directly to json.Unmarshal. Since form values are plain strings (e.g. "2020/09/23 13:20:49"), not valid JSON, the unmarshal fails. Fix this by retrying with the value wrapped in JSON quotes when the initial unmarshal fails. Fixes #2510
This commit is contained in:
parent
ecd26c8835
commit
cb5463e5d8
@ -373,9 +373,9 @@ func setWithProperType(val string, value reflect.Value, field reflect.StructFiel
|
||||
case multipart.FileHeader:
|
||||
return nil
|
||||
}
|
||||
return json.API.Unmarshal(bytesconv.StringToBytes(val), value.Addr().Interface())
|
||||
return unmarshalFieldAsJSON(val, value)
|
||||
case reflect.Map:
|
||||
return json.API.Unmarshal(bytesconv.StringToBytes(val), value.Addr().Interface())
|
||||
return unmarshalFieldAsJSON(val, value)
|
||||
case reflect.Ptr:
|
||||
if !value.Elem().IsValid() {
|
||||
value.Set(reflect.New(value.Type().Elem()))
|
||||
@ -548,3 +548,24 @@ func setFormMap(ptr any, form map[string][]string) error {
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// unmarshalFieldAsJSON tries to unmarshal a form value as JSON. If the initial
|
||||
// unmarshal fails, it retries by wrapping the value in double quotes, treating
|
||||
// the form value as a JSON string. This is necessary because form values are
|
||||
// plain strings (e.g. "2020/09/23 13:20:49") while json.Unmarshal expects
|
||||
// valid JSON input (e.g. quoted strings for string-like values).
|
||||
func unmarshalFieldAsJSON(val string, value reflect.Value) error {
|
||||
b := bytesconv.StringToBytes(val)
|
||||
if err := json.API.Unmarshal(b, value.Addr().Interface()); err != nil {
|
||||
// The raw form value is not valid JSON. Wrap it in quotes so that
|
||||
// types implementing json.Unmarshaler that expect a JSON string
|
||||
// (e.g. custom datetime types) can decode it successfully.
|
||||
quoted := `"` + val + `"`
|
||||
if errRetry := json.API.Unmarshal(bytesconv.StringToBytes(quoted), value.Addr().Interface()); errRetry != nil {
|
||||
// Return the original error — the quoted attempt was only a
|
||||
// best-effort fallback.
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@ -7,6 +7,7 @@ package binding
|
||||
import (
|
||||
"encoding"
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"mime/multipart"
|
||||
"reflect"
|
||||
@ -484,6 +485,50 @@ func TestMappingMapField(t *testing.T) {
|
||||
assert.Equal(t, map[string]int{"one": 1}, s.M)
|
||||
}
|
||||
|
||||
// customDateTime is a custom type that implements json.Unmarshaler.
|
||||
// It expects a JSON-quoted string in "2006/01/02 15:04:05" format.
|
||||
type customDateTime time.Time
|
||||
|
||||
const customDateTimeFormat = "2006/01/02 15:04:05"
|
||||
|
||||
func (t *customDateTime) UnmarshalJSON(data []byte) error {
|
||||
var s string
|
||||
if err := json.Unmarshal(data, &s); err != nil {
|
||||
return err
|
||||
}
|
||||
parsed, err := time.Parse(customDateTimeFormat, s)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
*t = customDateTime(parsed)
|
||||
return nil
|
||||
}
|
||||
|
||||
func TestMappingCustomJSONUnmarshalerStructFromForm(t *testing.T) {
|
||||
var s struct {
|
||||
Time customDateTime `form:"Time"`
|
||||
}
|
||||
|
||||
err := mappingByPtr(&s, formSource{"Time": {"2020/09/23 13:20:49"}}, "form")
|
||||
require.NoError(t, err)
|
||||
|
||||
expected, _ := time.Parse(customDateTimeFormat, "2020/09/23 13:20:49")
|
||||
assert.Equal(t, customDateTime(expected), s.Time)
|
||||
}
|
||||
|
||||
func TestMappingCustomJSONUnmarshalerStructValidJSON(t *testing.T) {
|
||||
// When the form value is already valid JSON, it should still work.
|
||||
var s struct {
|
||||
J struct {
|
||||
I int
|
||||
}
|
||||
}
|
||||
|
||||
err := mappingByPtr(&s, formSource{"J": {`{"I": 9}`}}, "form")
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, 9, s.J.I)
|
||||
}
|
||||
|
||||
func TestMappingIgnoredCircularRef(t *testing.T) {
|
||||
type S struct {
|
||||
S *S `form:"-"`
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user