Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions binding/binding.go
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,7 @@ var Validator StructValidator = &defaultValidator{}
// present in the request to struct instances.
var (
JSON BindingBody = jsonBinding{}
JSONStrict BindingBody = jsonStrictBinding{}
XML BindingBody = xmlBinding{}
Form Binding = formBinding{}
Query Binding = queryBinding{}
Expand Down
29 changes: 29 additions & 0 deletions binding/json.go
Original file line number Diff line number Diff line change
Expand Up @@ -54,3 +54,32 @@ func decodeJSON(r io.Reader, obj any) error {
}
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)
}
66 changes: 66 additions & 0 deletions binding/json_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -214,3 +214,69 @@ func (tpc timePointerCodec) Decode(ptr unsafe.Pointer, iter *jsoniter.Iterator)
}

// 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 changes: 14 additions & 0 deletions context.go
Original file line number Diff line number Diff line change
Expand Up @@ -764,6 +764,13 @@
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)

Check failure on line 771 in context.go

View workflow job for this annotation

GitHub Actions / ubuntu-latest @ Go 1.25 -tags nomsgpack

undefined: binding.JSONStrict

Check failure on line 771 in context.go

View workflow job for this annotation

GitHub Actions / ubuntu-latest @ Go 1.26 -tags nomsgpack

undefined: binding.JSONStrict
}

// BindXML is a shortcut for c.MustBindWith(obj, binding.BindXML).
func (c *Context) BindXML(obj any) error {
return c.MustBindWith(obj, binding.XML)
Expand Down Expand Up @@ -868,6 +875,13 @@
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)

Check failure on line 882 in context.go

View workflow job for this annotation

GitHub Actions / ubuntu-latest @ Go 1.25 -tags nomsgpack

undefined: binding.JSONStrict

Check failure on line 882 in context.go

View workflow job for this annotation

GitHub Actions / ubuntu-latest @ Go 1.26 -tags nomsgpack

undefined: binding.JSONStrict
}

// ShouldBindXML is a shortcut for c.ShouldBindWith(obj, binding.XML).
// It works like ShouldBindJSON but binds the request body as XML data.
func (c *Context) ShouldBindXML(obj any) error {
Expand Down
161 changes: 161 additions & 0 deletions context_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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