Repository: swaggest/rest (go)

Issue:
Setting additionalProperties=false in all validations
It's a very common issue that someone might have a typo on an optional API endpoint's `query`/`json body object property`/`url-encoded form field`, and since it doesn't match the correct one, it's ignored but doesn't necessarily need to fail in a noticeable way, therefore causing a silent error.

How can I set this?

I have these additional questions regarding validation:

- In general, it's not clear to me how to set a validation except when it's a tag on a struct field (e.g. in this case this validation is not bound to a field).
- Within struct fields, how to set a format validation when the field is a list or a map, e.g. how to define that each element on a slice must be an enum of certain strings.

Pull request:
Add control to forbid unknown params
Resolves #68.

Unknown request parameters (not only in body) can be now forbidden with struct tags

```go
	type helloInput struct {
		Locale string `query:"locale" default:"en-US" pattern:"^[a-z]{2}-[A-Z]{2}$" enum:"ru-RU,en-US"`
		Name   string `path:"name" minLength:"3"` // Field tags define parameter location and JSON schema constraints.

		// Field tags of unnamed fields are applied to parent schema.
		// they are optional and can be used to disallow unknown parameters.
		// For non-body params, name tag must be provided explicitly.
		// E.g. here no unknown `query` and `cookie` parameters allowed,
		// unknown `header` params are ok.
		_ struct{} `query:"_" cookie:"_" additionalProperties:"false"`
	}
```

or with an global interceptor

```go
	// An example of global schema override to disable additionalProperties for all object schemas.
	s.OpenAPICollector.Reflector().DefaultOptions = append(s.OpenAPICollector.Reflector().DefaultOptions, func(rc *jsonschema.ReflectContext) {
		it := rc.InterceptType
		rc.InterceptType = func(value reflect.Value, schema *jsonschema.Schema) (bool, error) {
			stop, err := it(value, schema)
			if err != nil {
				return stop, err
			}

			// Allow unknown request headers and skip response.
			if oc, ok := openapi3.OperationCtx(rc); !ok ||
				oc.ProcessingResponse || oc.ProcessingIn == string(rest.ParamInHeader) {
				return stop, nil
			}

			if schema.HasType(jsonschema.Object) && len(schema.Properties) > 0 && schema.AdditionalProperties == nil {
				schema.AdditionalProperties = (&jsonschema.SchemaOrBool{}).WithTypeBoolean(false)
			}

			return stop, nil
		}
	})
```

Patch:
diff --git a/.github/workflows/bench-examples.yml b/.github/workflows/bench-examples.yml
index f962928..32242df 100644
--- a/.github/workflows/bench-examples.yml
+++ b/.github/workflows/bench-examples.yml
@@ -15,7 +15,7 @@ env:
   GO111MODULE: "on"
   CACHE_BENCHMARK: "off"    # Enables benchmark result reuse between runs, may skew latency results.
   RUN_BASE_BENCHMARK: "on"  # Runs benchmark for PR base in case benchmark result is missing.
-  GO_VERSION: tip
+  GO_VERSION: 1.18.x
 jobs:
   bench:
     runs-on: ubuntu-latest
diff --git a/README.md b/README.md
index 229a0b1..d4a6f54 100644
--- a/README.md
+++ b/README.md
@@ -52,9 +52,15 @@ Request decoder populates field values from `http.Request` data before use case
 ```go
 // Declare input port type.
 type helloInput struct {
-    Locale string `query:"locale" default:"en-US" pattern:"^[a-z]{2}-[A-Z]{2}$"`
+    Locale string `query:"locale" default:"en-US" pattern:"^[a-z]{2}-[A-Z]{2}$" enum:"ru-RU,en-US"`
     Name   string `path:"name" minLength:"3"` // Field tags define parameter location and JSON schema constraints.
-	_      string `additionalProperties:"false"` // Field tags of unnamed fields are applied to parent schema.
+
+    // Field tags of unnamed fields are applied to parent schema, 
+	// they are optional and can be used to disallow unknown parameters.
+    // For non-body params, name tag must be provided explicitly.
+    // E.g. here no unknown `query` and `cookie` parameters allowed,
+    // unknown `header` params are ok.
+    _ struct{} `query:"_" cookie:"_" additionalProperties:"false"`
 }
 ```
 
diff --git a/_examples/advanced-generic/router.go b/_examples/advanced-generic/router.go
index 7dfb426..236e4c1 100644
--- a/_examples/advanced-generic/router.go
+++ b/_examples/advanced-generic/router.go
@@ -37,6 +37,12 @@ func NewRouter() http.Handler {
 				return stop, err
 			}
 
+			// Allow unknown request headers and skip response.
+			if oc, ok := openapi3.OperationCtx(rc); !ok ||
+				oc.ProcessingResponse || oc.ProcessingIn == string(rest.ParamInHeader) {
+				return stop, nil
+			}
+
 			if schema.HasType(jsonschema.Object) && len(schema.Properties) > 0 && schema.AdditionalProperties == nil {
 				schema.AdditionalProperties = (&jsonschema.SchemaOrBool{}).WithTypeBoolean(false)
 			}
diff --git a/_examples/advanced/router.go b/_examples/advanced/router.go
index 254cfa3..136b26f 100644
--- a/_examples/advanced/router.go
+++ b/_examples/advanced/router.go
@@ -31,6 +31,12 @@ func NewRouter() http.Handler {
 				return stop, err
 			}
 
+			// Allow unknown request headers and skip response.
+			if oc, ok := openapi3.OperationCtx(rc); !ok ||
+				oc.ProcessingResponse || oc.ProcessingIn == string(rest.ParamInHeader) {
+				return stop, nil
+			}
+
 			if schema.HasType(jsonschema.Object) && len(schema.Properties) > 0 && schema.AdditionalProperties == nil {
 				schema.AdditionalProperties = (&jsonschema.SchemaOrBool{}).WithTypeBoolean(false)
 			}
diff --git a/_examples/generic/main.go b/_examples/generic/main.go
index 0b3e9b2..f767589 100644
--- a/_examples/generic/main.go
+++ b/_examples/generic/main.go
@@ -35,6 +35,13 @@ func main() {
 	type helloInput struct {
 		Locale string `query:"locale" default:"en-US" pattern:"^[a-z]{2}-[A-Z]{2}$" enum:"ru-RU,en-US"`
 		Name   string `path:"name" minLength:"3"` // Field tags define parameter location and JSON schema constraints.
+
+		// Field tags of unnamed fields are applied to parent schema.
+		// they are optional and can be used to disallow unknown parameters.
+		// For non-body params, name tag must be provided explicitly.
+		// E.g. here no unknown `query` and `cookie` parameters allowed,
+		// unknown `header` params are ok.
+		_ struct{} `query:"_" cookie:"_" additionalProperties:"false"`
 	}
 
 	// Declare output port type.
diff --git a/_examples/go.mod b/_examples/go.mod
index 38f702f..e929ecb 100644
--- a/_examples/go.mod
+++ b/_examples/go.mod
@@ -13,11 +13,11 @@ require (
 	github.com/kelseyhightower/envconfig v1.4.0
 	github.com/stretchr/testify v1.7.1
 	github.com/swaggest/assertjson v1.6.8
-	github.com/swaggest/jsonschema-go v0.3.31
-	github.com/swaggest/openapi-go v0.2.15
+	github.com/swaggest/jsonschema-go v0.3.33
+	github.com/swaggest/openapi-go v0.2.17
 	github.com/swaggest/swgui v1.4.5
 	github.com/swaggest/usecase v1.1.2
-	github.com/valyala/fasthttp v1.33.0
+	github.com/valyala/fasthttp v1.35.0
 )
 
 require github.com/swaggest/rest v0.0.0-00010101000000-000000000000
@@ -28,7 +28,7 @@ require (
 	github.com/cespare/xxhash/v2 v2.1.2 // indirect
 	github.com/davecgh/go-spew v1.1.1 // indirect
 	github.com/iancoleman/orderedmap v0.2.0 // indirect
-	github.com/klauspost/compress v1.14.2 // indirect
+	github.com/klauspost/compress v1.15.1 // indirect
 	github.com/pmezard/go-difflib v1.0.0 // indirect
 	github.com/santhosh-tekuri/jsonschema/v3 v3.1.0 // indirect
 	github.com/sergi/go-diff v1.2.0 // indirect
diff --git a/_examples/go.sum b/_examples/go.sum
index 3254fd8..0384f9c 100644
--- a/_examples/go.sum
+++ b/_examples/go.sum
@@ -43,9 +43,9 @@ github.com/iancoleman/orderedmap v0.2.0/go.mod h1:N0Wam8K1arqPXNWjMo21EXnBPOPp36
 github.com/k0kubun/colorstring v0.0.0-20150214042306-9440f1994b88/go.mod h1:3w7q1U84EfirKl04SVQ/s7nPm1ZPhiXd34z40TNz36k=
 github.com/kelseyhightower/envconfig v1.4.0 h1:Im6hONhd3pLkfDFsbRgu68RDNkGF1r3dvMUtDTo2cv8=
 github.com/kelseyhightower/envconfig v1.4.0/go.mod h1:cccZRl6mQpaq41TPp5QxidR+Sa3axMbJDNb//FQX6Gg=
-github.com/klauspost/compress v1.14.1/go.mod h1:/3/Vjq9QcHkK5uEr5lBEmyoZ1iFhe47etQ6QUkpK6sk=
-github.com/klauspost/compress v1.14.2 h1:S0OHlFk/Gbon/yauFJ4FfJJF5V0fc5HbBTJazi28pRw=
-github.com/klauspost/compress v1.14.2/go.mod h1:/3/Vjq9QcHkK5uEr5lBEmyoZ1iFhe47etQ6QUkpK6sk=
+github.com/klauspost/compress v1.15.0/go.mod h1:/3/Vjq9QcHkK5uEr5lBEmyoZ1iFhe47etQ6QUkpK6sk=
+github.com/klauspost/compress v1.15.1 h1:y9FcTHGyrebwfP0ZZqFiaxTaiDnUrGkJkI+f583BL1A=
+github.com/klauspost/compress v1.15.1/go.mod h1:/3/Vjq9QcHkK5uEr5lBEmyoZ1iFhe47etQ6QUkpK6sk=
 github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI=
 github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
 github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
@@ -81,10 +81,10 @@ github.com/swaggest/assertjson v1.6.8 h1:1O/9UI5M+2OJI7BeEWKGj0wTvpRXZt5FkOJ4nRk
 github.com/swaggest/assertjson v1.6.8/go.mod h1:Euf0upn9Vlaf1/llYHTs+Kx5K3vVbpMbsZhth7zlN7M=
 github.com/swaggest/form/v5 v5.0.1 h1:YQH0REX7iMKhtoVPWXREZgbt50VYXNCKK61psnD8Fgo=
 github.com/swaggest/form/v5 v5.0.1/go.mod h1:vdnaSTze7cxVKhWiCabrfm1YeLwWLpb9P941Gxv4FnA=
-github.com/swaggest/jsonschema-go v0.3.31 h1:PMgghRyVEphrEsVX+3KByWUW3u42gmRN875S3MLedxY=
-github.com/swaggest/jsonschema-go v0.3.31/go.mod h1:JAF1nm+uIaMOXktuQepmkiRcgQ5yJk4Ccwx9HVt2cXw=
-github.com/swaggest/openapi-go v0.2.15 h1:Cp3c0IacJlDEvYYCBzVm3M51bINyI2XCH5RAwulSj/E=
-github.com/swaggest/openapi-go v0.2.15/go.mod h1:3Xd0go1dJkxlSlBHpVV3h76TccououBojV98hhXn6eQ=
+github.com/swaggest/jsonschema-go v0.3.33 h1:G9FxhWHdINUXUshHwHr4X2ptdZWEyBBDu53+VY94oy0=
+github.com/swaggest/jsonschema-go v0.3.33/go.mod h1:JAF1nm+uIaMOXktuQepmkiRcgQ5yJk4Ccwx9HVt2cXw=
+github.com/swaggest/openapi-go v0.2.17 h1:oBeSzMN+lxR4AXBu5HjNEBYlxQj29K56GGCUaQDXO5M=
+github.com/swaggest/openapi-go v0.2.17/go.mod h1:llsxcypyeyhFMPx5IPIxdk0ESN6zFcaAT3uEK5tlsmk=
 github.com/swaggest/refl v1.0.2 h1:VmP8smuDS1EzUPn31++TzMi13CAaVJdlWpIxzj0up88=
 github.com/swaggest/refl v1.0.2/go.mod h1:DoiPoBJPYHU6Z9fIA6zXQ9uI6VRL6M8BFX5YFT+ym9g=
 github.com/swaggest/swgui v1.4.5 h1:9kw3Lt+4qNdDwnRgpNSsx3s3BjoW/Inhbn7/38DX6Qk=
@@ -93,8 +93,8 @@ github.com/swaggest/usecase v1.1.2 h1:2LfuSyjYtPtpHnxqPwV87/eunbhGBC5HKdRp8/fINB
 github.com/swaggest/usecase v1.1.2/go.mod h1:abZWuMFYujaeLDODqRySJZpWD/ugsnE3Wj9K6jUeCjo=
 github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
 github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
-github.com/valyala/fasthttp v1.33.0 h1:mHBKd98J5NcXuBddgjvim1i3kWzlng1SzLhrnBOU9g8=
-github.com/valyala/fasthttp v1.33.0/go.mod h1:KJRK/MXx0J+yd0c5hlR+s1tIHD72sniU8ZJjl97LIw4=
+github.com/valyala/fasthttp v1.35.0 h1:wwkR8mZn2NbigFsaw2Zj5r+xkmzjbrA/lyTmiSlal/Y=
+github.com/valyala/fasthttp v1.35.0/go.mod h1:t/G+3rLek+CyY9bnIE+YlMRddxVAAGjhxndDB4i4C0I=
 github.com/valyala/tcplisten v1.0.0/go.mod h1:T0xQ8SeCZGxckz9qRXTfG43PvQ/mcWh7FwZEA7Ioqkc=
 github.com/vearutop/statigz v1.1.5 h1:qWvRgXFsseWVTFCkIvwHQPpaLNf9WI0+dDJE7I9432o=
 github.com/vearutop/statigz v1.1.5/go.mod h1:czAv7iXgPv/s+xsgXpVEhhD0NSOQ4wZPgmM/n7LANDI=
@@ -110,7 +110,7 @@ github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9dec
 golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
 golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
 golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
-golang.org/x/crypto v0.0.0-20220112180741-5e0467b6c7ce/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
+golang.org/x/crypto v0.0.0-20220214200702-86341886e292/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
 golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
 golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
 golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
@@ -119,8 +119,8 @@ golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/
 golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
 golang.org/x/net v0.0.0-20201202161906-c7110b5ffcbb/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
 golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
-golang.org/x/net v0.0.0-20220111093109-d55c255bac03 h1:0FB83qp0AzVJm+0wcIlauAjJ+tNdh7jLuacRYCIVv7s=
-golang.org/x/net v0.0.0-20220111093109-d55c255bac03/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
+golang.org/x/net v0.0.0-20220225172249-27dd8689420f h1:oA4XRj0qtSt8Yo1Zms0CUlsT3KG69V2UGQWPBxujDmc=
+golang.org/x/net v0.0.0-20220225172249-27dd8689420f/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
 golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
 golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
 golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
@@ -138,9 +138,11 @@ golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7w
 golang.org/x/sys v0.0.0-20210112080510-489259a85091/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/sys v0.0.0-20220111092808-5a964db01320 h1:0jf+tOCoZ3LyutmCOWpVni1chK4VfFLhRsDK7MhqGRY=
-golang.org/x/sys v0.0.0-20220111092808-5a964db01320/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20220227234510-4e6760a101f9 h1:nhht2DYV/Sn3qOayu8lM+cU1ii9sTLUeBQwQQfUHtrs=
+golang.org/x/sys v0.0.0-20220227234510-4e6760a101f9/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
+golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
 golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
 golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
 golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
diff --git a/_examples/task-api/internal/usecase/create_task.go b/_examples/task-api/internal/usecase/create_task.go
index 62ae7da..6e183ae 100644
--- a/_examples/task-api/internal/usecase/create_task.go
+++ b/_examples/task-api/internal/usecase/create_task.go
@@ -10,9 +10,11 @@ import (
 )
 
 // CreateTask creates usecase interactor.
-func CreateTask(deps interface {
-	TaskCreator() task.Creator
-}) usecase.IOInteractor {
+func CreateTask(
+	deps interface {
+		TaskCreator() task.Creator
+	},
+) usecase.IOInteractor {
 	u := usecase.NewIOI(new(task.Value), new(task.Entity), func(ctx context.Context, input, output interface{}) error {
 		var (
 			in  = input.(*task.Value)
diff --git a/_examples/task-api/internal/usecase/find_task.go b/_examples/task-api/internal/usecase/find_task.go
index 803a21a..af908e2 100644
--- a/_examples/task-api/internal/usecase/find_task.go
+++ b/_examples/task-api/internal/usecase/find_task.go
@@ -9,9 +9,11 @@ import (
 )
 
 // FindTask creates usecase interactor.
-func FindTask(deps interface {
-	TaskFinder() task.Finder
-}) usecase.IOInteractor {
+func FindTask(
+	deps interface {
+		TaskFinder() task.Finder
+	},
+) usecase.IOInteractor {
 	u := usecase.NewIOI(new(task.Identity), new(task.Entity),
 		func(ctx context.Context, input, output interface{}) error {
 			var (
diff --git a/_examples/task-api/internal/usecase/find_tasks.go b/_examples/task-api/internal/usecase/find_tasks.go
index 37bcae0..1c37739 100644
--- a/_examples/task-api/internal/usecase/find_tasks.go
+++ b/_examples/task-api/internal/usecase/find_tasks.go
@@ -10,9 +10,11 @@ import (
 )
 
 // FindTasks creates usecase interactor.
-func FindTasks(deps interface {
-	TaskFinder() task.Finder
-}) usecase.IOInteractor {
+func FindTasks(
+	deps interface {
+		TaskFinder() task.Finder
+	},
+) usecase.IOInteractor {
 	u := usecase.NewIOI(nil, new([]task.Entity), func(ctx context.Context, input, output interface{}) error {
 		out, ok := output.(*[]task.Entity)
 		if !ok {
diff --git a/_examples/task-api/internal/usecase/update_task.go b/_examples/task-api/internal/usecase/update_task.go
index cdcaf63..30f18e4 100644
--- a/_examples/task-api/internal/usecase/update_task.go
+++ b/_examples/task-api/internal/usecase/update_task.go
@@ -14,9 +14,11 @@ type updateTask struct {
 }
 
 // UpdateTask creates usecase interactor.
-func UpdateTask(deps interface {
-	TaskUpdater() task.Updater
-}) usecase.Interactor {
+func UpdateTask(
+	deps interface {
+		TaskUpdater() task.Updater
+	},
+) usecase.Interactor {
 	u := usecase.NewIOI(new(updateTask), nil, func(ctx context.Context, input, _ interface{}) error {
 		var (
 			in  = input.(*updateTask)
diff --git a/go.mod b/go.mod
index 40951b5..e4fd8f7 100644
--- a/go.mod
+++ b/go.mod
@@ -12,8 +12,8 @@ require (
 	github.com/stretchr/testify v1.7.1
 	github.com/swaggest/assertjson v1.6.8
 	github.com/swaggest/form/v5 v5.0.1
-	github.com/swaggest/jsonschema-go v0.3.31
-	github.com/swaggest/openapi-go v0.2.15
+	github.com/swaggest/jsonschema-go v0.3.33
+	github.com/swaggest/openapi-go v0.2.17
 	github.com/swaggest/refl v1.0.2
 	github.com/swaggest/usecase v1.1.2
 )
diff --git a/go.sum b/go.sum
index 83a33a6..cf25d3a 100644
--- a/go.sum
+++ b/go.sum
@@ -1,8 +1,6 @@
-github.com/bool64/dev v0.1.17/go.mod h1:cTHiTDNc8EewrQPy3p1obNilpMpdmlUesDkFTF2zRWU=
 github.com/bool64/dev v0.1.25/go.mod h1:cTHiTDNc8EewrQPy3p1obNilpMpdmlUesDkFTF2zRWU=
 github.com/bool64/dev v0.1.35/go.mod h1:cTHiTDNc8EewrQPy3p1obNilpMpdmlUesDkFTF2zRWU=
 github.com/bool64/dev v0.1.41/go.mod h1:cTHiTDNc8EewrQPy3p1obNilpMpdmlUesDkFTF2zRWU=
-github.com/bool64/dev v0.1.42/go.mod h1:cTHiTDNc8EewrQPy3p1obNilpMpdmlUesDkFTF2zRWU=
 github.com/bool64/dev v0.2.5/go.mod h1:cTHiTDNc8EewrQPy3p1obNilpMpdmlUesDkFTF2zRWU=
 github.com/bool64/dev v0.2.10 h1:ypAGBazcwyIy2JvIJio8V3kdqO7AgIAYvcckW54qxr4=
 github.com/bool64/dev v0.2.10/go.mod h1:/csLrm+4oDSsKJRIVS0mrywAonLnYKFG8RvGT7Jh9b8=
@@ -72,13 +70,10 @@ github.com/swaggest/assertjson v1.6.8 h1:1O/9UI5M+2OJI7BeEWKGj0wTvpRXZt5FkOJ4nRk
 github.com/swaggest/assertjson v1.6.8/go.mod h1:Euf0upn9Vlaf1/llYHTs+Kx5K3vVbpMbsZhth7zlN7M=
 github.com/swaggest/form/v5 v5.0.1 h1:YQH0REX7iMKhtoVPWXREZgbt50VYXNCKK61psnD8Fgo=
 github.com/swaggest/form/v5 v5.0.1/go.mod h1:vdnaSTze7cxVKhWiCabrfm1YeLwWLpb9P941Gxv4FnA=
-github.com/swaggest/jsonschema-go v0.3.24/go.mod h1:B2ZSqrrlkj21zhywlkh8VnKBmuqUwDv3dLQoPjgHk7M=
-github.com/swaggest/jsonschema-go v0.3.31 h1:PMgghRyVEphrEsVX+3KByWUW3u42gmRN875S3MLedxY=
-github.com/swaggest/jsonschema-go v0.3.31/go.mod h1:JAF1nm+uIaMOXktuQepmkiRcgQ5yJk4Ccwx9HVt2cXw=
-github.com/swaggest/openapi-go v0.2.15 h1:Cp3c0IacJlDEvYYCBzVm3M51bINyI2XCH5RAwulSj/E=
-github.com/swaggest/openapi-go v0.2.15/go.mod h1:3Xd0go1dJkxlSlBHpVV3h76TccououBojV98hhXn6eQ=
-github.com/swaggest/refl v1.0.0/go.mod h1:acYd5x8NNxivp+ZHdRZKJYz66n/qjo3Q9Sa/jAivljQ=
-github.com/swaggest/refl v1.0.1/go.mod h1:dnx+n9YaI0o+FH+OR2tJZWLABBVIPs9qc4VY9UdrhLE=
+github.com/swaggest/jsonschema-go v0.3.33 h1:G9FxhWHdINUXUshHwHr4X2ptdZWEyBBDu53+VY94oy0=
+github.com/swaggest/jsonschema-go v0.3.33/go.mod h1:JAF1nm+uIaMOXktuQepmkiRcgQ5yJk4Ccwx9HVt2cXw=
+github.com/swaggest/openapi-go v0.2.17 h1:oBeSzMN+lxR4AXBu5HjNEBYlxQj29K56GGCUaQDXO5M=
+github.com/swaggest/openapi-go v0.2.17/go.mod h1:llsxcypyeyhFMPx5IPIxdk0ESN6zFcaAT3uEK5tlsmk=
 github.com/swaggest/refl v1.0.2 h1:VmP8smuDS1EzUPn31++TzMi13CAaVJdlWpIxzj0up88=
 github.com/swaggest/refl v1.0.2/go.mod h1:DoiPoBJPYHU6Z9fIA6zXQ9uI6VRL6M8BFX5YFT+ym9g=
 github.com/swaggest/usecase v1.1.2 h1:2LfuSyjYtPtpHnxqPwV87/eunbhGBC5HKdRp8/fINBk=
diff --git a/jsonschema/validator.go b/jsonschema/validator.go
index e655052..c9ea684 100644
--- a/jsonschema/validator.go
+++ b/jsonschema/validator.go
@@ -4,6 +4,7 @@ package jsonschema
 import (
 	"bytes"
 	"encoding/json"
+	"fmt"
 
 	"github.com/santhosh-tekuri/jsonschema/v3"
 	"github.com/swaggest/rest"
@@ -18,6 +19,7 @@ type Validator struct {
 
 	inNamedSchemas map[rest.ParamIn]map[string]*jsonschema.Schema
 	inRequired     map[rest.ParamIn][]string
+	forbidUnknown  map[rest.ParamIn]bool
 }
 
 // NewFactory creates new validator factory.
@@ -85,6 +87,15 @@ func (f Factory) MakeResponseValidator(
 	return &v
 }
 
+// ForbidUnknownParams configures if unknown parameters should be forbidden.
+func (v *Validator) ForbidUnknownParams(in rest.ParamIn, forbidden bool) {
+	if v.forbidUnknown == nil {
+		v.forbidUnknown = make(map[rest.ParamIn]bool)
+	}
+
+	v.forbidUnknown[in] = forbidden
+}
+
 // AddSchema registers schema for validation.
 func (v *Validator) AddSchema(in rest.ParamIn, name string, jsonSchema []byte, required bool) error {
 	if v.JSONMarshal == nil {
@@ -106,6 +117,8 @@ func (v *Validator) AddSchema(in rest.ParamIn, name string, jsonSchema []byte, r
 	}
 
 	if len(jsonSchema) == 0 {
+		v.inNamedSchemas[in][name] = nil
+
 		return nil
 	}
 
@@ -191,6 +204,18 @@ func (v *Validator) ValidateData(in rest.ParamIn, namedData map[string]interface
 	for name, value := range namedData {
 		schema, found := v.inNamedSchemas[in][name]
 		if !found {
+			if v.forbidUnknown[in] {
+				if errs == nil {
+					errs = make(rest.ValidationErrors, 1)
+				}
+
+				errs[string(in)+":"+name] = []string{fmt.Sprintf("unknown parameter with value %v", value)}
+			}
+
+			continue
+		}
+
+		if schema == nil {
 			continue
 		}
 
diff --git a/openapi/collector.go b/openapi/collector.go
index 8956406..15054a7 100644
--- a/openapi/collector.go
+++ b/openapi/collector.go
@@ -231,7 +231,19 @@ func (c *Collector) processUseCase(op *openapi3.Operation, u usecase.Interactor,
 	return nil
 }
 
+type unknownFieldsValidator interface {
+	ForbidUnknownParams(in rest.ParamIn, forbidden bool)
+}
+
 func (c *Collector) provideParametersJSONSchemas(op openapi3.Operation, validator rest.JSONSchemaValidator) error {
+	if fv, ok := validator.(unknownFieldsValidator); ok {
+		for _, in := range []rest.ParamIn{rest.ParamInQuery, rest.ParamInCookie, rest.ParamInHeader} {
+			if op.UnknownParamIsForbidden(openapi3.ParameterIn(in)) {
+				fv.ForbidUnknownParams(in, true)
+			}
+		}
+	}
+
 	for _, p := range op.Parameters {
 		if p.Parameter.Schema == nil {
 			continue
diff --git a/request/decoder.go b/request/decoder.go
index 27b92d8..0494aed 100644
--- a/request/decoder.go
+++ b/request/decoder.go
@@ -30,6 +30,22 @@ func decodeValidate(d *form.Decoder, v interface{}, p url.Values, in rest.ParamI
 		return err
 	}
 
+	if len(p) > len(goValues) {
+		for k := range p {
+			if _, exists := goValues[k]; !exists {
+				pk := p[k]
+				switch len(pk) {
+				case 0:
+					goValues[k] = nil
+				case 1:
+					goValues[k] = p[k][0]
+				default:
+					goValues[k] = p[k]
+				}
+			}
+		}
+	}
+
 	return val.ValidateData(in, goValues)
 }
 


Test patch:
diff --git a/_examples/advanced-generic/_testdata/openapi.json b/_examples/advanced-generic/_testdata/openapi.json
index 50972ea..6693f83 100644
--- a/_examples/advanced-generic/_testdata/openapi.json
+++ b/_examples/advanced-generic/_testdata/openapi.json
@@ -25,7 +25,8 @@
             "description":"Conflict",
             "content":{"application/json":{"schema":{"$ref":"#/components/schemas/AdvancedCustomErr"}}}
           }
-        }
+        },
+        "x-forbid-unknown-query":true
       }
     },
     "/file-multi-upload":{
@@ -44,7 +45,8 @@
             "description":"OK",
             "content":{"application/json":{"schema":{"$ref":"#/components/schemas/AdvancedInfoType2"}}}
           }
-        }
+        },
+        "x-forbid-unknown-query":true
       }
     },
     "/file-upload":{
@@ -60,7 +62,8 @@
         "requestBody":{"content":{"multipart/form-data":{"schema":{"$ref":"#/components/schemas/FormDataAdvancedUpload"}}}},
         "responses":{
           "200":{"description":"OK","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AdvancedInfo"}}}}
-        }
+        },
+        "x-forbid-unknown-query":true
       }
     },
     "/gzip-pass-through":{
@@ -81,7 +84,8 @@
             "description":"OK","headers":{"X-Header":{"style":"simple","schema":{"type":"string"}}},
             "content":{"application/json":{"schema":{"$ref":"#/components/schemas/AdvancedGzipPassThroughStruct"}}}
           }
-        }
+        },
+        "x-forbid-unknown-query":true
       },
       "head":{
         "summary":"Direct Gzip","description":"","operationId":"_examples/advanced-generic.directGzip",
@@ -100,7 +104,8 @@
             "description":"OK","headers":{"X-Header":{"style":"simple","schema":{"type":"string"}}},
             "content":{"application/json":{}}
           }
-        }
+        },
+        "x-forbid-unknown-query":true
       }
     },
     "/json-body-validation/{in-path}":{
@@ -128,7 +133,8 @@
             "description":"OK",
             "content":{"application/json":{"schema":{"$ref":"#/components/schemas/AdvancedOutputWithJSONType3"}}}
           }
-        }
+        },
+        "x-forbid-unknown-path":true,"x-forbid-unknown-query":true
       }
     },
     "/json-body/{in-path}":{
@@ -156,7 +162,8 @@
             "description":"Created",
             "content":{"application/json":{"schema":{"$ref":"#/components/schemas/AdvancedOutputWithJSONType2"}}}
           }
-        }
+        },
+        "x-forbid-unknown-path":true,"x-forbid-unknown-query":true
       }
     },
     "/json-map-body":{
@@ -179,7 +186,8 @@
             "description":"OK",
             "content":{"application/json":{"schema":{"$ref":"#/components/schemas/AdvancedJsonOutputType2"}}}
           }
-        }
+        },
+        "x-forbid-unknown-query":true
       }
     },
     "/json-param/{in-path}":{
@@ -210,7 +218,8 @@
             "description":"OK",
             "content":{"application/json":{"schema":{"$ref":"#/components/schemas/AdvancedOutputWithJSON"}}}
           }
-        }
+        },
+        "x-forbid-unknown-path":true,"x-forbid-unknown-query":true
       }
     },
     "/json-slice-body":{
@@ -233,7 +242,8 @@
             "description":"OK",
             "content":{"application/json":{"schema":{"$ref":"#/components/schemas/AdvancedJsonOutput"}}}
           }
-        }
+        },
+        "x-forbid-unknown-query":true
       }
     },
     "/no-validation":{
@@ -254,7 +264,8 @@
             },
             "content":{"application/json":{"schema":{"$ref":"#/components/schemas/AdvancedOutputPortType3"}}}
           }
-        }
+        },
+        "x-forbid-unknown-query":true
       }
     },
     "/output-csv-writer":{
@@ -328,7 +339,8 @@
             "description":"OK",
             "content":{"application/json":{"schema":{"$ref":"#/components/schemas/AdvancedOutputQueryObject"}}}
           }
-        }
+        },
+        "x-forbid-unknown-query":true
       }
     },
     "/req-resp-mapping":{
@@ -393,30 +405,26 @@
             },
             "content":{"application/json":{"schema":{"$ref":"#/components/schemas/AdvancedOutputPortType2"}}}
           }
-        }
+        },
+        "x-forbid-unknown-query":true
       }
     }
   },
   "components":{
     "schemas":{
-      "AdvancedCustomErr":{
-        "type":"object","properties":{"details":{"type":"object","additionalProperties":{}},"msg":{"type":"string"}},
-        "additionalProperties":false
-      },
+      "AdvancedCustomErr":{"type":"object","properties":{"details":{"type":"object","additionalProperties":{}},"msg":{"type":"string"}}},
       "AdvancedGzipPassThroughStruct":{
         "type":"object",
-        "properties":{"id":{"type":"integer"},"text":{"type":"array","items":{"type":"string"},"nullable":true}},
-        "additionalProperties":false
+        "properties":{"id":{"type":"integer"},"text":{"type":"array","items":{"type":"string"},"nullable":true}}
       },
-      "AdvancedHeaderOutput":{"type":"object","properties":{"inBody":{"type":"string","deprecated":true}},"additionalProperties":false},
+      "AdvancedHeaderOutput":{"type":"object","properties":{"inBody":{"type":"string","deprecated":true}}},
       "AdvancedInfo":{
         "type":"object",
         "properties":{
           "filename":{"type":"string"},"header":{"$ref":"#/components/schemas/TextprotoMIMEHeader"},
           "inQuery":{"type":"integer"},"peek1":{"type":"string"},"peek2":{"type":"string"},"simple":{"type":"string"},
           "size":{"type":"integer"}
-        },
-        "additionalProperties":false
+        }
       },
       "AdvancedInfoType2":{
         "type":"object",
@@ -426,8 +434,7 @@
           "inQuery":{"type":"integer"},"peeks1":{"type":"array","items":{"type":"string"},"nullable":true},
           "peeks2":{"type":"array","items":{"type":"string"},"nullable":true},"simple":{"type":"string"},
           "sizes":{"type":"array","items":{"type":"integer"},"nullable":true}
-        },
-        "additionalProperties":false
+        }
       },
       "AdvancedInputPortType2":{
         "required":["data"],"type":"object",
@@ -467,49 +474,39 @@
         "properties":{
           "data":{"$ref":"#/components/schemas/AdvancedJSONSlicePayload"},"inHeader":{"type":"string"},
           "inQuery":{"type":"integer"}
-        },
-        "additionalProperties":false
+        }
       },
       "AdvancedJsonOutputType2":{
         "type":"object",
         "properties":{
           "data":{"$ref":"#/components/schemas/AdvancedJSONMapPayload"},"inHeader":{"type":"string"},
           "inQuery":{"type":"integer"}
-        },
-        "additionalProperties":false
+        }
       },
       "AdvancedJsonSliceReq":{"type":"array","items":{"type":"integer"}},
-      "AdvancedOkResp":{"type":"object","properties":{"status":{"type":"string"}},"additionalProperties":false},
+      "AdvancedOkResp":{"type":"object","properties":{"status":{"type":"string"}}},
       "AdvancedOutputPortType2":{
         "required":["data"],"type":"object",
-        "properties":{"data":{"type":"object","properties":{"value":{"maxLength":7,"type":"string"}},"additionalProperties":false}},
-        "additionalProperties":false
-      },
-      "AdvancedOutputPortType3":{
-        "type":"object",
-        "properties":{"data":{"type":"object","properties":{"value":{"type":"string"}},"additionalProperties":false}},
-        "additionalProperties":false
+        "properties":{"data":{"type":"object","properties":{"value":{"maxLength":7,"type":"string"}}}}
       },
+      "AdvancedOutputPortType3":{"type":"object","properties":{"data":{"type":"object","properties":{"value":{"type":"string"}}}}},
       "AdvancedOutputQueryObject":{
         "type":"object",
-        "properties":{"inQuery":{"type":"object","additionalProperties":{"type":"number"},"nullable":true}},
-        "additionalProperties":false
+        "properties":{"inQuery":{"type":"object","additionalProperties":{"type":"number"},"nullable":true}}
       },
       "AdvancedOutputWithJSON":{
         "type":"object",
         "properties":{
           "id":{"type":"integer"},"inHeader":{"type":"string"},"inPath":{"type":"string"},"inQuery":{"type":"integer"},
           "name":{"type":"string"}
-        },
-        "additionalProperties":false
+        }
       },
       "AdvancedOutputWithJSONType2":{
         "type":"object",
         "properties":{
           "id":{"type":"integer"},"inHeader":{"type":"string"},"inPath":{"type":"string"},
           "inQuery":{"type":"string","format":"date","deprecated":true},"name":{"type":"string"}
-        },
-        "additionalProperties":false
+        }
       },
       "AdvancedOutputWithJSONType3":{
         "type":"object",
@@ -517,8 +514,7 @@
           "id":{"minimum":100,"type":"integer"},"inHeader":{"minLength":3,"type":"string"},
           "inPath":{"minLength":3,"type":"string"},"inQuery":{"minimum":3,"type":"integer"},
           "name":{"minLength":3,"type":"string"}
-        },
-        "additionalProperties":false
+        }
       },
       "FormDataAdvancedInputPort":{
         "required":["val2"],"type":"object",
diff --git a/_examples/advanced-generic/request_response_mapping_test.go b/_examples/advanced-generic/request_response_mapping_test.go
index d5990b5..0c85192 100644
--- a/_examples/advanced-generic/request_response_mapping_test.go
+++ b/_examples/advanced-generic/request_response_mapping_test.go
@@ -35,7 +35,7 @@ func Test_requestResponseMapping(t *testing.T) {
 	body, err := ioutil.ReadAll(resp.Body)
 	assert.NoError(t, err)
 	assert.NoError(t, resp.Body.Close())
-	assert.Empty(t, body)
+	assert.Equal(t, "", string(body))
 
 	assert.Equal(t, "abc", resp.Header.Get("X-Value-1"))
 	assert.Equal(t, "3", resp.Header.Get("X-Value-2"))
diff --git a/_examples/advanced/_testdata/openapi.json b/_examples/advanced/_testdata/openapi.json
index ce688fa..dee02e8 100644
--- a/_examples/advanced/_testdata/openapi.json
+++ b/_examples/advanced/_testdata/openapi.json
@@ -25,7 +25,8 @@
             "description":"Conflict",
             "content":{"application/json":{"schema":{"$ref":"#/components/schemas/AdvancedCustomErr"}}}
           }
-        }
+        },
+        "x-forbid-unknown-query":true
       }
     },
     "/file-multi-upload":{
@@ -44,7 +45,8 @@
             "description":"OK",
             "content":{"application/json":{"schema":{"$ref":"#/components/schemas/AdvancedInfoType2"}}}
           }
-        }
+        },
+        "x-forbid-unknown-query":true
       }
     },
     "/file-upload":{
@@ -60,7 +62,8 @@
         "requestBody":{"content":{"multipart/form-data":{"schema":{"$ref":"#/components/schemas/FormDataAdvancedUpload"}}}},
         "responses":{
           "200":{"description":"OK","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AdvancedInfo"}}}}
-        }
+        },
+        "x-forbid-unknown-query":true
       }
     },
     "/gzip-pass-through":{
@@ -81,7 +84,8 @@
             "description":"OK","headers":{"X-Header":{"style":"simple","schema":{"type":"string"}}},
             "content":{"application/json":{"schema":{"$ref":"#/components/schemas/AdvancedGzipPassThroughStruct"}}}
           }
-        }
+        },
+        "x-forbid-unknown-query":true
       },
       "head":{
         "summary":"Direct Gzip","description":"","operationId":"_examples/advanced.directGzip",
@@ -100,7 +104,8 @@
             "description":"OK","headers":{"X-Header":{"style":"simple","schema":{"type":"string"}}},
             "content":{"application/json":{}}
           }
-        }
+        },
+        "x-forbid-unknown-query":true
       }
     },
     "/json-body-validation/{in-path}":{
@@ -128,7 +133,8 @@
             "description":"OK",
             "content":{"application/json":{"schema":{"$ref":"#/components/schemas/AdvancedOutputWithJSONType3"}}}
           }
-        }
+        },
+        "x-forbid-unknown-path":true,"x-forbid-unknown-query":true
       }
     },
     "/json-body/{in-path}":{
@@ -156,7 +162,8 @@
             "description":"Created",
             "content":{"application/json":{"schema":{"$ref":"#/components/schemas/AdvancedOutputWithJSONType2"}}}
           }
-        }
+        },
+        "x-forbid-unknown-path":true,"x-forbid-unknown-query":true
       }
     },
     "/json-map-body":{
@@ -179,7 +186,8 @@
             "description":"OK",
             "content":{"application/json":{"schema":{"$ref":"#/components/schemas/AdvancedJsonOutputType2"}}}
           }
-        }
+        },
+        "x-forbid-unknown-query":true
       }
     },
     "/json-param/{in-path}":{
@@ -210,7 +218,8 @@
             "description":"OK",
             "content":{"application/json":{"schema":{"$ref":"#/components/schemas/AdvancedOutputWithJSON"}}}
           }
-        }
+        },
+        "x-forbid-unknown-path":true,"x-forbid-unknown-query":true
       }
     },
     "/json-slice-body":{
@@ -232,7 +241,8 @@
             "description":"OK",
             "content":{"application/json":{"schema":{"$ref":"#/components/schemas/AdvancedJsonOutput"}}}
           }
-        }
+        },
+        "x-forbid-unknown-query":true
       }
     },
     "/no-validation":{
@@ -253,7 +263,8 @@
             },
             "content":{"application/json":{"schema":{"$ref":"#/components/schemas/AdvancedOutputPortType3"}}}
           }
-        }
+        },
+        "x-forbid-unknown-query":true
       }
     },
     "/output-csv-writer":{
@@ -324,7 +335,8 @@
             "description":"OK",
             "content":{"application/json":{"schema":{"$ref":"#/components/schemas/AdvancedOutputQueryObject"}}}
           }
-        }
+        },
+        "x-forbid-unknown-query":true
       }
     },
     "/req-resp-mapping":{
@@ -389,30 +401,26 @@
             },
             "content":{"application/json":{"schema":{"$ref":"#/components/schemas/AdvancedOutputPortType2"}}}
           }
-        }
+        },
+        "x-forbid-unknown-query":true
       }
     }
   },
   "components":{
     "schemas":{
-      "AdvancedCustomErr":{
-        "type":"object","properties":{"details":{"type":"object","additionalProperties":{}},"msg":{"type":"string"}},
-        "additionalProperties":false
-      },
+      "AdvancedCustomErr":{"type":"object","properties":{"details":{"type":"object","additionalProperties":{}},"msg":{"type":"string"}}},
       "AdvancedGzipPassThroughStruct":{
         "type":"object",
-        "properties":{"id":{"type":"integer"},"text":{"type":"array","items":{"type":"string"},"nullable":true}},
-        "additionalProperties":false
+        "properties":{"id":{"type":"integer"},"text":{"type":"array","items":{"type":"string"},"nullable":true}}
       },
-      "AdvancedHeaderOutput":{"type":"object","properties":{"inBody":{"type":"string","deprecated":true}},"additionalProperties":false},
+      "AdvancedHeaderOutput":{"type":"object","properties":{"inBody":{"type":"string","deprecated":true}}},
       "AdvancedInfo":{
         "type":"object",
         "properties":{
           "filename":{"type":"string"},"header":{"$ref":"#/components/schemas/TextprotoMIMEHeader"},
           "inQuery":{"type":"integer"},"peek1":{"type":"string"},"peek2":{"type":"string"},"simple":{"type":"string"},
           "size":{"type":"integer"}
-        },
-        "additionalProperties":false
+        }
       },
       "AdvancedInfoType2":{
         "type":"object",
@@ -422,8 +430,7 @@
           "inQuery":{"type":"integer"},"peeks1":{"type":"array","items":{"type":"string"},"nullable":true},
           "peeks2":{"type":"array","items":{"type":"string"},"nullable":true},"simple":{"type":"string"},
           "sizes":{"type":"array","items":{"type":"integer"},"nullable":true}
-        },
-        "additionalProperties":false
+        }
       },
       "AdvancedInputPortType2":{
         "required":["data"],"type":"object",
@@ -463,49 +470,39 @@
         "properties":{
           "data":{"$ref":"#/components/schemas/AdvancedJSONSlicePayload"},"inHeader":{"type":"string"},
           "inQuery":{"type":"integer"}
-        },
-        "additionalProperties":false
+        }
       },
       "AdvancedJsonOutputType2":{
         "type":"object",
         "properties":{
           "data":{"$ref":"#/components/schemas/AdvancedJSONMapPayload"},"inHeader":{"type":"string"},
           "inQuery":{"type":"integer"}
-        },
-        "additionalProperties":false
+        }
       },
       "AdvancedJsonSliceReq":{"type":"array","items":{"type":"integer"},"nullable":true},
-      "AdvancedOkResp":{"type":"object","properties":{"status":{"type":"string"}},"additionalProperties":false},
+      "AdvancedOkResp":{"type":"object","properties":{"status":{"type":"string"}}},
       "AdvancedOutputPortType2":{
         "required":["data"],"type":"object",
-        "properties":{"data":{"type":"object","properties":{"value":{"maxLength":7,"type":"string"}},"additionalProperties":false}},
-        "additionalProperties":false
-      },
-      "AdvancedOutputPortType3":{
-        "type":"object",
-        "properties":{"data":{"type":"object","properties":{"value":{"type":"string"}},"additionalProperties":false}},
-        "additionalProperties":false
+        "properties":{"data":{"type":"object","properties":{"value":{"maxLength":7,"type":"string"}}}}
       },
+      "AdvancedOutputPortType3":{"type":"object","properties":{"data":{"type":"object","properties":{"value":{"type":"string"}}}}},
       "AdvancedOutputQueryObject":{
         "type":"object",
-        "properties":{"inQuery":{"type":"object","additionalProperties":{"type":"number"},"nullable":true}},
-        "additionalProperties":false
+        "properties":{"inQuery":{"type":"object","additionalProperties":{"type":"number"},"nullable":true}}
       },
       "AdvancedOutputWithJSON":{
         "type":"object",
         "properties":{
           "id":{"type":"integer"},"inHeader":{"type":"string"},"inPath":{"type":"string"},"inQuery":{"type":"integer"},
           "name":{"type":"string"}
-        },
-        "additionalProperties":false
+        }
       },
       "AdvancedOutputWithJSONType2":{
         "type":"object",
         "properties":{
           "id":{"type":"integer"},"inHeader":{"type":"string"},"inPath":{"type":"string"},
           "inQuery":{"type":"string","format":"date","deprecated":true},"name":{"type":"string"}
-        },
-        "additionalProperties":false
+        }
       },
       "AdvancedOutputWithJSONType3":{
         "type":"object",
@@ -513,8 +510,7 @@
           "id":{"minimum":100,"type":"integer"},"inHeader":{"minLength":3,"type":"string"},
           "inPath":{"minLength":3,"type":"string"},"inQuery":{"minimum":3,"type":"integer"},
           "name":{"minLength":3,"type":"string"}
-        },
-        "additionalProperties":false
+        }
       },
       "FormDataAdvancedInputPort":{
         "required":["val2"],"type":"object",
diff --git a/_examples/advanced/request_response_mapping_test.go b/_examples/advanced/request_response_mapping_test.go
index 6215d12..5db99a5 100644
--- a/_examples/advanced/request_response_mapping_test.go
+++ b/_examples/advanced/request_response_mapping_test.go
@@ -33,7 +33,7 @@ func Test_requestResponseMapping(t *testing.T) {
 	body, err := ioutil.ReadAll(resp.Body)
 	assert.NoError(t, err)
 	assert.NoError(t, resp.Body.Close())
-	assert.Empty(t, body)
+	assert.Equal(t, "", string(body))
 
 	assert.Equal(t, "abc", resp.Header.Get("X-Value-1"))
 	assert.Equal(t, "3", resp.Header.Get("X-Value-2"))
diff --git a/jsonschema/validator_test.go b/jsonschema/validator_test.go
index 0e69ee7..992a057 100644
--- a/jsonschema/validator_test.go
+++ b/jsonschema/validator_test.go
@@ -1,6 +1,8 @@
 package jsonschema_test
 
 import (
+	"context"
+	"fmt"
 	"net/http"
 	"testing"
 	"time"
@@ -9,6 +11,7 @@ import (
 	"github.com/swaggest/rest"
 	"github.com/swaggest/rest/jsonschema"
 	"github.com/swaggest/rest/openapi"
+	"github.com/swaggest/rest/request"
 )
 
 // BenchmarkRequestValidator_ValidateRequestData-4   	  634356	      1761 ns/op	    2496 B/op	       8 allocs/op.
@@ -98,3 +101,25 @@ func TestNullableTime(t *testing.T) {
 
 	assert.NoError(t, err, "%+v", err)
 }
+
+func TestValidator_ForbidUnknownParams(t *testing.T) {
+	req, err := http.NewRequestWithContext(context.Background(), http.MethodGet,
+		"/?foo=bar&baz=1", nil)
+	assert.NoError(t, err)
+
+	type input struct {
+		Foo string `query:"foo"`
+
+		_ struct{} `query:"_" additionalProperties:"false"`
+	}
+
+	in := new(input)
+
+	dec := request.NewDecoderFactory().MakeDecoder(http.MethodGet, in, nil)
+	validator := jsonschema.NewFactory(&openapi.Collector{}, &openapi.Collector{}).
+		MakeRequestValidator(http.MethodGet, in, nil)
+
+	err = dec.Decode(req, in, validator)
+	assert.Equal(t, rest.ValidationErrors{"query:baz": []string{"unknown parameter with value 1"}}, err,
+		fmt.Sprintf("%#v", err))
+}
diff --git a/request/decoder_test.go b/request/decoder_test.go
index 736f7b0..9070d6e 100644
--- a/request/decoder_test.go
+++ b/request/decoder_test.go
@@ -443,3 +443,27 @@ func TestDecoder_Decode_manualLoader(t *testing.T) {
 	assert.True(t, loadTriggered)
 	assert.True(t, input.Time.IsZero())
 }
+
+func TestDecoder_Decode_unknownParams(t *testing.T) {
+	req, err := http.NewRequestWithContext(context.Background(), http.MethodGet,
+		"/?foo=1&bar=1&bar=2&baz&quux=123", nil)
+	assert.NoError(t, err)
+
+	type input struct {
+		Foo string   `query:"foo"`
+		Bar []string `query:"bar"`
+		Baz *string  `query:"baz"`
+
+		_ struct{} `query:"_" additionalProperties:"false"`
+	}
+
+	in := new(input)
+
+	dec := request.NewDecoderFactory().MakeDecoder(http.MethodGet, in, nil)
+	validator := jsonschema.NewFactory(&openapi.Collector{}, &openapi.Collector{}).
+		MakeRequestValidator(http.MethodGet, in, nil)
+
+	err = dec.Decode(req, in, validator)
+	assert.Equal(t, rest.ValidationErrors{"query:quux": []string{"unknown parameter with value 123"}}, err,
+		fmt.Sprintf("%#v", err))
+}