Repository: swaggest/rest
Pull request #4 (2020-11-20 20:53:29)

Allow custom JSON decoder for request body

codecov[bot]: # [Codecov](https://codecov.io/gh/swaggest/rest/pull/4?src=pr&el=h1) Report
> Merging [#4](https://codecov.io/gh/swaggest/rest/pull/4?src=pr&el=desc) (99ae7d3) into [master](https://codecov.io/gh/swaggest/rest/commit/f3de013fd6b77515127e76e71786203424a83d5c?el=desc) (f3de013) will **decrease** coverage by `0.07%`.
> The diff coverage is `75.00%`.

[![Impacted file tree graph](https://codecov.io/gh/swaggest/rest/pull/4/graphs/tree.svg?width=650&height=150&src=pr&token=OpLHxxPdmn)](https://codecov.io/gh/swaggest/rest/pull/4?src=pr&el=tree)

```diff
@@            Coverage Diff             @@
##           master       #4      +/-   ##
==========================================
- Coverage   69.11%   69.03%   -0.08%     
==========================================
  Files          25       25              
  Lines        1308     1321      +13     
==========================================
+ Hits          904      912       +8     
- Misses        307      310       +3     
- Partials       97       99       +2     
```

| Flag | Coverage Δ | |
|---|---|---|
| unittests | `69.03% <75.00%> (-0.08%)` | :arrow_down: |

Flags with carried forward coverage won't be shown. [Click here](https://docs.codecov.io/docs/carryforward-flags#carryforward-flags-in-the-pull-request-comment) to find out more.

| [Impacted Files](https://codecov.io/gh/swaggest/rest/pull/4?src=pr&el=tree) | Coverage Δ | |
|---|---|---|
| [jsonschema/validator.go](https://codecov.io/gh/swaggest/rest/pull/4/diff?src=pr&el=tree#diff-anNvbnNjaGVtYS92YWxpZGF0b3IuZ28=) | `59.55% <0.00%> (-0.68%)` | :arrow_down: |
| [openapi/collector.go](https://codecov.io/gh/swaggest/rest/pull/4/diff?src=pr&el=tree#diff-b3BlbmFwaS9jb2xsZWN0b3IuZ28=) | `41.34% <0.00%> (ø)` | |
| [validator.go](https://codecov.io/gh/swaggest/rest/pull/4/diff?src=pr&el=tree#diff-dmFsaWRhdG9yLmdv) | `0.00% <0.00%> (ø)` | |
| [request/factory.go](https://codecov.io/gh/swaggest/rest/pull/4/diff?src=pr&el=tree#diff-cmVxdWVzdC9mYWN0b3J5Lmdv) | `85.91% <25.00%> (-3.80%)` | :arrow_down: |
| [request/jsonbody.go](https://codecov.io/gh/swaggest/rest/pull/4/diff?src=pr&el=tree#diff-cmVxdWVzdC9qc29uYm9keS5nbw==) | `100.00% <100.00%> (ø)` | |

------

[Continue to review full report at Codecov](https://codecov.io/gh/swaggest/rest/pull/4?src=pr&el=continue).
> **Legend** - [Click here to learn more](https://docs.codecov.io/docs/codecov-delta)
> `Δ = absolute <relative> (impact)`, `ø = not affected`, `? = missing data`
> Powered by [Codecov](https://codecov.io/gh/swaggest/rest/pull/4?src=pr&el=footer). Last update [f3de013...99ae7d3](https://codecov.io/gh/swaggest/rest/pull/4?src=pr&el=lastupdated). Read the [comment docs](https://docs.codecov.io/docs/pull-request-comments).


Patch:
diff --git a/.github/workflows/bench.yml b/.github/workflows/bench.yml
index 259d8c9..31b117d 100644
--- a/.github/workflows/bench.yml
+++ b/.github/workflows/bench.yml
@@ -9,7 +9,6 @@ on:
   pull_request:
 env:
   GO111MODULE: "on"
-
 jobs:
   bench:
     strategy:
diff --git a/_examples/go.mod b/_examples/go.mod
index d64c642..4b841e9 100644
--- a/_examples/go.mod
+++ b/_examples/go.mod
@@ -6,15 +6,15 @@ replace github.com/swaggest/rest => ../
 
 require (
 	github.com/andybalholm/brotli v1.0.1 // indirect
-	github.com/bool64/dev v0.1.6
+	github.com/bool64/dev v0.1.7
 	github.com/bool64/httptestbench v0.1.0
 	github.com/go-chi/chi v4.1.2+incompatible
 	github.com/kelseyhightower/envconfig v1.4.0
 	github.com/klauspost/compress v1.11.1 // indirect
 	github.com/stretchr/testify v1.6.1
-	github.com/swaggest/assertjson v1.3.0
-	github.com/swaggest/jsonschema-go v0.3.11
-	github.com/swaggest/openapi-go v0.2.3
+	github.com/swaggest/assertjson v1.4.0
+	github.com/swaggest/jsonschema-go v0.3.12
+	github.com/swaggest/openapi-go v0.2.4
 	github.com/swaggest/rest v0.0.0-20201021182948-25d31f7f38d4
 	github.com/swaggest/swgui v1.0.9
 	github.com/swaggest/usecase v0.0.0-20200928062416-27f47131b0f8
diff --git a/_examples/go.sum b/_examples/go.sum
index c63ffba..3b0a44f 100644
--- a/_examples/go.sum
+++ b/_examples/go.sum
@@ -7,6 +7,8 @@ github.com/bool64/dev v0.1.3/go.mod h1:pn52JC52uSgpazChx9CeXyG+S3sW2V36HHoLNBbsc
 github.com/bool64/dev v0.1.5/go.mod h1:pn52JC52uSgpazChx9CeXyG+S3sW2V36HHoLNBbscdg=
 github.com/bool64/dev v0.1.6 h1:QP8buknknKrPVizPbOKeyCt6/2+iZ309wxjreckowVY=
 github.com/bool64/dev v0.1.6/go.mod h1:pn52JC52uSgpazChx9CeXyG+S3sW2V36HHoLNBbscdg=
+github.com/bool64/dev v0.1.7 h1:1xs4ff6lUFIt7euQCmfjU0Xi7IiSmwcQyMokKdq6qAM=
+github.com/bool64/dev v0.1.7/go.mod h1:pn52JC52uSgpazChx9CeXyG+S3sW2V36HHoLNBbscdg=
 github.com/bool64/httptestbench v0.1.0 h1:Nr+9KjZbrlgmd8ABGfFvEYEnMdbAWxj13YHeQR6OOzA=
 github.com/bool64/httptestbench v0.1.0/go.mod h1:xPlusBpnquKisPwqjpgK8fBPwa0q1r+ecFoWxRtNjgU=
 github.com/cespare/xxhash/v2 v2.1.1 h1:6MnRN8NT7+YBpUIWxHtefFZOKTAPgGjpQSxqLNn0+qY=
@@ -75,6 +77,8 @@ github.com/swaggest/assertjson v1.0.0/go.mod h1:mE5ltBbrB+Ya8Xar5OMITxya76vwLZMo
 github.com/swaggest/assertjson v1.2.0/go.mod h1:VR1/jzZHtROewu/SBEdFrBOBuNm1FO9LDjvP9fLf6+I=
 github.com/swaggest/assertjson v1.3.0 h1:751J+Plmg68cDGeMwMIq5nEvBw8g+ErG4UMcv9kpHmk=
 github.com/swaggest/assertjson v1.3.0/go.mod h1:VR1/jzZHtROewu/SBEdFrBOBuNm1FO9LDjvP9fLf6+I=
+github.com/swaggest/assertjson v1.4.0 h1:WPHYWa102jB8BnO76+86IquNH5e3wllC60TsUT4Iq4U=
+github.com/swaggest/assertjson v1.4.0/go.mod h1:mJG4OUkBTu+H4jT2oSQ1Y1DrRSu81PtSJ6bTR6e8avg=
 github.com/swaggest/form v3.6.4+incompatible h1:K0EaZjf8C7cScuZgnJG+Sg0GUGAtjPZivTv1jN+1ni4=
 github.com/swaggest/form v3.6.4+incompatible/go.mod h1:eTJPpSayjnJlOQQbDYsSHlbREbSAtudNW/Y6HouVWr0=
 github.com/swaggest/jsonschema-go v0.2.1/go.mod h1:QFauBdPTrU1UltwocM5FzOWnVjVVtcWkJWG3NlK9sV0=
@@ -82,11 +86,15 @@ github.com/swaggest/jsonschema-go v0.2.4/go.mod h1:m4VV88Gbi7lCrt9ckJzigK1rMlEeF
 github.com/swaggest/jsonschema-go v0.3.8/go.mod h1:AQijowS82ZcUId9RStqvyGTxyhzYUiVDxelwQxEdOy8=
 github.com/swaggest/jsonschema-go v0.3.11 h1:rbYBJhAV2Uz1PB4iJyKbvzGO+o1FsdX60aLi+o67IHU=
 github.com/swaggest/jsonschema-go v0.3.11/go.mod h1:lJUbAc2E3OpGrrUGvayONeRBXsFJeedWTpm23jhOtKs=
+github.com/swaggest/jsonschema-go v0.3.12 h1:KgzIVv3OYCcwjUBdkYLYcQXQ/0q+40cSwZBtHDF+gKY=
+github.com/swaggest/jsonschema-go v0.3.12/go.mod h1:UXDViNdMa0fEpc/QgPu9ac1itOuxgbZyExZI6pg024A=
 github.com/swaggest/openapi-go v0.1.3/go.mod h1:Zx4ZgJ7XvlFH9wCOHE7u8RAjLfiHAnCHeaD5kUDujVM=
 github.com/swaggest/openapi-go v0.2.2 h1:ZV6tjpR/W97sIOknkaOAt/U2FpqoEptRZrYiksfNKkY=
 github.com/swaggest/openapi-go v0.2.2/go.mod h1:GyvI08XD7gnTZxFteI2/Rnk6hgmcSufqG8wVBz/yzqo=
 github.com/swaggest/openapi-go v0.2.3 h1:DsbY56dzJQzKLjoJKl15WtykoyF1OJrqIzUUH+ovTDg=
 github.com/swaggest/openapi-go v0.2.3/go.mod h1:R4cj4xlxiODL8m+x0pKbWXRZYx67UTYOx0xfH0zxm3k=
+github.com/swaggest/openapi-go v0.2.4 h1:7OkZxIJ6PGH9n4ULguE87utNxcEQqa4/2KEmCmCf/rI=
+github.com/swaggest/openapi-go v0.2.4/go.mod h1:+PRHGYsaroCoINZDx/m+fxlKaRp+Mwu6QlG4VbloKtI=
 github.com/swaggest/refl v0.1.0/go.mod h1:kmYWhxNEvjfRDdMRqpaR/vLULk/SotJs9HFUCIVMK8o=
 github.com/swaggest/refl v0.1.2/go.mod h1:kmYWhxNEvjfRDdMRqpaR/vLULk/SotJs9HFUCIVMK8o=
 github.com/swaggest/refl v0.1.3/go.mod h1:kmYWhxNEvjfRDdMRqpaR/vLULk/SotJs9HFUCIVMK8o=
diff --git a/go.mod b/go.mod
index e11ea27..5b7d592 100644
--- a/go.mod
+++ b/go.mod
@@ -3,15 +3,14 @@ module github.com/swaggest/rest
 go 1.13
 
 require (
-	github.com/bool64/dev v0.1.6
+	github.com/bool64/dev v0.1.7
 	github.com/cespare/xxhash/v2 v2.1.1
 	github.com/go-chi/chi v4.1.2+incompatible
-	github.com/iancoleman/orderedmap v0.1.0 // indirect
 	github.com/santhosh-tekuri/jsonschema/v2 v2.2.0
 	github.com/stretchr/testify v1.6.1
-	github.com/swaggest/assertjson v1.3.0
+	github.com/swaggest/assertjson v1.4.0
 	github.com/swaggest/form v3.6.4+incompatible
-	github.com/swaggest/openapi-go v0.2.3
+	github.com/swaggest/openapi-go v0.2.4
 	github.com/swaggest/refl v0.1.5
 	github.com/swaggest/usecase v0.0.0-20200928062416-27f47131b0f8
 	golang.org/x/net v0.0.0-20200822124328-c89045814202 // indirect
diff --git a/go.sum b/go.sum
index 6504622..69d8821 100644
--- a/go.sum
+++ b/go.sum
@@ -1,8 +1,9 @@
 github.com/bool64/dev v0.1.0/go.mod h1:pn52JC52uSgpazChx9CeXyG+S3sW2V36HHoLNBbscdg=
 github.com/bool64/dev v0.1.2/go.mod h1:pn52JC52uSgpazChx9CeXyG+S3sW2V36HHoLNBbscdg=
-github.com/bool64/dev v0.1.5/go.mod h1:pn52JC52uSgpazChx9CeXyG+S3sW2V36HHoLNBbscdg=
 github.com/bool64/dev v0.1.6 h1:QP8buknknKrPVizPbOKeyCt6/2+iZ309wxjreckowVY=
 github.com/bool64/dev v0.1.6/go.mod h1:pn52JC52uSgpazChx9CeXyG+S3sW2V36HHoLNBbscdg=
+github.com/bool64/dev v0.1.7 h1:1xs4ff6lUFIt7euQCmfjU0Xi7IiSmwcQyMokKdq6qAM=
+github.com/bool64/dev v0.1.7/go.mod h1:pn52JC52uSgpazChx9CeXyG+S3sW2V36HHoLNBbscdg=
 github.com/cespare/xxhash/v2 v2.1.1 h1:6MnRN8NT7+YBpUIWxHtefFZOKTAPgGjpQSxqLNn0+qY=
 github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
 github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
@@ -59,17 +60,17 @@ github.com/stretchr/testify v1.6.1 h1:hDPOHmpOpP40lSULcqw7IrRb/u7w6RpDC9399XyoNd
 github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
 github.com/swaggest/assertjson v1.0.0/go.mod h1:mE5ltBbrB+Ya8Xar5OMITxya76vwLZMoPG8aXF7sVQc=
 github.com/swaggest/assertjson v1.2.0/go.mod h1:VR1/jzZHtROewu/SBEdFrBOBuNm1FO9LDjvP9fLf6+I=
-github.com/swaggest/assertjson v1.3.0 h1:751J+Plmg68cDGeMwMIq5nEvBw8g+ErG4UMcv9kpHmk=
-github.com/swaggest/assertjson v1.3.0/go.mod h1:VR1/jzZHtROewu/SBEdFrBOBuNm1FO9LDjvP9fLf6+I=
+github.com/swaggest/assertjson v1.4.0 h1:WPHYWa102jB8BnO76+86IquNH5e3wllC60TsUT4Iq4U=
+github.com/swaggest/assertjson v1.4.0/go.mod h1:mJG4OUkBTu+H4jT2oSQ1Y1DrRSu81PtSJ6bTR6e8avg=
 github.com/swaggest/form v3.6.4+incompatible h1:K0EaZjf8C7cScuZgnJG+Sg0GUGAtjPZivTv1jN+1ni4=
 github.com/swaggest/form v3.6.4+incompatible/go.mod h1:eTJPpSayjnJlOQQbDYsSHlbREbSAtudNW/Y6HouVWr0=
 github.com/swaggest/jsonschema-go v0.2.1/go.mod h1:QFauBdPTrU1UltwocM5FzOWnVjVVtcWkJWG3NlK9sV0=
 github.com/swaggest/jsonschema-go v0.2.4/go.mod h1:m4VV88Gbi7lCrt9ckJzigK1rMlEeFjdZUkJr1o5MnDE=
-github.com/swaggest/jsonschema-go v0.3.11 h1:rbYBJhAV2Uz1PB4iJyKbvzGO+o1FsdX60aLi+o67IHU=
-github.com/swaggest/jsonschema-go v0.3.11/go.mod h1:lJUbAc2E3OpGrrUGvayONeRBXsFJeedWTpm23jhOtKs=
+github.com/swaggest/jsonschema-go v0.3.12 h1:KgzIVv3OYCcwjUBdkYLYcQXQ/0q+40cSwZBtHDF+gKY=
+github.com/swaggest/jsonschema-go v0.3.12/go.mod h1:UXDViNdMa0fEpc/QgPu9ac1itOuxgbZyExZI6pg024A=
 github.com/swaggest/openapi-go v0.1.3/go.mod h1:Zx4ZgJ7XvlFH9wCOHE7u8RAjLfiHAnCHeaD5kUDujVM=
-github.com/swaggest/openapi-go v0.2.3 h1:DsbY56dzJQzKLjoJKl15WtykoyF1OJrqIzUUH+ovTDg=
-github.com/swaggest/openapi-go v0.2.3/go.mod h1:R4cj4xlxiODL8m+x0pKbWXRZYx67UTYOx0xfH0zxm3k=
+github.com/swaggest/openapi-go v0.2.4 h1:7OkZxIJ6PGH9n4ULguE87utNxcEQqa4/2KEmCmCf/rI=
+github.com/swaggest/openapi-go v0.2.4/go.mod h1:+PRHGYsaroCoINZDx/m+fxlKaRp+Mwu6QlG4VbloKtI=
 github.com/swaggest/refl v0.1.0/go.mod h1:kmYWhxNEvjfRDdMRqpaR/vLULk/SotJs9HFUCIVMK8o=
 github.com/swaggest/refl v0.1.2/go.mod h1:kmYWhxNEvjfRDdMRqpaR/vLULk/SotJs9HFUCIVMK8o=
 github.com/swaggest/refl v0.1.3/go.mod h1:kmYWhxNEvjfRDdMRqpaR/vLULk/SotJs9HFUCIVMK8o=
diff --git a/jsonschema/validator.go b/jsonschema/validator.go
index fd2100f..43ab9a5 100644
--- a/jsonschema/validator.go
+++ b/jsonschema/validator.go
@@ -167,6 +167,11 @@ func (v *Validator) ValidateJSONBody(jsonBody []byte) error {
 	return errs
 }
 
+// HasConstraints indicates if there are validation rules for parameter location.
+func (v *Validator) HasConstraints(in rest.ParamIn) bool {
+	return len(v.inNamedSchemas[in]) > 0
+}
+
 // ValidateData performs validation of a mapped request data.
 func (v *Validator) ValidateData(in rest.ParamIn, namedData map[string]interface{}) error {
 	var errs rest.ValidationErrors
diff --git a/openapi/collector.go b/openapi/collector.go
index 4f80d06..4627cc7 100644
--- a/openapi/collector.go
+++ b/openapi/collector.go
@@ -240,7 +240,7 @@ func (c *Collector) provideParametersJSONSchemas(op openapi3.Operation, validato
 			schemaData []byte
 		)
 
-		if !schema.IsTrivial() {
+		if !schema.IsTrivial(c.Reflector().ResolveJSONSchemaRef) {
 			schemaData, err = schema.JSONSchemaBytes()
 			if err != nil {
 				return fmt.Errorf("failed to build JSON Schema for parameter (%s, %s)", pp.In, pp.Name)
@@ -301,7 +301,7 @@ func (c *Collector) ProvideRequestJSONSchemas(
 			}
 
 			schema := content.Schema.ToJSONSchema(c.Reflector().Spec)
-			if schema.IsTrivial() {
+			if schema.IsTrivial(c.Reflector().ResolveJSONSchemaRef) {
 				continue
 			}
 
@@ -334,7 +334,7 @@ func (c *Collector) provideHeaderSchemas(resp *openapi3.Response, validator rest
 			schemaData []byte
 		)
 
-		if !schema.IsTrivial() {
+		if !schema.IsTrivial(c.Reflector().ResolveJSONSchemaRef) {
 			schemaData, err = schema.JSONSchemaBytes()
 			if err != nil {
 				return fmt.Errorf("failed to build JSON Schema for response header (%s)", name)
@@ -389,7 +389,7 @@ func (c *Collector) ProvideResponseJSONSchemas(
 
 		schema := cont.Schema.ToJSONSchema(c.Reflector().Spec)
 
-		if schema.IsTrivial() {
+		if schema.IsTrivial(c.Reflector().ResolveJSONSchemaRef) {
 			continue
 		}
 
diff --git a/request/factory.go b/request/factory.go
index 4972b16..3c6cc0c 100644
--- a/request/factory.go
+++ b/request/factory.go
@@ -1,6 +1,7 @@
 package request
 
 import (
+	"io"
 	"net/http"
 	"net/url"
 	"reflect"
@@ -28,6 +29,10 @@ const (
 type DecoderFactory struct {
 	ApplyDefaults bool
 
+	// JSONReader allows custom JSON decoder for request body.
+	// If not set encoding/json.Decoder is used.
+	JSONReader func(rd io.Reader, v interface{}) error
+
 	formDecoders      map[rest.ParamIn]*form.Decoder
 	decoderFunctions  map[rest.ParamIn]decoderFunc
 	defaultValDecoder *form.Decoder
@@ -120,7 +125,12 @@ func (df *DecoderFactory) MakeDecoder(
 
 	// Checking for body tags.
 	if refl.HasTaggedFields(input, jsonTag) || refl.FindEmbeddedSliceOrMap(input) != nil {
-		m.decoders = append(m.decoders, decodeJSONBody)
+		if df.JSONReader != nil {
+			m.decoders = append(m.decoders, decodeJSONBody(df.JSONReader))
+		} else {
+			m.decoders = append(m.decoders, decodeJSONBody(readJSON))
+		}
+
 		m.in = append(m.in, rest.ParamInBody)
 	}
 
diff --git a/request/jsonbody.go b/request/jsonbody.go
index a9794b2..d17d5b4 100644
--- a/request/jsonbody.go
+++ b/request/jsonbody.go
@@ -17,37 +17,52 @@ var bufPool = sync.Pool{
 	},
 }
 
-func decodeJSONBody(r *http.Request, input interface{}, validator rest.Validator) error {
-	if r.ContentLength == 0 {
-		return ErrMissingRequestBody
-	}
+func readJSON(rd io.Reader, v interface{}) error {
+	d := json.NewDecoder(rd)
+
+	return d.Decode(v)
+}
 
-	contentType := r.Header.Get("Content-Type")
-	if contentType != "" {
-		if len(contentType) < 16 || contentType[0:16] != "application/json" { // allow 'application/json;charset=UTF-8'
-			return fmt.Errorf("%w, received: %s", ErrJSONExpected, contentType)
+func decodeJSONBody(readJSON func(rd io.Reader, v interface{}) error) valueDecoderFunc {
+	return func(r *http.Request, input interface{}, validator rest.Validator) error {
+		if r.ContentLength == 0 {
+			return ErrMissingRequestBody
 		}
-	}
 
-	b := bufPool.Get().(*bytes.Buffer) // nolint:errcheck // bufPool is configured to provide *bytes.Buffer.
-	defer bufPool.Put(b)
+		contentType := r.Header.Get("Content-Type")
+		if contentType != "" {
+			if len(contentType) < 16 || contentType[0:16] != "application/json" { // allow 'application/json;charset=UTF-8'
+				return fmt.Errorf("%w, received: %s", ErrJSONExpected, contentType)
+			}
+		}
 
-	b.Reset()
+		var (
+			rd io.Reader = r.Body
+			b  *bytes.Buffer
+		)
 
-	rd := io.TeeReader(r.Body, b)
-	d := json.NewDecoder(rd)
+		validate := validator != nil && validator.HasConstraints(rest.ParamInBody)
 
-	err := d.Decode(&input)
-	if err != nil {
-		return fmt.Errorf("failed to decode json: %w", err)
-	}
+		if validate {
+			b = bufPool.Get().(*bytes.Buffer) // nolint:errcheck // bufPool is configured to provide *bytes.Buffer.
+			defer bufPool.Put(b)
+
+			b.Reset()
+			rd = io.TeeReader(r.Body, b)
+		}
 
-	if validator != nil {
-		err = validator.ValidateJSONBody(b.Bytes())
+		err := readJSON(rd, &input)
 		if err != nil {
-			return err
+			return fmt.Errorf("failed to decode json: %w", err)
 		}
-	}
 
-	return nil
+		if validator != nil && validate {
+			err = validator.ValidateJSONBody(b.Bytes())
+			if err != nil {
+				return err
+			}
+		}
+
+		return nil
+	}
 }
diff --git a/validator.go b/validator.go
index 2015bcc..797ebda 100644
--- a/validator.go
+++ b/validator.go
@@ -9,6 +9,9 @@ type Validator interface {
 
 	// ValidateData validates JSON encoded body and returns error in case of invalid data.
 	ValidateJSONBody(jsonBody []byte) error
+
+	// HasConstraints indicates if there are validation rules for parameter location.
+	HasConstraints(in ParamIn) bool
 }
 
 // ValidatorFunc implements Validator with a func.
@@ -19,6 +22,11 @@ func (v ValidatorFunc) ValidateData(in ParamIn, namedData map[string]interface{}
 	return v(in, namedData)
 }
 
+// HasConstraints indicates if there are validation rules for parameter location.
+func (v ValidatorFunc) HasConstraints(_ ParamIn) bool {
+	return true
+}
+
 // ValidateJSONBody implements Validator.
 func (v ValidatorFunc) ValidateJSONBody(body []byte) error {
 	return v(ParamInBody, map[string]interface{}{"body": json.RawMessage(body)})


Test patch:
diff --git a/_examples/advanced/json_body_test.go b/_examples/advanced/json_body_test.go
index a623c51..4208167 100644
--- a/_examples/advanced/json_body_test.go
+++ b/_examples/advanced/json_body_test.go
@@ -10,6 +10,7 @@ import (
 )
 
 // Benchmark_jsonBody-4   	   29671	     37417 ns/op	       194 B:rcvd/op	       181 B:sent/op	     26705 rps	    6068 B/op	      58 allocs/op.
+// Benchmark_jsonBody-4   	   29749	     35934 ns/op	       194 B:rcvd/op	       181 B:sent/op	     27829 rps	    6063 B/op	      57 allocs/op.
 func Benchmark_jsonBody(b *testing.B) {
 	r := NewRouter()
 
diff --git a/request/jsonbody_test.go b/request/jsonbody_test.go
index 5e1943f..9296195 100644
--- a/request/jsonbody_test.go
+++ b/request/jsonbody_test.go
@@ -25,7 +25,7 @@ func Test_decodeJSONBody(t *testing.T) {
 	}
 
 	i := Input{}
-	assert.NoError(t, decodeJSONBody(createReq, &i, nil))
+	assert.NoError(t, decodeJSONBody(readJSON)(createReq, &i, nil))
 	assert.Equal(t, 123, i.Amount)
 	assert.Equal(t, "248df4b7-aa70-47b8-a036-33ac447e668d", i.CustomerID)
 	assert.Equal(t, "withdraw", i.Type)
@@ -37,7 +37,7 @@ func Test_decodeJSONBody(t *testing.T) {
 	i = Input{}
 	_, err = createBody.Seek(0, io.SeekStart)
 	assert.NoError(t, err)
-	assert.NoError(t, decodeJSONBody(createReq, &i, vl))
+	assert.NoError(t, decodeJSONBody(readJSON)(createReq, &i, vl))
 	assert.Equal(t, 123, i.Amount)
 	assert.Equal(t, "248df4b7-aa70-47b8-a036-33ac447e668d", i.CustomerID)
 	assert.Equal(t, "withdraw", i.Type)
@@ -49,7 +49,7 @@ func Test_decodeJSONBody_emptyBody(t *testing.T) {
 
 	var i []int
 
-	err = decodeJSONBody(req, &i, nil)
+	err = decodeJSONBody(readJSON)(req, &i, nil)
 	assert.EqualError(t, err, "missing request body")
 }
 
@@ -60,7 +60,7 @@ func Test_decodeJSONBody_badContentType(t *testing.T) {
 
 	var i []int
 
-	err = decodeJSONBody(req, &i, nil)
+	err = decodeJSONBody(readJSON)(req, &i, nil)
 	assert.EqualError(t, err, "request with application/json content type expected, received: text/plain")
 }
 
@@ -70,7 +70,7 @@ func Test_decodeJSONBody_decodeFailed(t *testing.T) {
 
 	var i []int
 
-	err = decodeJSONBody(req, &i, nil)
+	err = decodeJSONBody(readJSON)(req, &i, nil)
 	assert.Error(t, err)
 }
 
@@ -80,7 +80,7 @@ func Test_decodeJSONBody_unmarshalFailed(t *testing.T) {
 
 	var i []int
 
-	err = decodeJSONBody(req, &i, nil)
+	err = decodeJSONBody(readJSON)(req, &i, nil)
 	assert.EqualError(t, err, "failed to decode json: json: cannot unmarshal number into Go value of type []int")
 }
 
@@ -94,6 +94,6 @@ func Test_decodeJSONBody_validateFailed(t *testing.T) {
 		return errors.New("failed")
 	})
 
-	err = decodeJSONBody(req, &i, vl)
+	err = decodeJSONBody(readJSON)(req, &i, vl)
 	assert.EqualError(t, err, "failed")
 }