mirror of
https://github.com/gin-gonic/gin.git
synced 2026-04-29 23:23:18 +08:00
Merge 159db68702aac16693797b76428495d141b3cf84 into d3ffc9985281dcf4d3bef604cce4e662b1a327a6
This commit is contained in:
commit
49f58edd6f
@ -75,6 +75,7 @@ var Validator StructValidator = &defaultValidator{}
|
|||||||
// present in the request to struct instances.
|
// present in the request to struct instances.
|
||||||
var (
|
var (
|
||||||
JSON BindingBody = jsonBinding{}
|
JSON BindingBody = jsonBinding{}
|
||||||
|
JSONStrict BindingBody = jsonStrictBinding{}
|
||||||
XML BindingBody = xmlBinding{}
|
XML BindingBody = xmlBinding{}
|
||||||
Form Binding = formBinding{}
|
Form Binding = formBinding{}
|
||||||
Query Binding = queryBinding{}
|
Query Binding = queryBinding{}
|
||||||
|
|||||||
@ -54,3 +54,32 @@ func decodeJSON(r io.Reader, obj any) error {
|
|||||||
}
|
}
|
||||||
return validate(obj)
|
return validate(obj)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type jsonStrictBinding struct{}
|
||||||
|
|
||||||
|
func (jsonStrictBinding) Name() string {
|
||||||
|
return "json-strict"
|
||||||
|
}
|
||||||
|
|
||||||
|
func (jsonStrictBinding) Bind(req *http.Request, obj any) error {
|
||||||
|
if req == nil || req.Body == nil {
|
||||||
|
return errors.New("invalid request")
|
||||||
|
}
|
||||||
|
return decodeJSONStrict(req.Body, obj)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (jsonStrictBinding) BindBody(body []byte, obj any) error {
|
||||||
|
return decodeJSONStrict(bytes.NewReader(body), obj)
|
||||||
|
}
|
||||||
|
|
||||||
|
func decodeJSONStrict(r io.Reader, obj any) error {
|
||||||
|
decoder := json.API.NewDecoder(r)
|
||||||
|
if EnableDecoderUseNumber {
|
||||||
|
decoder.UseNumber()
|
||||||
|
}
|
||||||
|
decoder.DisallowUnknownFields()
|
||||||
|
if err := decoder.Decode(obj); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return validate(obj)
|
||||||
|
}
|
||||||
|
|||||||
@ -214,3 +214,69 @@ func (tpc timePointerCodec) Decode(ptr unsafe.Pointer, iter *jsoniter.Iterator)
|
|||||||
}
|
}
|
||||||
|
|
||||||
// endregion
|
// endregion
|
||||||
|
|
||||||
|
func TestJSONStrictBindingBindBody(t *testing.T) {
|
||||||
|
t.Run("normal request with known fields", func(t *testing.T) {
|
||||||
|
var s struct {
|
||||||
|
Foo string `json:"foo"`
|
||||||
|
}
|
||||||
|
err := jsonStrictBinding{}.BindBody([]byte(`{"foo": "FOO"}`), &s)
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Equal(t, "FOO", s.Foo)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("request with unknown fields should error", func(t *testing.T) {
|
||||||
|
var s struct {
|
||||||
|
Foo string `json:"foo"`
|
||||||
|
}
|
||||||
|
err := jsonStrictBinding{}.BindBody([]byte(`{"foo": "FOO", "bar": "BAR"}`), &s)
|
||||||
|
require.Error(t, err)
|
||||||
|
assert.Contains(t, err.Error(), "unknown field")
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("empty body should error", func(t *testing.T) {
|
||||||
|
var s struct {
|
||||||
|
Foo string `json:"foo"`
|
||||||
|
}
|
||||||
|
err := jsonStrictBinding{}.BindBody([]byte{}, &s)
|
||||||
|
require.Error(t, err)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("invalid JSON should error", func(t *testing.T) {
|
||||||
|
var s struct {
|
||||||
|
Foo string `json:"foo"`
|
||||||
|
}
|
||||||
|
err := jsonStrictBinding{}.BindBody([]byte(`{"foo": "FOO"`), &s)
|
||||||
|
require.Error(t, err)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("jsonBinding should ignore unknown fields when global switch is off", func(t *testing.T) {
|
||||||
|
oldValue := EnableDecoderDisallowUnknownFields
|
||||||
|
defer func() {
|
||||||
|
EnableDecoderDisallowUnknownFields = oldValue
|
||||||
|
}()
|
||||||
|
EnableDecoderDisallowUnknownFields = false
|
||||||
|
|
||||||
|
var s struct {
|
||||||
|
Foo string `json:"foo"`
|
||||||
|
}
|
||||||
|
err := jsonBinding{}.BindBody([]byte(`{"foo": "FOO", "bar": "BAR"}`), &s)
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Equal(t, "FOO", s.Foo)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("jsonStrictBinding should always reject unknown fields regardless of global switch", func(t *testing.T) {
|
||||||
|
oldValue := EnableDecoderDisallowUnknownFields
|
||||||
|
defer func() {
|
||||||
|
EnableDecoderDisallowUnknownFields = oldValue
|
||||||
|
}()
|
||||||
|
EnableDecoderDisallowUnknownFields = false
|
||||||
|
|
||||||
|
var s struct {
|
||||||
|
Foo string `json:"foo"`
|
||||||
|
}
|
||||||
|
err := jsonStrictBinding{}.BindBody([]byte(`{"foo": "FOO", "bar": "BAR"}`), &s)
|
||||||
|
require.Error(t, err)
|
||||||
|
assert.Contains(t, err.Error(), "unknown field")
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|||||||
14
context.go
14
context.go
@ -764,6 +764,13 @@ func (c *Context) BindJSON(obj any) error {
|
|||||||
return c.MustBindWith(obj, binding.JSON)
|
return c.MustBindWith(obj, binding.JSON)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// BindJSONStrict is a shortcut for c.MustBindWith(obj, binding.JSONStrict).
|
||||||
|
// It will return an error and abort the request with HTTP 400 if any error occurs,
|
||||||
|
// including when the JSON contains unknown fields.
|
||||||
|
func (c *Context) BindJSONStrict(obj any) error {
|
||||||
|
return c.MustBindWith(obj, binding.JSONStrict)
|
||||||
|
}
|
||||||
|
|
||||||
// BindXML is a shortcut for c.MustBindWith(obj, binding.BindXML).
|
// BindXML is a shortcut for c.MustBindWith(obj, binding.BindXML).
|
||||||
func (c *Context) BindXML(obj any) error {
|
func (c *Context) BindXML(obj any) error {
|
||||||
return c.MustBindWith(obj, binding.XML)
|
return c.MustBindWith(obj, binding.XML)
|
||||||
@ -868,6 +875,13 @@ func (c *Context) ShouldBindJSON(obj any) error {
|
|||||||
return c.ShouldBindWith(obj, binding.JSON)
|
return c.ShouldBindWith(obj, binding.JSON)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ShouldBindJSONStrict is a shortcut for c.ShouldBindWith(obj, binding.JSONStrict).
|
||||||
|
// It works like ShouldBindJSON but returns an error if the JSON contains unknown fields.
|
||||||
|
// This method does not set the response status code to 400 or abort if input is not valid.
|
||||||
|
func (c *Context) ShouldBindJSONStrict(obj any) error {
|
||||||
|
return c.ShouldBindWith(obj, binding.JSONStrict)
|
||||||
|
}
|
||||||
|
|
||||||
// ShouldBindXML is a shortcut for c.ShouldBindWith(obj, binding.XML).
|
// ShouldBindXML is a shortcut for c.ShouldBindWith(obj, binding.XML).
|
||||||
// It works like ShouldBindJSON but binds the request body as XML data.
|
// It works like ShouldBindJSON but binds the request body as XML data.
|
||||||
func (c *Context) ShouldBindXML(obj any) error {
|
func (c *Context) ShouldBindXML(obj any) error {
|
||||||
|
|||||||
161
context_test.go
161
context_test.go
@ -3808,3 +3808,164 @@ func BenchmarkGetMapFromFormData(b *testing.B) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestContextBindJSONStrict(t *testing.T) {
|
||||||
|
t.Run("normal request with known fields should succeed", func(t *testing.T) {
|
||||||
|
w := httptest.NewRecorder()
|
||||||
|
c, _ := CreateTestContext(w)
|
||||||
|
|
||||||
|
c.Request, _ = http.NewRequest(http.MethodPost, "/", strings.NewReader(`{"foo":"bar", "bar":"foo"}`))
|
||||||
|
c.Request.Header.Add("Content-Type", MIMEJSON)
|
||||||
|
|
||||||
|
var obj struct {
|
||||||
|
Foo string `json:"foo"`
|
||||||
|
Bar string `json:"bar"`
|
||||||
|
}
|
||||||
|
require.NoError(t, c.BindJSONStrict(&obj))
|
||||||
|
assert.Equal(t, "foo", obj.Bar)
|
||||||
|
assert.Equal(t, "bar", obj.Foo)
|
||||||
|
assert.Equal(t, 0, w.Body.Len())
|
||||||
|
assert.False(t, c.IsAborted())
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("request with unknown fields should return 400 and abort", func(t *testing.T) {
|
||||||
|
w := httptest.NewRecorder()
|
||||||
|
c, _ := CreateTestContext(w)
|
||||||
|
|
||||||
|
c.Request, _ = http.NewRequest(http.MethodPost, "/", strings.NewReader(`{"foo":"bar", "unknown":"field"}`))
|
||||||
|
c.Request.Header.Add("Content-Type", MIMEJSON)
|
||||||
|
|
||||||
|
var obj struct {
|
||||||
|
Foo string `json:"foo"`
|
||||||
|
}
|
||||||
|
require.Error(t, c.BindJSONStrict(&obj))
|
||||||
|
assert.Contains(t, c.Errors.Last().Err.Error(), "unknown field")
|
||||||
|
assert.Equal(t, http.StatusBadRequest, w.Code)
|
||||||
|
assert.True(t, c.IsAborted())
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("invalid JSON should return 400 and abort", func(t *testing.T) {
|
||||||
|
w := httptest.NewRecorder()
|
||||||
|
c, _ := CreateTestContext(w)
|
||||||
|
|
||||||
|
c.Request, _ = http.NewRequest(http.MethodPost, "/", strings.NewReader(`{"foo":"bar"`))
|
||||||
|
c.Request.Header.Add("Content-Type", MIMEJSON)
|
||||||
|
|
||||||
|
var obj struct {
|
||||||
|
Foo string `json:"foo"`
|
||||||
|
}
|
||||||
|
require.Error(t, c.BindJSONStrict(&obj))
|
||||||
|
assert.Equal(t, http.StatusBadRequest, w.Code)
|
||||||
|
assert.True(t, c.IsAborted())
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("empty body should return 400 and abort", func(t *testing.T) {
|
||||||
|
w := httptest.NewRecorder()
|
||||||
|
c, _ := CreateTestContext(w)
|
||||||
|
|
||||||
|
c.Request, _ = http.NewRequest(http.MethodPost, "/", strings.NewReader(""))
|
||||||
|
c.Request.Header.Add("Content-Type", MIMEJSON)
|
||||||
|
|
||||||
|
var obj struct {
|
||||||
|
Foo string `json:"foo"`
|
||||||
|
}
|
||||||
|
require.Error(t, c.BindJSONStrict(&obj))
|
||||||
|
assert.Equal(t, http.StatusBadRequest, w.Code)
|
||||||
|
assert.True(t, c.IsAborted())
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestContextShouldBindJSONStrict(t *testing.T) {
|
||||||
|
t.Run("normal request with known fields should succeed", func(t *testing.T) {
|
||||||
|
w := httptest.NewRecorder()
|
||||||
|
c, _ := CreateTestContext(w)
|
||||||
|
|
||||||
|
c.Request, _ = http.NewRequest(http.MethodPost, "/", strings.NewReader(`{"foo":"bar", "bar":"foo"}`))
|
||||||
|
c.Request.Header.Add("Content-Type", MIMEJSON)
|
||||||
|
|
||||||
|
var obj struct {
|
||||||
|
Foo string `json:"foo"`
|
||||||
|
Bar string `json:"bar"`
|
||||||
|
}
|
||||||
|
require.NoError(t, c.ShouldBindJSONStrict(&obj))
|
||||||
|
assert.Equal(t, "foo", obj.Bar)
|
||||||
|
assert.Equal(t, "bar", obj.Foo)
|
||||||
|
assert.Equal(t, 0, w.Body.Len())
|
||||||
|
assert.False(t, c.IsAborted())
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("request with unknown fields should return error but not abort", func(t *testing.T) {
|
||||||
|
w := httptest.NewRecorder()
|
||||||
|
c, _ := CreateTestContext(w)
|
||||||
|
|
||||||
|
c.Request, _ = http.NewRequest(http.MethodPost, "/", strings.NewReader(`{"foo":"bar", "unknown":"field"}`))
|
||||||
|
c.Request.Header.Add("Content-Type", MIMEJSON)
|
||||||
|
|
||||||
|
var obj struct {
|
||||||
|
Foo string `json:"foo"`
|
||||||
|
}
|
||||||
|
err := c.ShouldBindJSONStrict(&obj)
|
||||||
|
require.Error(t, err)
|
||||||
|
assert.Contains(t, err.Error(), "unknown field")
|
||||||
|
assert.False(t, c.IsAborted())
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("invalid JSON should return error but not abort", func(t *testing.T) {
|
||||||
|
w := httptest.NewRecorder()
|
||||||
|
c, _ := CreateTestContext(w)
|
||||||
|
|
||||||
|
c.Request, _ = http.NewRequest(http.MethodPost, "/", strings.NewReader(`{"foo":"bar"`))
|
||||||
|
c.Request.Header.Add("Content-Type", MIMEJSON)
|
||||||
|
|
||||||
|
var obj struct {
|
||||||
|
Foo string `json:"foo"`
|
||||||
|
}
|
||||||
|
err := c.ShouldBindJSONStrict(&obj)
|
||||||
|
require.Error(t, err)
|
||||||
|
assert.False(t, c.IsAborted())
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestContextJSONBindingIndependence(t *testing.T) {
|
||||||
|
t.Run("BindJSON should ignore unknown fields when global switch is off", func(t *testing.T) {
|
||||||
|
oldValue := binding.EnableDecoderDisallowUnknownFields
|
||||||
|
defer func() {
|
||||||
|
binding.EnableDecoderDisallowUnknownFields = oldValue
|
||||||
|
}()
|
||||||
|
binding.EnableDecoderDisallowUnknownFields = false
|
||||||
|
|
||||||
|
w := httptest.NewRecorder()
|
||||||
|
c, _ := CreateTestContext(w)
|
||||||
|
|
||||||
|
c.Request, _ = http.NewRequest(http.MethodPost, "/", strings.NewReader(`{"foo":"bar", "unknown":"field"}`))
|
||||||
|
c.Request.Header.Add("Content-Type", MIMEJSON)
|
||||||
|
|
||||||
|
var obj struct {
|
||||||
|
Foo string `json:"foo"`
|
||||||
|
}
|
||||||
|
require.NoError(t, c.BindJSON(&obj))
|
||||||
|
assert.Equal(t, "bar", obj.Foo)
|
||||||
|
assert.False(t, c.IsAborted())
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("BindJSONStrict should always reject unknown fields regardless of global switch", func(t *testing.T) {
|
||||||
|
oldValue := binding.EnableDecoderDisallowUnknownFields
|
||||||
|
defer func() {
|
||||||
|
binding.EnableDecoderDisallowUnknownFields = oldValue
|
||||||
|
}()
|
||||||
|
binding.EnableDecoderDisallowUnknownFields = false
|
||||||
|
|
||||||
|
w := httptest.NewRecorder()
|
||||||
|
c, _ := CreateTestContext(w)
|
||||||
|
|
||||||
|
c.Request, _ = http.NewRequest(http.MethodPost, "/", strings.NewReader(`{"foo":"bar", "unknown":"field"}`))
|
||||||
|
c.Request.Header.Add("Content-Type", MIMEJSON)
|
||||||
|
|
||||||
|
var obj struct {
|
||||||
|
Foo string `json:"foo"`
|
||||||
|
}
|
||||||
|
require.Error(t, c.BindJSONStrict(&obj))
|
||||||
|
assert.Contains(t, c.Errors.Last().Err.Error(), "unknown field")
|
||||||
|
assert.True(t, c.IsAborted())
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user