Repository: swaggest/rest
Pull request #165 (2023-08-03 13:56:36)

Add support for gorilla/mux
This PR reduces direct dependency on `openapi3` package to prepare for the future inclusion of `openapi31`.

Instrumentation for `gorilla/mux` is built on-top of improved API.

github-actions[bot]: ### Lines Of Code

| Language  |  Files   |    Lines    |    Code     |  Comments  |   Blanks   | Complexity |      Bytes      |
|-----------|----------|-------------|-------------|------------|------------|------------|-----------------|
| Go        | 109 (+3) | 7980 (+220) | 5783 (+133) | 548 (+44)  | 1649 (+43) | 971 (+4)   | 193.7K (+5.6K)  |
| Go (test) | 53 (+4)  | 6321 (+637) | 4846 (+441) | 290 (+103) | 1185 (+93) | 186 (+4)   | 174.1K (+16.2K) |
| JSON      | 3        | 1575 (-15)  | 1575 (-15)  | 0          | 0          | 0          | 65.8K (-451B)   |
| Markdown  | 6        | 544 (+4)    | 414 (+2)    | 0          | 130 (+2)   | 0          | 18.4K (+154B)   |
| YAML      | 10       | 736 (+5)    | 631 (+5)    | 46         | 59         | 0          | 23K (+81B)      |
<!-- Sticky Pull Request CommentLOC -->
github-actions[bot]: ### Go API Changes

<pre>
# github.com/swaggest/rest
## compatible changes
HandlerTrait.OpenAPIAnnotations: added

# github.com/swaggest/rest/gorillamux
## compatible changes
package added

# github.com/swaggest/rest/nethttp
## compatible changes
AnnotateOpenAPIOperation: added
Handler.OpenAPIAnnotations: added
OpenAPIAnnotationsMiddleware: added

# github.com/swaggest/rest/openapi
## compatible changes
(*Collector).AnnotateOperation: added
(*Collector).CollectOperation: added
(*Collector).CollectUseCase: added
(*Collector).HasAnnotation: added
(*Collector).Refl: added
(*Collector).SpecSchema: added
NewCollector: added

# github.com/swaggest/rest/web
## compatible changes
(*Service).OpenAPIReflector: added
(*Service).OpenAPISchema: added

# summary
Inferred base version: v0.2.53
Suggested version: v0.3.0
</pre>
<!-- Sticky Pull Request Commentgorelease -->
github-actions[bot]: ### Unit Test Coverage
total:								(statements)			82.4%
changed lines: (statements) 69.4%, coverage is less than 90.0%, consider testing the changes more thoroughly
<details><summary>Coverage of changed lines</summary>

|             File              |           Function           | Coverage |
|-------------------------------|------------------------------|----------|
| Total                         |                              | 69.4%    |
| gorillamux/collector.go       |                              | 85.7%    |
| gorillamux/collector.go:31    | NewOpenAPICollector          | 100.0%   |
| gorillamux/collector.go:49    | Walker                       | 89.4%    |
| gorillamux/path_decoder.go    |                              | 100.0%   |
| gorillamux/path_decoder.go:11 | PathToURLValues              | 100.0%   |
| jsonschema/validator.go       |                              | 75.0%    |
| jsonschema/validator.go:166   | ValidateJSONBody             | 100.0%   |
| nethttp/handler.go            |                              | 100.0%   |
| nethttp/handler.go:17         | NewHandler                   | 100.0%   |
| nethttp/openapi.go            |                              | 100.0%   |
| nethttp/openapi.go:13         | OpenAPIMiddleware            | 100.0%   |
| nethttp/openapi.go:151        | OpenAPIAnnotationsMiddleware | 100.0%   |
| nethttp/openapi.go:174        | securityMiddleware           | 100.0%   |
| nethttp/options.go            |                              | 72.7%    |
| nethttp/options.go:57         | RequestBodyContent           | 100.0%   |
| nethttp/options.go:40         | AnnotateOperation            | 90.9%    |
| nethttp/options.go:31         | AnnotateOpenAPIOperation     | 0.0%     |
| openapi/collector.go          |                              | 60.2%    |
| openapi/collector.go:60       | SpecSchema                   | 100.0%   |
| openapi/collector.go:330      | setupInput                   | 100.0%   |
| openapi/collector.go:340      | setFieldMapping              | 100.0%   |
| openapi/collector.go:350      | processUseCase               | 100.0%   |
| openapi/collector.go:412      | setOCJSONResponse            | 100.0%   |
| openapi/collector.go:421      | processOCExpectedErrors      | 100.0%   |
| openapi/collector.go:565      | ServeHTTP                    | 100.0%   |
| openapi/collector.go:514      | ProvideResponseJSONSchemas   | 98.1%    |
| openapi/collector.go:278      | setupOutput                  | 94.0%    |
| openapi/collector.go:224      | Collect                      | 92.4%    |
| openapi/collector.go:485      | ProvideRequestJSONSchemas    | 81.6%    |
| openapi/collector.go:537      | jsonSchemaCallback           | 77.8%    |
| openapi/collector.go:74       | Reflector                    | 75.0%    |
| openapi/collector.go:65       | Refl                         | 71.4%    |
| openapi/collector.go:460      | combineOCErrors              | 71.4%    |
| openapi/collector.go:47       | NewCollector                 | 0.0%     |
| openapi/collector.go:102      | AnnotateOperation            | 0.0%     |
| openapi/collector.go:114      | HasAnnotation                | 0.0%     |
| openapi/collector.go:123      | CollectOperation             | 0.0%     |
| openapi/collector.go:165      | CollectUseCase               | 0.0%     |
| request/factory.go            |                              | 100.0%   |
| request/factory.go:98         | MakeDecoder                  | 100.0%   |
| web/service.go                |                              | 75.0%    |
| web/service.go:25             | DefaultService               | 100.0%   |
| web/service.go:105            | OpenAPISchema                | 0.0%     |
| web/service.go:110            | OpenAPIReflector             | 0.0%     |

</details>

<details><summary>Coverage diff with base branch</summary>

|                        File                         |           Function           | Base Coverage | Current Coverage |
|-----------------------------------------------------|------------------------------|---------------|------------------|
| Total                                               |                              | 83.6%         | 82.4% (-1.2%)    |
| github.com/swaggest/rest/gorillamux/collector.go    | NewOpenAPICollector          | no function   | 100.0%           |
| github.com/swaggest/rest/gorillamux/collector.go    | Walker                       | no function   | 85.0%            |
| github.com/swaggest/rest/gorillamux/path_decoder.go | PathToURLValues              | no function   | 100.0%           |
| github.com/swaggest/rest/nethttp/handler.go         | NewHandler                   | 83.3%         | 87.5% (+4.2%)    |
| github.com/swaggest/rest/nethttp/openapi.go         | AnnotateOpenAPI              | 100.0%        | 0.0% (-100.0%)   |
| github.com/swaggest/rest/nethttp/openapi.go         | OpenAPIAnnotationsMiddleware | no function   | 100.0%           |
| github.com/swaggest/rest/nethttp/options.go         | AnnotateOpenAPIOperation     | no function   | 0.0%             |
| github.com/swaggest/rest/nethttp/options.go         | AnnotateOperation            | 100.0%        | 85.7% (-14.3%)   |
| github.com/swaggest/rest/openapi/collector.go       | AnnotateOperation            | no function   | 0.0%             |
| github.com/swaggest/rest/openapi/collector.go       | Collect                      | 70.4%         | 66.7% (-3.7%)    |
| github.com/swaggest/rest/openapi/collector.go       | CollectOperation             | no function   | 0.0%             |
| github.com/swaggest/rest/openapi/collector.go       | CollectUseCase               | no function   | 0.0%             |
| github.com/swaggest/rest/openapi/collector.go       | HasAnnotation                | no function   | 0.0%             |
| github.com/swaggest/rest/openapi/collector.go       | NewCollector                 | no function   | 0.0%             |
| github.com/swaggest/rest/openapi/collector.go       | ProvideRequestJSONSchemas    | 53.8%         | 75.0% (+21.2%)   |
| github.com/swaggest/rest/openapi/collector.go       | ProvideResponseJSONSchemas   | 66.7%         | 90.0% (+23.3%)   |
| github.com/swaggest/rest/openapi/collector.go       | Refl                         | no function   | 66.7%            |
| github.com/swaggest/rest/openapi/collector.go       | Reflector                    | 100.0%        | 80.0% (-20.0%)   |
| github.com/swaggest/rest/openapi/collector.go       | SpecSchema                   | no function   | 100.0%           |
| github.com/swaggest/rest/openapi/collector.go       | combineErrors                | 72.7%         | no function      |
| github.com/swaggest/rest/openapi/collector.go       | combineOCErrors              | no function   | 75.0%            |
| github.com/swaggest/rest/openapi/collector.go       | jsonSchemaCallback           | no function   | 78.6%            |
| github.com/swaggest/rest/openapi/collector.go       | paramSchema                  | 60.0%         | no function      |
| github.com/swaggest/rest/openapi/collector.go       | processExpectedErrors        | 87.5%         | no function      |
| github.com/swaggest/rest/openapi/collector.go       | processOCExpectedErrors      | no function   | 93.3%            |
| github.com/swaggest/rest/openapi/collector.go       | provideFormDataSchemas       | 0.0%          | no function      |
| github.com/swaggest/rest/openapi/collector.go       | provideHeaderSchemas         | 68.4%         | no function      |
| github.com/swaggest/rest/openapi/collector.go       | provideParametersJSONSchemas | 63.0%         | no function      |
| github.com/swaggest/rest/openapi/collector.go       | setFieldMapping              | no function   | 100.0%           |
| github.com/swaggest/rest/openapi/collector.go       | setJSONResponse              | 100.0%        | no function      |
| github.com/swaggest/rest/openapi/collector.go       | setOCJSONResponse            | no function   | 100.0%           |
| github.com/swaggest/rest/openapi/collector.go       | setRequestMapping            | 100.0%        | no function      |
| github.com/swaggest/rest/openapi/collector.go       | setupInput                   | 87.5%         | 100.0% (+12.5%)  |
| github.com/swaggest/rest/openapi/collector.go       | setupOutput                  | 74.1%         | 92.0% (+17.9%)   |
| github.com/swaggest/rest/web/service.go             | OpenAPIReflector             | no function   | 0.0%             |
| github.com/swaggest/rest/web/service.go             | OpenAPISchema                | no function   | 0.0%             |

</details>
<!-- Sticky Pull Request Commentunit-test -->
codecov[bot]: ## [Codecov](https://app.codecov.io/gh/swaggest/rest/pull/165?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=swaggest) Report
> Merging [#165](https://app.codecov.io/gh/swaggest/rest/pull/165?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=swaggest) (12b6bf6) into [master](https://app.codecov.io/gh/swaggest/rest/commit/18f55a444409183543f3e7a46652c19a01f38c69?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=swaggest) (18f55a4) will **decrease** coverage by `0.28%`.
> The diff coverage is `65.62%`.

```diff
@@            Coverage Diff             @@
##           master     #165      +/-   ##
==========================================
- Coverage   77.91%   77.63%   -0.28%     
==========================================
  Files          28       30       +2     
  Lines        1639     1677      +38     
==========================================
+ Hits         1277     1302      +25     
- Misses        239      269      +30     
+ Partials      123      106      -17     
```

| Flag | Coverage Δ | |
|---|---|---|
| unittests | `77.63% <65.62%> (-0.28%)` | :arrow_down: |

Flags with carried forward coverage won't be shown. [Click here](https://docs.codecov.io/docs/carryforward-flags?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=swaggest#carryforward-flags-in-the-pull-request-comment) to find out more.

| [Files Changed](https://app.codecov.io/gh/swaggest/rest/pull/165?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=swaggest) | Coverage Δ | |
|---|---|---|
| [jsonschema/validator.go](https://app.codecov.io/gh/swaggest/rest/pull/165?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=swaggest#diff-anNvbnNjaGVtYS92YWxpZGF0b3IuZ28=) | `80.19% <0.00%> (ø)` | |
| [trait.go](https://app.codecov.io/gh/swaggest/rest/pull/165?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=swaggest#diff-dHJhaXQuZ28=) | `85.18% <ø> (ø)` | |
| [openapi/collector.go](https://app.codecov.io/gh/swaggest/rest/pull/165?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=swaggest#diff-b3BlbmFwaS9jb2xsZWN0b3IuZ28=) | `62.74% <55.41%> (-0.26%)` | :arrow_down: |
| [web/service.go](https://app.codecov.io/gh/swaggest/rest/pull/165?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=swaggest#diff-d2ViL3NlcnZpY2UuZ28=) | `95.45% <60.00%> (-4.55%)` | :arrow_down: |
| [nethttp/options.go](https://app.codecov.io/gh/swaggest/rest/pull/165?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=swaggest#diff-bmV0aHR0cC9vcHRpb25zLmdv) | `84.00% <72.72%> (-4.89%)` | :arrow_down: |
| [gorillamux/collector.go](https://app.codecov.io/gh/swaggest/rest/pull/165?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=swaggest#diff-Z29yaWxsYW11eC9jb2xsZWN0b3IuZ28=) | `78.57% <78.57%> (ø)` | |
| [gorillamux/path\_decoder.go](https://app.codecov.io/gh/swaggest/rest/pull/165?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=swaggest#diff-Z29yaWxsYW11eC9wYXRoX2RlY29kZXIuZ28=) | `100.00% <100.00%> (ø)` | |
| [nethttp/handler.go](https://app.codecov.io/gh/swaggest/rest/pull/165?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=swaggest#diff-bmV0aHR0cC9oYW5kbGVyLmdv) | `85.22% <100.00%> (ø)` | |
| [nethttp/openapi.go](https://app.codecov.io/gh/swaggest/rest/pull/165?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=swaggest#diff-bmV0aHR0cC9vcGVuYXBpLmdv) | `80.72% <100.00%> (-13.49%)` | :arrow_down: |
| [request/factory.go](https://app.codecov.io/gh/swaggest/rest/pull/165?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=swaggest#diff-cmVxdWVzdC9mYWN0b3J5Lmdv) | `89.36% <100.00%> (ø)` | |

:mega: We’re building smart automated test selection to slash your CI/CD build times. [Learn more](https://about.codecov.io/iterative-testing/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=swaggest)

github-actions[bot]: ### Benchmark Result
<details><summary>Benchmark diff with base branch</summary>

```
name                                    old time/op    new time/op    delta
pkg:github.com/swaggest/rest/jsonschema goos:linux goarch:amd64
RequestValidator_ValidateRequestData-2    1.62µs ± 5%    1.76µs ± 3%  +8.23%  (p=0.004 n=6+5)
pkg:github.com/swaggest/rest/request goos:linux goarch:amd64
Decoder_Decode-2                           790ns ± 3%     856ns ± 1%  +8.40%  (p=0.008 n=5+5)
DecoderFunc_Decode-2                      2.30µs ± 5%    2.49µs ±14%  +8.59%  (p=0.026 n=6+6)
Decoder_Decode_json-2                     26.6µs ± 5%    27.6µs ± 3%    ~     (p=0.052 n=6+5)
Decoder_Decode_queryObject-2              5.43µs ± 5%    5.30µs ± 3%    ~     (p=0.180 n=6+6)
Decoder_Decode_jsonParam-2                2.06µs ± 5%    2.02µs ± 3%    ~     (p=0.240 n=6+6)
DecoderFactory_SetDecoderFunc-2           1.92µs ± 0%    2.00µs ± 0%  +4.29%  (p=0.016 n=4+5)
pkg:github.com/swaggest/rest/response/gzip goos:linux goarch:amd64
Middleware-2                              12.6µs ± 5%    12.4µs ± 2%    ~     (p=0.310 n=6+6)
Middleware_control-2                      4.23µs ± 3%    4.19µs ± 3%    ~     (p=0.537 n=5+6)

name                                    old alloc/op   new alloc/op   delta
pkg:github.com/swaggest/rest/jsonschema goos:linux goarch:amd64
RequestValidator_ValidateRequestData-2    2.46kB ± 0%    2.46kB ± 0%    ~     (all equal)
pkg:github.com/swaggest/rest/request goos:linux goarch:amd64
Decoder_Decode-2                            440B ± 0%      440B ± 0%    ~     (all equal)
DecoderFunc_Decode-2                      1.51kB ± 0%    1.51kB ± 0%    ~     (all equal)
Decoder_Decode_json-2                     12.2kB ± 0%    12.2kB ± 0%    ~     (all equal)
Decoder_Decode_queryObject-2              2.00kB ± 0%    2.00kB ± 0%    ~     (all equal)
Decoder_Decode_jsonParam-2                  736B ± 0%      736B ± 0%    ~     (all equal)
DecoderFactory_SetDecoderFunc-2           1.02kB ± 0%    1.02kB ± 0%    ~     (all equal)
pkg:github.com/swaggest/rest/response/gzip goos:linux goarch:amd64
Middleware-2                              1.16kB ± 2%    1.13kB ± 2%  -2.61%  (p=0.009 n=6+6)
Middleware_control-2                      11.2kB ± 0%    11.2kB ± 0%    ~     (all equal)

name                                    old allocs/op  new allocs/op  delta
pkg:github.com/swaggest/rest/jsonschema goos:linux goarch:amd64
RequestValidator_ValidateRequestData-2      8.00 ± 0%      8.00 ± 0%    ~     (all equal)
pkg:github.com/swaggest/rest/request goos:linux goarch:amd64
Decoder_Decode-2                            4.00 ± 0%      4.00 ± 0%    ~     (all equal)
DecoderFunc_Decode-2                        12.0 ± 0%      12.0 ± 0%    ~     (all equal)
Decoder_Decode_json-2                        177 ± 0%       177 ± 0%    ~     (all equal)
Decoder_Decode_queryObject-2                36.0 ± 0%      36.0 ± 0%    ~     (all equal)
Decoder_Decode_jsonParam-2                  13.0 ± 0%      13.0 ± 0%    ~     (all equal)
DecoderFactory_SetDecoderFunc-2             16.0 ± 0%      16.0 ± 0%    ~     (all equal)
pkg:github.com/swaggest/rest/response/gzip goos:linux goarch:amd64
Middleware-2                                11.0 ± 0%      11.0 ± 0%    ~     (all equal)
Middleware_control-2                        9.00 ± 0%      9.00 ± 0%    ~     (all equal)
```
</details>

<details><summary>Benchmark result</summary>

```
name                                    time/op
pkg:github.com/swaggest/rest/jsonschema goos:linux goarch:amd64
RequestValidator_ValidateRequestData-2  1.76µs ± 3%
pkg:github.com/swaggest/rest/request goos:linux goarch:amd64
Decoder_Decode-2                         856ns ± 1%
DecoderFunc_Decode-2                    2.49µs ±14%
Decoder_Decode_json-2                   27.6µs ± 3%
Decoder_Decode_queryObject-2            5.30µs ± 3%
Decoder_Decode_jsonParam-2              2.02µs ± 3%
DecoderFactory_SetDecoderFunc-2         2.00µs ± 0%
pkg:github.com/swaggest/rest/response/gzip goos:linux goarch:amd64
Middleware-2                            12.4µs ± 2%
Middleware_control-2                    4.19µs ± 3%

name                                    alloc/op
pkg:github.com/swaggest/rest/jsonschema goos:linux goarch:amd64
RequestValidator_ValidateRequestData-2  2.46kB ± 0%
pkg:github.com/swaggest/rest/request goos:linux goarch:amd64
Decoder_Decode-2                          440B ± 0%
DecoderFunc_Decode-2                    1.51kB ± 0%
Decoder_Decode_json-2                   12.2kB ± 0%
Decoder_Decode_queryObject-2            2.00kB ± 0%
Decoder_Decode_jsonParam-2                736B ± 0%
DecoderFactory_SetDecoderFunc-2         1.02kB ± 0%
pkg:github.com/swaggest/rest/response/gzip goos:linux goarch:amd64
Middleware-2                            1.13kB ± 2%
Middleware_control-2                    11.2kB ± 0%

name                                    allocs/op
pkg:github.com/swaggest/rest/jsonschema goos:linux goarch:amd64
RequestValidator_ValidateRequestData-2    8.00 ± 0%
pkg:github.com/swaggest/rest/request goos:linux goarch:amd64
Decoder_Decode-2                          4.00 ± 0%
DecoderFunc_Decode-2                      12.0 ± 0%
Decoder_Decode_json-2                      177 ± 0%
Decoder_Decode_queryObject-2              36.0 ± 0%
Decoder_Decode_jsonParam-2                13.0 ± 0%
DecoderFactory_SetDecoderFunc-2           16.0 ± 0%
pkg:github.com/swaggest/rest/response/gzip goos:linux goarch:amd64
Middleware-2                              11.0 ± 0%
Middleware_control-2                      9.00 ± 0%
```
</details>
<!-- Sticky Pull Request Commentbench -->
github-actions[bot]: ### Examples Benchmark Result
<details><summary>Benchmark diff with base branch</summary>

```
name                       old time/op    new time/op    delta
pkg:github.com/swaggest/rest/_examples/advanced goos:linux goarch:amd64
_directGzip-2                23.6µs ± 1%    24.7µs ± 6%   +4.68%  (p=0.008 n=5+5)
_directGzipHead-2            23.3µs ± 1%    23.7µs ± 1%   +1.72%  (p=0.008 n=5+5)
_noDirectGzip-2               133µs ± 1%     133µs ± 1%     ~     (p=1.000 n=5+5)
_directGzip_decode-2          481µs ± 1%     500µs ± 3%   +3.99%  (p=0.008 n=5+5)
_noDirectGzip_decode-2        134µs ± 1%     135µs ± 2%     ~     (p=0.421 n=5+5)
_jsonBody-2                  47.1µs ± 3%    49.8µs ± 5%   +5.79%  (p=0.016 n=5+5)
_jsonBodyValidation-2        55.7µs ± 1%    60.7µs ± 1%   +8.80%  (p=0.008 n=5+5)
_outputHeaders-2             24.4µs ± 2%    25.7µs ± 2%   +5.63%  (p=0.008 n=5+5)
_requestResponseMapping-2    49.0µs ± 1%    51.7µs ±15%     ~     (p=0.095 n=5+5)
_validation-2                52.8µs ± 2%    54.0µs ± 1%     ~     (p=0.056 n=5+5)
_noValidation-2              36.7µs ± 2%    37.5µs ± 2%   +2.37%  (p=0.032 n=5+5)
pkg:github.com/swaggest/rest/_examples/advanced-generic goos:linux goarch:amd64
_directGzip-2                24.5µs ± 6%    24.1µs ± 0%     ~     (p=0.730 n=5+4)
_directGzipHead-2            24.1µs ± 3%    24.8µs ± 2%     ~     (p=0.095 n=5+5)
_noDirectGzip-2               127µs ± 1%     128µs ± 1%     ~     (p=0.095 n=5+5)
_directGzip_decode-2          485µs ± 4%     475µs ± 1%     ~     (p=0.095 n=5+5)
_noDirectGzip_decode-2        129µs ± 1%     129µs ± 1%     ~     (p=0.548 n=5+5)
_htmlResponse-2              42.4µs ± 7%    41.9µs ± 1%     ~     (p=0.548 n=5+5)
_jsonBodyManual-2            29.7µs ± 3%    30.1µs ± 2%     ~     (p=0.151 n=5+5)
_jsonBody-2                  45.2µs ± 2%    44.0µs ± 2%   -2.75%  (p=0.032 n=5+5)
_jsonBodyValidation-2        59.0µs ± 2%    59.8µs ± 2%     ~     (p=0.310 n=5+5)
_outputHeaders-2             41.2µs ± 1%    42.9µs ± 3%   +4.06%  (p=0.008 n=5+5)
_requestResponseMapping-2    50.3µs ± 3%    51.1µs ± 2%     ~     (p=0.151 n=5+5)
_validation-2                52.2µs ± 1%    53.7µs ± 2%   +2.86%  (p=0.008 n=5+5)
_noValidation-2              37.6µs ± 2%    38.6µs ± 4%   +2.66%  (p=0.024 n=5+5)
pkg:github.com/swaggest/rest/_examples/task-api/internal/infra/nethttp goos:linux goarch:amd64
_notFoundSrv-2               26.7µs ± 1%    26.7µs ± 1%     ~     (p=0.905 n=5+4)
_ok-2                        26.5µs ± 1%    26.9µs ± 1%   +1.40%  (p=0.032 n=5+5)
_invalidBody-2               39.5µs ± 1%    39.0µs ± 2%     ~     (p=0.310 n=5+5)

name                       old 50%:ms     new 50%:ms     delta
pkg:github.com/swaggest/rest/_examples/advanced goos:linux goarch:amd64
_directGzip-2                  1.13 ± 7%      1.16 ± 8%     ~     (p=0.841 n=5+5)
_directGzipHead-2              1.12 ± 4%      1.13 ± 6%     ~     (p=1.000 n=5+5)
_noDirectGzip-2                6.59 ± 4%      6.36 ± 2%     ~     (p=0.063 n=5+5)
_directGzip_decode-2           18.2 ± 6%      19.3 ± 1%   +5.74%  (p=0.032 n=5+5)
_noDirectGzip_decode-2         6.52 ± 6%      6.54 ± 6%     ~     (p=0.841 n=5+5)
_jsonBody-2                    2.12 ± 5%      2.33 ± 5%   +9.88%  (p=0.016 n=5+5)
_jsonBodyValidation-2          2.53 ± 3%      2.90 ± 6%  +14.61%  (p=0.008 n=5+5)
_outputHeaders-2               1.13 ± 2%      1.19 ± 9%     ~     (p=0.151 n=5+5)
_requestResponseMapping-2      2.22 ± 4%      2.23 ± 7%     ~     (p=1.000 n=5+5)
_validation-2                  2.38 ± 6%      2.33 ± 0%     ~     (p=0.175 n=5+4)
_noValidation-2                1.67 ± 5%      1.67 ± 2%     ~     (p=0.690 n=5+5)
pkg:github.com/swaggest/rest/_examples/advanced-generic goos:linux goarch:amd64
_directGzip-2                  1.17 ± 7%      1.17 ± 8%     ~     (p=1.000 n=5+5)
_directGzipHead-2              1.12 ± 3%      1.13 ± 8%     ~     (p=0.730 n=5+5)
_noDirectGzip-2                6.22 ± 5%      6.17 ± 3%     ~     (p=1.000 n=5+5)
_directGzip_decode-2           18.4 ± 8%      18.1 ± 3%     ~     (p=0.841 n=5+5)
_noDirectGzip_decode-2         6.09 ± 2%      6.32 ± 4%   +3.79%  (p=0.016 n=5+5)
_htmlResponse-2                1.95 ± 6%      1.91 ± 3%     ~     (p=0.548 n=5+5)
_jsonBodyManual-2              1.37 ± 7%      1.40 ± 6%     ~     (p=0.421 n=5+5)
_jsonBody-2                    2.04 ± 5%      1.94 ± 4%   -4.92%  (p=0.040 n=5+5)
_jsonBodyValidation-2          2.63 ± 2%      2.72 ± 3%   +3.60%  (p=0.032 n=5+5)
_outputHeaders-2               1.74 ± 5%      1.91 ± 9%   +9.70%  (p=0.032 n=5+5)
_requestResponseMapping-2      2.19 ± 4%      2.17 ± 8%     ~     (p=0.421 n=5+5)
_validation-2                  2.28 ± 7%      2.46 ± 4%   +7.69%  (p=0.032 n=5+5)
_noValidation-2                1.61 ± 3%      1.69 ± 8%     ~     (p=0.310 n=5+5)
pkg:github.com/swaggest/rest/_examples/task-api/internal/infra/nethttp goos:linux goarch:amd64
_notFoundSrv-2                 1.30 ± 7%      1.30 ± 7%     ~     (p=0.897 n=5+5)
_ok-2                          1.31 ± 6%      1.29 ± 7%     ~     (p=0.690 n=5+5)
_invalidBody-2                 1.74 ± 3%      1.76 ± 5%     ~     (p=0.690 n=5+5)

name                       old 90%:ms     new 90%:ms     delta
pkg:github.com/swaggest/rest/_examples/advanced goos:linux goarch:amd64
_directGzip-2                  1.97 ± 3%      2.09 ±10%     ~     (p=0.151 n=5+5)
_directGzipHead-2              2.00 ± 6%      2.06 ± 4%     ~     (p=0.246 n=5+5)
_noDirectGzip-2                11.4 ± 4%      11.3 ± 4%     ~     (p=0.500 n=5+5)
_directGzip_decode-2           40.2 ± 6%      42.9 ± 7%     ~     (p=0.095 n=5+5)
_noDirectGzip_decode-2         11.6 ± 3%      11.5 ± 3%     ~     (p=0.500 n=5+5)
_jsonBody-2                    4.25 ± 5%      4.43 ± 7%     ~     (p=0.151 n=5+5)
_jsonBodyValidation-2          4.81 ± 3%      5.33 ± 5%  +10.78%  (p=0.008 n=5+5)
_outputHeaders-2               2.10 ± 7%      2.14 ± 8%     ~     (p=1.000 n=5+5)
_requestResponseMapping-2      4.34 ± 6%      4.60 ±14%     ~     (p=0.421 n=5+5)
_validation-2                  4.68 ± 3%      4.83 ± 4%     ~     (p=0.222 n=5+5)
_noValidation-2                3.44 ± 5%      3.55 ± 6%     ~     (p=0.222 n=5+5)
pkg:github.com/swaggest/rest/_examples/advanced-generic goos:linux goarch:amd64
_directGzip-2                  2.04 ± 5%      2.07 ± 5%     ~     (p=0.690 n=5+5)
_directGzipHead-2              2.13 ± 8%      2.20 ± 5%     ~     (p=0.222 n=5+5)
_noDirectGzip-2                11.0 ± 2%      11.1 ± 4%     ~     (p=0.841 n=5+5)
_directGzip_decode-2           41.3 ± 6%      41.5 ± 5%     ~     (p=1.000 n=5+5)
_noDirectGzip_decode-2         10.9 ± 3%      10.8 ± 4%     ~     (p=0.548 n=5+5)
_htmlResponse-2                3.80 ± 3%      3.76 ± 7%     ~     (p=0.548 n=5+5)
_jsonBodyManual-2              2.49 ± 5%      2.50 ± 3%     ~     (p=0.841 n=5+5)
_jsonBody-2                    4.39 ± 5%      4.22 ± 5%     ~     (p=0.151 n=5+5)
_jsonBodyValidation-2          5.19 ± 6%      5.35 ± 6%     ~     (p=0.421 n=5+5)
_outputHeaders-2               3.92 ± 5%      4.02 ± 6%     ~     (p=0.421 n=5+5)
_requestResponseMapping-2      4.53 ± 5%      4.78 ± 4%     ~     (p=0.056 n=5+5)
_validation-2                  4.73 ± 5%      4.96 ± 5%     ~     (p=0.151 n=5+5)
_noValidation-2                3.65 ± 4%      3.59 ± 3%     ~     (p=0.421 n=5+5)
pkg:github.com/swaggest/rest/_examples/task-api/internal/infra/nethttp goos:linux goarch:amd64
_notFoundSrv-2                 2.30 ± 7%      2.37 ± 9%     ~     (p=0.135 n=5+5)
_ok-2                          2.25 ± 6%      2.29 ± 6%     ~     (p=0.690 n=5+5)
_invalidBody-2                 3.70 ±10%      3.55 ± 6%     ~     (p=0.310 n=5+5)

name                       old 99%:ms     new 99%:ms     delta
pkg:github.com/swaggest/rest/_examples/advanced goos:linux goarch:amd64
_directGzip-2                  3.25 ± 2%      4.07 ±56%  +25.13%  (p=0.016 n=5+5)
_directGzipHead-2              3.22 ± 4%      3.40 ±12%     ~     (p=0.310 n=5+5)
_noDirectGzip-2                16.2 ± 3%      15.6 ± 4%     ~     (p=0.056 n=5+5)
_directGzip_decode-2           65.5 ±10%      69.6 ±10%     ~     (p=0.310 n=5+5)
_noDirectGzip_decode-2         15.4 ± 1%      15.7 ± 6%     ~     (p=0.206 n=4+5)
_jsonBody-2                    5.99 ± 6%      6.93 ±18%  +15.70%  (p=0.008 n=5+5)
_jsonBodyValidation-2          6.93 ± 4%      8.33 ±17%  +20.24%  (p=0.008 n=5+5)
_outputHeaders-2               4.13 ±29%      4.71 ±15%     ~     (p=0.095 n=5+5)
_requestResponseMapping-2      8.02 ±11%      9.29 ±46%     ~     (p=0.222 n=5+5)
_validation-2                  8.26 ±36%      9.45 ±17%     ~     (p=0.310 n=5+5)
_noValidation-2                6.17 ±16%      6.15 ±15%     ~     (p=1.000 n=5+5)
pkg:github.com/swaggest/rest/_examples/advanced-generic goos:linux goarch:amd64
_directGzip-2                  3.87 ±42%      3.36 ± 2%     ~     (p=0.286 n=5+4)
_directGzipHead-2              3.52 ±13%      4.08 ±16%  +15.88%  (p=0.032 n=5+5)
_noDirectGzip-2                15.2 ± 6%      15.1 ± 7%     ~     (p=1.000 n=5+5)
_directGzip_decode-2           71.9 ±17%      67.8 ±11%     ~     (p=0.841 n=5+5)
_noDirectGzip_decode-2         14.7 ± 3%      15.0 ± 8%     ~     (p=1.000 n=5+5)
_htmlResponse-2                7.21 ±39%      6.55 ±18%     ~     (p=0.690 n=5+5)
_jsonBodyManual-2              4.85 ± 9%      4.80 ± 9%     ~     (p=1.000 n=5+5)
_jsonBody-2                    7.18 ±23%      6.53 ±11%     ~     (p=0.222 n=5+5)
_jsonBodyValidation-2          8.75 ±15%      8.54 ±12%     ~     (p=0.690 n=5+5)
_outputHeaders-2               6.16 ± 9%      6.57 ± 7%     ~     (p=0.095 n=5+5)
_requestResponseMapping-2      7.47 ±24%      7.60 ±11%     ~     (p=0.548 n=5+5)
_validation-2                  6.89 ± 2%      7.30 ± 5%     ~     (p=0.056 n=5+5)
_noValidation-2                6.10 ± 7%      6.54 ± 9%     ~     (p=0.151 n=5+5)
pkg:github.com/swaggest/rest/_examples/task-api/internal/infra/nethttp goos:linux goarch:amd64
_notFoundSrv-2                 3.65 ± 6%      3.66 ± 3%     ~     (p=0.905 n=5+4)
_ok-2                          3.73 ±11%      3.77 ± 3%     ~     (p=0.841 n=5+5)
_invalidBody-2                 6.49 ±17%      6.31 ± 9%     ~     (p=0.841 n=5+5)

name                       old 99.9%:ms   new 99.9%:ms   delta
pkg:github.com/swaggest/rest/_examples/advanced goos:linux goarch:amd64
_directGzip-2                  6.06 ±14%      8.55 ±59%     ~     (p=0.151 n=5+5)
_directGzipHead-2              5.63 ±37%      6.79 ±45%     ~     (p=0.222 n=5+5)
_noDirectGzip-2                21.1 ± 7%      18.1 ± 6%  -14.23%  (p=0.008 n=5+5)
_directGzip_decode-2           82.1 ± 7%      92.7 ±11%     ~     (p=0.095 n=5+5)
_noDirectGzip_decode-2         21.2 ±12%      19.8 ± 7%     ~     (p=0.421 n=5+5)
_jsonBody-2                    9.66 ±74%     12.26 ±32%     ~     (p=0.222 n=5+5)
_jsonBodyValidation-2          12.6 ±18%      12.6 ±25%     ~     (p=1.000 n=5+5)
_outputHeaders-2               10.3 ±26%       9.4 ±25%     ~     (p=0.548 n=5+5)
_requestResponseMapping-2      12.7 ±13%      14.8 ±39%     ~     (p=0.310 n=5+5)
_validation-2                  14.9 ±34%      15.0 ±20%     ~     (p=0.841 n=5+5)
_noValidation-2                10.8 ±15%      13.0 ±32%     ~     (p=0.222 n=5+5)
pkg:github.com/swaggest/rest/_examples/advanced-generic goos:linux goarch:amd64
_directGzip-2                  9.54 ±44%      5.93 ± 4%     ~     (p=0.190 n=5+4)
_directGzipHead-2              7.63 ±47%      8.78 ±37%     ~     (p=0.841 n=5+5)
_noDirectGzip-2                19.3 ±18%      18.3 ± 4%     ~     (p=0.548 n=5+5)
_directGzip_decode-2           91.2 ±22%      86.7 ±14%     ~     (p=0.548 n=5+5)
_noDirectGzip_decode-2         20.2 ±11%      17.8 ± 6%  -11.48%  (p=0.016 n=5+5)
_htmlResponse-2                13.3 ±74%      11.9 ±16%     ~     (p=1.000 n=5+5)
_jsonBodyManual-2              11.8 ±20%      11.2 ±19%     ~     (p=0.841 n=5+5)
_jsonBody-2                    12.7 ±27%      12.2 ±38%     ~     (p=0.841 n=5+5)
_jsonBodyValidation-2          15.4 ± 5%      14.7 ±10%     ~     (p=0.548 n=5+5)
_outputHeaders-2               10.6 ±70%      11.1 ±18%     ~     (p=0.421 n=5+5)
_requestResponseMapping-2      14.1 ±35%      14.0 ±12%     ~     (p=0.690 n=5+5)
_validation-2                  13.2 ±24%      13.8 ±28%     ~     (p=0.841 n=5+5)
_noValidation-2                10.0 ±26%      15.7 ±19%  +57.24%  (p=0.008 n=5+5)
pkg:github.com/swaggest/rest/_examples/task-api/internal/infra/nethttp goos:linux goarch:amd64
_notFoundSrv-2                 9.81 ±49%      8.13 ±70%     ~     (p=0.310 n=5+5)
_ok-2                          9.54 ±46%      7.08 ±41%     ~     (p=0.310 n=5+5)
_invalidBody-2                 12.1 ±86%      11.1 ±87%     ~     (p=1.000 n=5+5)

name                       old B:rcvd/op  new B:rcvd/op  delta
pkg:github.com/swaggest/rest/_examples/advanced goos:linux goarch:amd64
_directGzip-2                   630 ± 0%       630 ± 0%     ~     (all equal)
_directGzipHead-2               174 ± 0%       174 ± 0%     ~     (all equal)
_noDirectGzip-2               1.03k ± 0%     1.03k ± 0%     ~     (all equal)
_directGzip_decode-2            630 ± 0%       630 ± 0%     ~     (all equal)
_noDirectGzip_decode-2        1.03k ± 0%     1.03k ± 0%     ~     (all equal)
_jsonBody-2                     199 ± 0%       199 ± 0%     ~     (all equal)
_jsonBodyValidation-2           185 ± 0%       185 ± 0%     ~     (all equal)
_outputHeaders-2                146 ± 0%       146 ± 0%     ~     (all equal)
_requestResponseMapping-2      94.0 ± 0%      94.0 ± 0%     ~     (all equal)
_validation-2                   168 ± 0%       168 ± 0%     ~     (all equal)
_noValidation-2                 168 ± 0%       168 ± 0%     ~     (all equal)
pkg:github.com/swaggest/rest/_examples/advanced-generic goos:linux goarch:amd64
_directGzip-2                   638 ± 0%       638 ± 0%     ~     (all equal)
_directGzipHead-2               182 ± 0%       182 ± 0%     ~     (all equal)
_noDirectGzip-2               1.04k ± 0%     1.04k ± 0%     ~     (all equal)
_directGzip_decode-2            638 ± 0%       638 ± 0%     ~     (all equal)
_noDirectGzip_decode-2        1.04k ± 0%     1.04k ± 0%     ~     (all equal)
_htmlResponse-2                 355 ± 0%       355 ± 0%     ~     (all equal)
_jsonBodyManual-2               207 ± 0%       207 ± 0%     ~     (all equal)
_jsonBody-2                     207 ± 0%       207 ± 0%     ~     (all equal)
_jsonBodyValidation-2           193 ± 0%       193 ± 0%     ~     (all equal)
_outputHeaders-2                214 ± 0%       214 ± 0%     ~     (all equal)
_requestResponseMapping-2       108 ± 0%       108 ± 0%     ~     (all equal)
_validation-2                   176 ± 0%       176 ± 0%     ~     (all equal)
_noValidation-2                 176 ± 0%       176 ± 0%     ~     (all equal)
pkg:github.com/swaggest/rest/_examples/task-api/internal/infra/nethttp goos:linux goarch:amd64
_notFoundSrv-2                  322 ± 0%       322 ± 0%     ~     (all equal)
_ok-2                           344 ± 0%       344 ± 0%     ~     (all equal)
_invalidBody-2                  420 ± 0%       420 ± 0%     ~     (all equal)

name                       old B:sent/op  new B:sent/op  delta
pkg:github.com/swaggest/rest/_examples/advanced goos:linux goarch:amd64
_directGzip-2                   103 ± 0%       103 ± 0%     ~     (all equal)
_directGzipHead-2               104 ± 0%       104 ± 0%     ~     (all equal)
_noDirectGzip-2                 117 ± 0%       117 ± 0%     ~     (all equal)
_directGzip_decode-2            116 ± 0%       116 ± 0%     ~     (all equal)
_noDirectGzip_decode-2          130 ± 0%       130 ± 0%     ~     (all equal)
_jsonBody-2                     188 ± 0%       188 ± 0%     ~     (all equal)
_jsonBodyValidation-2           192 ± 0%       192 ± 0%     ~     (all equal)
_outputHeaders-2               77.0 ± 0%      77.0 ± 0%     ~     (all equal)
_requestResponseMapping-2       169 ± 0%       169 ± 0%     ~     (all equal)
_validation-2                   170 ± 0%       170 ± 0%     ~     (all equal)
_noValidation-2                 173 ± 0%       173 ± 0%     ~     (all equal)
pkg:github.com/swaggest/rest/_examples/advanced-generic goos:linux goarch:amd64
_directGzip-2                   103 ± 0%       103 ± 0%     ~     (all equal)
_directGzipHead-2               104 ± 0%       104 ± 0%     ~     (all equal)
_noDirectGzip-2                 117 ± 0%       117 ± 0%     ~     (all equal)
_directGzip_decode-2            116 ± 0%       116 ± 0%     ~     (all equal)
_noDirectGzip_decode-2          130 ± 0%       130 ± 0%     ~     (all equal)
_htmlResponse-2                 108 ± 0%       108 ± 0%     ~     (all equal)
_jsonBodyManual-2               195 ± 0%       195 ± 0%     ~     (all equal)
_jsonBody-2                     188 ± 0%       188 ± 0%     ~     (all equal)
_jsonBodyValidation-2           192 ± 0%       192 ± 0%     ~     (all equal)
_outputHeaders-2               88.0 ± 0%      88.0 ± 0%     ~     (all equal)
_requestResponseMapping-2       169 ± 0%       169 ± 0%     ~     (all equal)
_validation-2                   170 ± 0%       170 ± 0%     ~     (all equal)
_noValidation-2                 173 ± 0%       173 ± 0%     ~     (all equal)
pkg:github.com/swaggest/rest/_examples/task-api/internal/infra/nethttp goos:linux goarch:amd64
_notFoundSrv-2                 74.0 ± 0%      74.0 ± 0%     ~     (all equal)
_ok-2                          74.0 ± 0%      74.0 ± 0%     ~     (all equal)
_invalidBody-2                  137 ± 0%       137 ± 0%     ~     (all equal)

name                       old rps        new rps        delta
pkg:github.com/swaggest/rest/_examples/advanced goos:linux goarch:amd64
_directGzip-2                 42.4k ± 1%     40.5k ± 6%   -4.40%  (p=0.008 n=5+5)
_directGzipHead-2             42.9k ± 1%     42.2k ± 1%   -1.69%  (p=0.008 n=5+5)
_noDirectGzip-2               7.54k ± 1%     7.55k ± 1%     ~     (p=0.937 n=5+5)
_directGzip_decode-2          2.08k ± 1%     2.00k ± 3%   -3.83%  (p=0.008 n=5+5)
_noDirectGzip_decode-2        7.49k ± 1%     7.42k ± 2%     ~     (p=0.421 n=5+5)
_jsonBody-2                   21.2k ± 3%     20.1k ± 5%   -5.42%  (p=0.016 n=5+5)
_jsonBodyValidation-2         17.9k ± 1%     16.5k ± 1%   -8.09%  (p=0.008 n=5+5)
_outputHeaders-2              41.0k ± 2%     38.9k ± 2%   -5.33%  (p=0.008 n=5+5)
_requestResponseMapping-2     20.4k ± 1%     19.4k ±14%     ~     (p=0.095 n=5+5)
_validation-2                 18.9k ± 2%     18.5k ± 1%     ~     (p=0.056 n=5+5)
_noValidation-2               27.3k ± 2%     26.6k ± 2%   -2.32%  (p=0.032 n=5+5)
pkg:github.com/swaggest/rest/_examples/advanced-generic goos:linux goarch:amd64
_directGzip-2                 40.8k ± 6%     41.4k ± 0%     ~     (p=0.730 n=5+4)
_directGzipHead-2             41.5k ± 3%     40.4k ± 2%     ~     (p=0.095 n=5+5)
_noDirectGzip-2               7.88k ± 1%     7.80k ± 1%     ~     (p=0.095 n=5+5)
_directGzip_decode-2          2.06k ± 4%     2.10k ± 1%     ~     (p=0.119 n=5+5)
_noDirectGzip_decode-2        7.74k ± 1%     7.77k ± 1%     ~     (p=0.548 n=5+5)
_htmlResponse-2               23.6k ± 7%     23.9k ± 1%     ~     (p=0.548 n=5+5)
_jsonBodyManual-2             33.6k ± 3%     33.3k ± 2%     ~     (p=0.151 n=5+5)
_jsonBody-2                   22.1k ± 2%     22.7k ± 2%   +2.82%  (p=0.024 n=5+5)
_jsonBodyValidation-2         16.9k ± 2%     16.7k ± 2%     ~     (p=0.310 n=5+5)
_outputHeaders-2              24.3k ± 1%     23.3k ± 3%   -3.88%  (p=0.008 n=5+5)
_requestResponseMapping-2     19.9k ± 3%     19.6k ± 2%     ~     (p=0.151 n=5+5)
_validation-2                 19.1k ± 1%     18.6k ± 2%   -2.77%  (p=0.008 n=5+5)
_noValidation-2               26.6k ± 2%     25.9k ± 4%   -2.57%  (p=0.016 n=5+5)
pkg:github.com/swaggest/rest/_examples/task-api/internal/infra/nethttp goos:linux goarch:amd64
_notFoundSrv-2                37.5k ± 1%     37.4k ± 1%     ~     (p=0.905 n=5+4)
_ok-2                         37.7k ± 1%     37.2k ± 1%   -1.38%  (p=0.032 n=5+5)
_invalidBody-2                25.3k ± 1%     25.6k ± 2%     ~     (p=0.310 n=5+5)

name                       old alloc/op   new alloc/op   delta
pkg:github.com/swaggest/rest/_examples/advanced goos:linux goarch:amd64
_directGzip-2                3.85kB ± 0%    3.85kB ± 0%     ~     (p=0.397 n=5+4)
_directGzipHead-2            3.84kB ± 0%    3.84kB ± 0%     ~     (p=0.381 n=5+5)
_noDirectGzip-2              6.38kB ±20%    6.63kB ±14%     ~     (p=0.690 n=5+5)
_directGzip_decode-2          403kB ± 0%     403kB ± 0%     ~     (p=0.151 n=5+5)
_noDirectGzip_decode-2       5.80kB ±15%    5.84kB ± 5%     ~     (p=0.841 n=5+5)
_jsonBody-2                  13.1kB ± 0%    13.1kB ± 0%     ~     (p=0.127 n=5+5)
_jsonBodyValidation-2        18.9kB ± 0%    18.9kB ± 0%   +0.06%  (p=0.008 n=5+5)
_outputHeaders-2             3.56kB ± 0%    3.56kB ± 0%     ~     (p=0.460 n=5+5)
_requestResponseMapping-2    16.6kB ± 0%    16.6kB ± 0%     ~     (p=0.452 n=5+5)
_validation-2                16.5kB ± 0%    16.5kB ± 0%     ~     (p=0.468 n=5+5)
_noValidation-2              7.75kB ± 0%    7.75kB ± 0%     ~     (p=0.548 n=5+5)
pkg:github.com/swaggest/rest/_examples/advanced-generic goos:linux goarch:amd64
_directGzip-2                3.88kB ± 0%    3.88kB ± 0%     ~     (p=1.000 n=5+5)
_directGzipHead-2            3.88kB ± 0%    3.88kB ± 0%     ~     (p=0.738 n=5+5)
_noDirectGzip-2              6.42kB ±19%    7.02kB ±23%     ~     (p=0.421 n=5+5)
_directGzip_decode-2          403kB ± 0%     403kB ± 0%     ~     (p=0.095 n=5+5)
_noDirectGzip_decode-2       5.92kB ± 8%    5.84kB ± 2%     ~     (p=0.841 n=5+5)
_htmlResponse-2              8.02kB ± 0%    8.05kB ± 0%   +0.41%  (p=0.008 n=5+5)
_jsonBodyManual-2            4.48kB ± 0%    4.48kB ± 0%     ~     (p=0.937 n=5+5)
_jsonBody-2                  10.2kB ± 0%    10.2kB ± 0%     ~     (p=0.270 n=5+5)
_jsonBodyValidation-2        19.0kB ± 0%    19.0kB ± 0%     ~     (p=0.198 n=5+5)
_outputHeaders-2             10.4kB ± 0%    10.4kB ± 0%   +0.04%  (p=0.040 n=5+5)
_requestResponseMapping-2    16.6kB ± 0%    16.6kB ± 0%   -0.06%  (p=0.016 n=5+5)
_validation-2                16.5kB ± 0%    16.5kB ± 0%     ~     (p=0.690 n=5+5)
_noValidation-2              7.81kB ± 0%    7.81kB ± 0%     ~     (p=0.587 n=5+5)
pkg:github.com/swaggest/rest/_examples/task-api/internal/infra/nethttp goos:linux goarch:amd64
_notFoundSrv-2               4.63kB ± 0%    4.63kB ± 0%     ~     (p=0.119 n=5+5)
_ok-2                        4.54kB ± 0%    4.54kB ± 0%     ~     (p=0.886 n=4+4)
_invalidBody-2               8.25kB ± 0%    8.25kB ± 0%     ~     (p=1.000 n=4+4)

name                       old allocs/op  new allocs/op  delta
pkg:github.com/swaggest/rest/_examples/advanced goos:linux goarch:amd64
_directGzip-2                  42.0 ± 0%      42.0 ± 0%     ~     (all equal)
_directGzipHead-2              42.0 ± 0%      42.0 ± 0%     ~     (all equal)
_noDirectGzip-2                49.6 ± 3%      49.6 ± 3%     ~     (p=1.000 n=5+5)
_directGzip_decode-2            504 ± 0%       504 ± 0%     ~     (p=1.000 n=5+4)
_noDirectGzip_decode-2         50.0 ± 0%      50.0 ± 0%     ~     (all equal)
_jsonBody-2                     128 ± 0%       128 ± 0%     ~     (all equal)
_jsonBodyValidation-2           185 ± 0%       185 ± 0%     ~     (all equal)
_outputHeaders-2               36.0 ± 0%      36.0 ± 0%     ~     (all equal)
_requestResponseMapping-2       124 ± 0%       124 ± 0%     ~     (all equal)
_validation-2                   154 ± 0%       154 ± 0%     ~     (all equal)
_noValidation-2                91.0 ± 0%      91.0 ± 0%     ~     (all equal)
pkg:github.com/swaggest/rest/_examples/advanced-generic goos:linux goarch:amd64
_directGzip-2                  44.0 ± 0%      44.0 ± 0%     ~     (all equal)
_directGzipHead-2              44.0 ± 0%      44.0 ± 0%     ~     (all equal)
_noDirectGzip-2                51.4 ± 3%      51.6 ± 3%     ~     (p=1.000 n=5+5)
_directGzip_decode-2            506 ± 0%       505 ± 0%     ~     (p=0.143 n=5+5)
_noDirectGzip_decode-2         52.0 ± 0%      52.0 ± 0%     ~     (all equal)
_htmlResponse-2                 145 ± 0%       145 ± 0%     ~     (all equal)
_jsonBodyManual-2              49.0 ± 0%      49.0 ± 0%     ~     (all equal)
_jsonBody-2                     100 ± 0%       100 ± 0%     ~     (all equal)
_jsonBodyValidation-2           187 ± 0%       187 ± 0%     ~     (all equal)
_outputHeaders-2                113 ± 0%       113 ± 0%     ~     (all equal)
_requestResponseMapping-2       126 ± 0%       126 ± 0%     ~     (all equal)
_validation-2                   156 ± 0%       156 ± 0%     ~     (all equal)
_noValidation-2                93.0 ± 0%      93.0 ± 0%     ~     (all equal)
pkg:github.com/swaggest/rest/_examples/task-api/internal/infra/nethttp goos:linux goarch:amd64
_notFoundSrv-2                 53.0 ± 0%      53.0 ± 0%     ~     (all equal)
_ok-2                          49.0 ± 0%      49.0 ± 0%     ~     (all equal)
_invalidBody-2                 97.0 ± 0%      97.0 ± 0%     ~     (all equal)
```
</details>
<!-- Sticky Pull Request Commentbench-examples -->

Patch:
diff --git a/README.md b/README.md
index fe8d5e1..b442972 100644
--- a/README.md
+++ b/README.md
@@ -452,6 +452,10 @@ func main() {
 
 ![Documentation Page](./_examples/basic/screen.png)
 
+## Additional Integrations
+
+* [`github.com/gorilla/mux`](https://github.com/gorilla/mux), see [example](./gorillamux/example_openapi_collector_test.go).
+
 ## Performance Optimization
 
 If top performance is critical for the service or particular endpoints, you can trade 
diff --git a/_examples/.golangci.yml b/_examples/.golangci.yml
index 97710e1..5e23975 100644
--- a/_examples/.golangci.yml
+++ b/_examples/.golangci.yml
@@ -20,6 +20,11 @@ linters-settings:
 linters:
   enable-all: true
   disable:
+    - funlen
+    - exhaustruct
+    - musttag
+    - nonamedreturns
+    - goerr113
     - lll
     - maligned
     - gochecknoglobals
diff --git a/_examples/advanced-generic/gzip_pass_through.go b/_examples/advanced-generic/gzip_pass_through.go
index d2a5c42..f09d3b7 100644
--- a/_examples/advanced-generic/gzip_pass_through.go
+++ b/_examples/advanced-generic/gzip_pass_through.go
@@ -62,6 +62,7 @@ func directGzip() usecase.Interactor {
 
 	// Precompute compressed data container. Generally this step should be owned by a caching storage of data.
 	dataFromCache := gzipPassThroughContainer{}
+
 	err := dataFromCache.PackJSON(rawData)
 	if err != nil {
 		panic(err)
diff --git a/_examples/advanced-generic/json_slice_body.go b/_examples/advanced-generic/json_slice_body.go
index aa09884..13be45c 100644
--- a/_examples/advanced-generic/json_slice_body.go
+++ b/_examples/advanced-generic/json_slice_body.go
@@ -9,6 +9,7 @@ import (
 	"github.com/swaggest/usecase"
 )
 
+// JSONSlicePayload is an example non-scalar type without `json` tags.
 type JSONSlicePayload []int
 
 type jsonSliceReq struct {
diff --git a/_examples/advanced-generic/output_writer.go b/_examples/advanced-generic/output_writer.go
index fb832f3..61f88cc 100644
--- a/_examples/advanced-generic/output_writer.go
+++ b/_examples/advanced-generic/output_writer.go
@@ -34,6 +34,7 @@ func outputCSVWriter() usecase.Interactor {
 		out.ContentHash = contentHash
 
 		c := csv.NewWriter(out)
+
 		return c.WriteAll([][]string{{"abc", "def", "hij"}, {"klm", "nop", "qrs"}})
 	})
 
diff --git a/_examples/advanced-generic/router.go b/_examples/advanced-generic/router.go
index 7ec26ff..ba2e29e 100644
--- a/_examples/advanced-generic/router.go
+++ b/_examples/advanced-generic/router.go
@@ -14,6 +14,7 @@ import (
 	"github.com/google/uuid"
 	"github.com/rs/cors"
 	"github.com/swaggest/jsonschema-go"
+	oapi "github.com/swaggest/openapi-go"
 	"github.com/swaggest/openapi-go/openapi3"
 	"github.com/swaggest/rest"
 	"github.com/swaggest/rest/nethttp"
@@ -27,12 +28,17 @@ import (
 func NewRouter() http.Handler {
 	s := web.DefaultService()
 
-	s.OpenAPI.Info.Title = "Advanced Example"
-	s.OpenAPI.Info.WithDescription("This app showcases a variety of features.")
-	s.OpenAPI.Info.Version = "v1.2.3"
-	s.OpenAPICollector.Reflector().InterceptDefName(func(t reflect.Type, defaultDefName string) string {
-		return strings.ReplaceAll(defaultDefName, "Generic", "")
-	})
+	s.OpenAPISchema().SetTitle("Advanced Example")
+	s.OpenAPISchema().SetDescription("This app showcases a variety of features.")
+	s.OpenAPISchema().SetVersion("v1.2.3")
+
+	jsr := s.OpenAPIReflector().JSONSchemaReflector()
+
+	jsr.DefaultOptions = append(jsr.DefaultOptions, jsonschema.InterceptDefName(
+		func(t reflect.Type, defaultDefName string) string {
+			return strings.ReplaceAll(defaultDefName, "Generic", "")
+		},
+	))
 
 	// Usecase middlewares can be added to web.Service or chirouter.Wrapper.
 	s.Wrap(nethttp.UseCaseMiddlewares(usecase.MiddlewareFunc(func(next usecase.Interactor) usecase.Interactor {
@@ -47,7 +53,7 @@ func NewRouter() http.Handler {
 
 		return usecase.Interact(func(ctx context.Context, input, output interface{}) error {
 			err := next.Interact(ctx, input, output)
-			if err != nil && err != rest.HTTPCodeAsError(http.StatusNotModified) {
+			if err != nil && !errors.Is(err, rest.HTTPCodeAsError(http.StatusNotModified)) {
 				log.Printf("usecase %s request (%+v) failed: %v\n", name, input, err)
 			}
 
@@ -56,11 +62,11 @@ func NewRouter() http.Handler {
 	})))
 
 	// An example of global schema override to disable additionalProperties for all object schemas.
-	s.OpenAPICollector.Reflector().DefaultOptions = append(s.OpenAPICollector.Reflector().DefaultOptions,
+	jsr.DefaultOptions = append(jsr.DefaultOptions,
 		jsonschema.InterceptSchema(func(params jsonschema.InterceptSchemaParams) (stop bool, err error) {
 			// Allow unknown request headers and skip response.
-			if oc, ok := openapi3.OperationCtx(params.Context); !params.Processed || !ok ||
-				oc.ProcessingResponse || oc.ProcessingIn == string(rest.ParamInHeader) {
+			if oc, ok := oapi.OperationCtx(params.Context); !params.Processed || !ok ||
+				oc.IsProcessingResponse() || oc.ProcessingIn() == oapi.InHeader {
 				return false, nil
 			}
 
@@ -79,7 +85,7 @@ func NewRouter() http.Handler {
 	uuidDef.AddType(jsonschema.String)
 	uuidDef.WithFormat("uuid")
 	uuidDef.WithExamples("248df4b7-aa70-47b8-a036-33ac447e668d")
-	s.OpenAPICollector.Reflector().AddTypeMapping(uuid.UUID{}, uuidDef)
+	jsr.AddTypeMapping(uuid.UUID{}, uuidDef)
 
 	// When multiple structures can be returned with the same HTTP status code, it is possible to combine them into a
 	// single schema with such configuration.
@@ -130,7 +136,14 @@ func NewRouter() http.Handler {
 	)
 
 	// Annotations can be used to alter documentation of operation identified by method and path.
-	s.OpenAPICollector.Annotate(http.MethodPost, "/validation", func(op *openapi3.Operation) error {
+	s.OpenAPICollector.AnnotateOperation(http.MethodPost, "/validation", func(oc oapi.OperationContext) error {
+		o3, ok := oc.(openapi3.OperationExposer)
+		if !ok {
+			return nil
+		}
+
+		op := o3.Operation()
+
 		if op.Description != nil {
 			*op.Description = *op.Description + " Custom annotation."
 		}
@@ -153,8 +166,8 @@ func NewRouter() http.Handler {
 
 	s.Post("/json-map-body", jsonMapBody(),
 		// Annotate operation to add post-processing if necessary.
-		nethttp.AnnotateOperation(func(op *openapi3.Operation) error {
-			op.WithDescription("Request with JSON object (map) body.")
+		nethttp.AnnotateOpenAPIOperation(func(oc oapi.OperationContext) error {
+			oc.SetDescription("Request with JSON object (map) body.")
 
 			return nil
 		}))
@@ -181,7 +194,7 @@ func NewRouter() http.Handler {
 	s.Post("/no-validation", noValidation())
 
 	// Type mapping is necessary to pass interface as structure into documentation.
-	s.OpenAPICollector.Reflector().AddTypeMapping(new(gzipPassThroughOutput), new(gzipPassThroughStruct))
+	jsr.AddTypeMapping(new(gzipPassThroughOutput), new(gzipPassThroughStruct))
 	s.Get("/gzip-pass-through", directGzip())
 	s.Head("/gzip-pass-through", directGzip())
 
@@ -197,6 +210,8 @@ func NewRouter() http.Handler {
 			if c, err := r.Cookie("sessid"); err == nil {
 				r = r.WithContext(context.WithValue(r.Context(), "sessionID", c.Value))
 			}
+
+			handler.ServeHTTP(w, r)
 		})
 	}
 
diff --git a/_examples/advanced/gzip_pass_through.go b/_examples/advanced/gzip_pass_through.go
index 4b597ef..8611277 100644
--- a/_examples/advanced/gzip_pass_through.go
+++ b/_examples/advanced/gzip_pass_through.go
@@ -60,6 +60,7 @@ func directGzip() usecase.Interactor {
 
 	// Precompute compressed data container. Generally this step should be owned by a caching storage of data.
 	dataFromCache := gzipPassThroughContainer{}
+
 	err := dataFromCache.PackJSON(rawData)
 	if err != nil {
 		panic(err)
diff --git a/_examples/advanced/output_writer.go b/_examples/advanced/output_writer.go
index 14eb1f2..6481d14 100644
--- a/_examples/advanced/output_writer.go
+++ b/_examples/advanced/output_writer.go
@@ -32,6 +32,7 @@ func outputCSVWriter() usecase.Interactor {
 		out.Header = "abc"
 
 		c := csv.NewWriter(out)
+
 		return c.WriteAll([][]string{{"abc", "def", "hij"}, {"klm", "nop", "qrs"}})
 	})
 
diff --git a/_examples/advanced/router.go b/_examples/advanced/router.go
index fd5335b..8584c47 100644
--- a/_examples/advanced/router.go
+++ b/_examples/advanced/router.go
@@ -8,6 +8,7 @@ import (
 	"github.com/go-chi/chi/v5"
 	"github.com/google/uuid"
 	"github.com/swaggest/jsonschema-go"
+	oapi "github.com/swaggest/openapi-go"
 	"github.com/swaggest/openapi-go/openapi3"
 	"github.com/swaggest/rest"
 	"github.com/swaggest/rest/nethttp"
@@ -23,16 +24,17 @@ func NewRouter() http.Handler {
 
 	s := web.DefaultService()
 
-	s.OpenAPI.Info.Title = "Advanced Example"
-	s.OpenAPI.Info.WithDescription("This app showcases a variety of features.")
-	s.OpenAPI.Info.Version = "v1.2.3"
+	s.OpenAPISchema().SetTitle("Advanced Example")
+	s.OpenAPISchema().SetDescription("This app showcases a variety of features.")
+	s.OpenAPISchema().SetVersion("v1.2.3")
 
 	// An example of global schema override to disable additionalProperties for all object schemas.
-	s.OpenAPICollector.Reflector().DefaultOptions = append(s.OpenAPICollector.Reflector().DefaultOptions,
+	jsr := s.OpenAPIReflector().JSONSchemaReflector()
+	jsr.DefaultOptions = append(jsr.DefaultOptions,
 		jsonschema.InterceptSchema(func(params jsonschema.InterceptSchemaParams) (stop bool, err error) {
 			// Allow unknown request headers and skip response.
-			if oc, ok := openapi3.OperationCtx(params.Context); !params.Processed || !ok ||
-				oc.ProcessingResponse || oc.ProcessingIn == string(rest.ParamInHeader) {
+			if oc, ok := oapi.OperationCtx(params.Context); !params.Processed || !ok ||
+				oc.IsProcessingResponse() || oc.ProcessingIn() == oapi.InHeader {
 				return false, nil
 			}
 
@@ -51,7 +53,7 @@ func NewRouter() http.Handler {
 	uuidDef.AddType(jsonschema.String)
 	uuidDef.WithFormat("uuid")
 	uuidDef.WithExamples("248df4b7-aa70-47b8-a036-33ac447e668d")
-	s.OpenAPICollector.Reflector().AddTypeMapping(uuid.UUID{}, uuidDef)
+	jsr.AddTypeMapping(uuid.UUID{}, uuidDef)
 
 	s.OpenAPICollector.CombineErrors = "anyOf"
 
@@ -88,7 +90,14 @@ func NewRouter() http.Handler {
 	)
 
 	// Annotations can be used to alter documentation of operation identified by method and path.
-	s.OpenAPICollector.Annotate(http.MethodPost, "/validation", func(op *openapi3.Operation) error {
+	s.OpenAPICollector.AnnotateOperation(http.MethodPost, "/validation", func(oc oapi.OperationContext) error {
+		o3, ok := oc.(openapi3.OperationExposer)
+		if !ok {
+			return nil
+		}
+
+		op := o3.Operation()
+
 		if op.Description != nil {
 			*op.Description = *op.Description + " Custom annotation."
 		}
@@ -108,8 +117,8 @@ func NewRouter() http.Handler {
 
 	s.Post("/json-map-body", jsonMapBody(),
 		// Annotate operation to add post-processing if necessary.
-		nethttp.AnnotateOperation(func(op *openapi3.Operation) error {
-			op.WithDescription("Request with JSON object (map) body.")
+		nethttp.AnnotateOpenAPIOperation(func(oc oapi.OperationContext) error {
+			oc.SetDescription("Request with JSON object (map) body.")
 
 			return nil
 		}))
@@ -134,7 +143,7 @@ func NewRouter() http.Handler {
 	s.Post("/no-validation", noValidation())
 
 	// Type mapping is necessary to pass interface as structure into documentation.
-	s.OpenAPICollector.Reflector().AddTypeMapping(new(gzipPassThroughOutput), new(gzipPassThroughStruct))
+	jsr.AddTypeMapping(new(gzipPassThroughOutput), new(gzipPassThroughStruct))
 	s.Get("/gzip-pass-through", directGzip())
 	s.Head("/gzip-pass-through", directGzip())
 
@@ -148,6 +157,8 @@ func NewRouter() http.Handler {
 			if c, err := r.Cookie("sessid"); err == nil {
 				r = r.WithContext(context.WithValue(r.Context(), "sessionID", c.Value))
 			}
+
+			handler.ServeHTTP(w, r)
 		})
 	}
 
diff --git a/_examples/basic/main.go b/_examples/basic/main.go
index 3f209f1..4136039 100644
--- a/_examples/basic/main.go
+++ b/_examples/basic/main.go
@@ -19,9 +19,9 @@ func main() {
 	s := web.DefaultService()
 
 	// Init API documentation schema.
-	s.OpenAPI.Info.Title = "Basic Example"
-	s.OpenAPI.Info.WithDescription("This app showcases a trivial REST API.")
-	s.OpenAPI.Info.Version = "v1.2.3"
+	s.OpenAPISchema().SetTitle("Basic Example")
+	s.OpenAPISchema().SetDescription("This app showcases a trivial REST API.")
+	s.OpenAPISchema().SetVersion("v1.2.3")
 
 	// Setup middlewares.
 	s.Wrap(
diff --git a/_examples/generic/main.go b/_examples/generic/main.go
index eb839f6..ab97a90 100644
--- a/_examples/generic/main.go
+++ b/_examples/generic/main.go
@@ -22,9 +22,9 @@ func main() {
 	s := web.DefaultService()
 
 	// Init API documentation schema.
-	s.OpenAPI.Info.Title = "Basic Example"
-	s.OpenAPI.Info.WithDescription("This app showcases a trivial REST API.")
-	s.OpenAPI.Info.Version = "v1.2.3"
+	s.OpenAPISchema().SetTitle("Basic Example")
+	s.OpenAPISchema().SetDescription("This app showcases a trivial REST API.")
+	s.OpenAPISchema().SetVersion("v1.2.3")
 
 	// Setup middlewares.
 	s.Wrap(
diff --git a/_examples/go.mod b/_examples/go.mod
index 71723ae..71b61b3 100644
--- a/_examples/go.mod
+++ b/_examples/go.mod
@@ -14,11 +14,11 @@ require (
 	github.com/rs/cors v1.9.0
 	github.com/stretchr/testify v1.8.4
 	github.com/swaggest/assertjson v1.9.0
-	github.com/swaggest/jsonschema-go v0.3.52
-	github.com/swaggest/openapi-go v0.2.33
-	github.com/swaggest/swgui v1.6.4
+	github.com/swaggest/jsonschema-go v0.3.55
+	github.com/swaggest/openapi-go v0.2.37
+	github.com/swaggest/swgui v1.7.2
 	github.com/swaggest/usecase v1.2.1
-	github.com/valyala/fasthttp v1.46.0
+	github.com/valyala/fasthttp v1.48.0
 )
 
 require (
@@ -33,7 +33,7 @@ require (
 	github.com/davecgh/go-spew v1.1.1 // indirect
 	github.com/fsnotify/fsnotify v1.5.4 // indirect
 	github.com/iancoleman/orderedmap v0.3.0 // indirect
-	github.com/klauspost/compress v1.16.5 // indirect
+	github.com/klauspost/compress v1.16.7 // 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.3.1 // indirect
@@ -41,7 +41,7 @@ require (
 	github.com/swaggest/refl v1.2.0 // indirect
 	github.com/valyala/bytebufferpool v1.0.0 // indirect
 	github.com/vearutop/dynhist-go v1.1.0 // indirect
-	github.com/vearutop/statigz v1.3.0 // indirect
+	github.com/vearutop/statigz v1.4.0 // indirect
 	github.com/yosuke-furukawa/json5 v0.1.2-0.20201207051438-cf7bb3f354ff // indirect
 	github.com/yudai/gojsondiff v1.0.0 // indirect
 	github.com/yudai/golcs v0.0.0-20170316035057-ecda9a501e82 // indirect
diff --git a/_examples/go.sum b/_examples/go.sum
index c8044d9..3d6f109 100644
--- a/_examples/go.sum
+++ b/_examples/go.sum
@@ -27,8 +27,8 @@ github.com/iancoleman/orderedmap v0.3.0 h1:5cbR2grmZR/DiVt+VJopEhtVs9YGInGIxAoMJ
 github.com/iancoleman/orderedmap v0.3.0/go.mod h1:XuLcCUkdL5owUCQeF2Ue9uuw1EptkJDkXXS7VoV7XGE=
 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.16.5 h1:IFV2oUNUzZaz+XyusxpLzpzS8Pt5rh0Z16For/djlyI=
-github.com/klauspost/compress v1.16.5/go.mod h1:ntbaceVETuRiXiv4DpjP66DpAtAGkEQskQzEyD//IeE=
+github.com/klauspost/compress v1.16.7 h1:2mk3MPGNzKyxErAw8YaohYh69+pa4sIQSC0fPGCFR9I=
+github.com/klauspost/compress v1.16.7/go.mod h1:ntbaceVETuRiXiv4DpjP66DpAtAGkEQskQzEyD//IeE=
 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=
@@ -55,24 +55,24 @@ github.com/swaggest/assertjson v1.9.0 h1:dKu0BfJkIxv/xe//mkCrK5yZbs79jL7OVf9Ija7
 github.com/swaggest/assertjson v1.9.0/go.mod h1:b+ZKX2VRiUjxfUIal0HDN85W0nHPAYUbYH5WkkSsFsU=
 github.com/swaggest/form/v5 v5.1.1 h1:ct6/rOQBGrqWUQ0FUv3vW5sHvTUb31AwTUWj947N6cY=
 github.com/swaggest/form/v5 v5.1.1/go.mod h1:X1hraaoONee20PMnGNLQpO32f9zbQ0Czfm7iZThuEKg=
-github.com/swaggest/jsonschema-go v0.3.52 h1:cFk0Ma34MuZvA+JJ8S5WuQMedwxzUa+9+qrGwtoE39U=
-github.com/swaggest/jsonschema-go v0.3.52/go.mod h1:sRly7iaIIvbheAqsyvnKU3H7ogWbF0wtUBXyGpRXNm4=
-github.com/swaggest/openapi-go v0.2.33 h1:5S9XCSuaex9KkLQVNiH0SYukiTEWCa2z66k7QQ+UUiQ=
-github.com/swaggest/openapi-go v0.2.33/go.mod h1:h7dk9ApzrU9yJ7O8TSoW+a1PyT7R+bnYHDw/NpexlYA=
+github.com/swaggest/jsonschema-go v0.3.55 h1:xbDQaLw9NxxkL3meYnUnX5f6Hhav2wNAEfb/We53CkM=
+github.com/swaggest/jsonschema-go v0.3.55/go.mod h1:5WFFGBBte5JAWAV8gDpNRJ/tlQnb1AHDdf/ghgsVUik=
+github.com/swaggest/openapi-go v0.2.37 h1:Fz894zoeV5SNo4mm/tpADDwqQAHws6UWsjZbjrvBgw0=
+github.com/swaggest/openapi-go v0.2.37/go.mod h1:iDIKsjFeVItoGOtb1jmLf+ULYg2yP0uqbBJ4OzE0nxM=
 github.com/swaggest/refl v1.2.0 h1:Qqqhfwi7REXF6/4cwJmj3gQMzl0Q0cYquxTYdD0kvi0=
 github.com/swaggest/refl v1.2.0/go.mod h1:CkC6g7h1PW33KprTuYRSw8UUOslRUt4lF3oe7tTIgNU=
-github.com/swaggest/swgui v1.6.4 h1:9G7HTUMOAu/Y9YF2wDvoq9GXFlV4BH5y8BSOl4MWfl8=
-github.com/swaggest/swgui v1.6.4/go.mod h1:xsfGb4NtnBspBXKXtlPdVrvoqzCIZ338Aj3tHNikz2Q=
+github.com/swaggest/swgui v1.7.2 h1:N5hMPCQ+bIedVJoQDNjFUn8BqtISQDwaqEa76VkvzLs=
+github.com/swaggest/swgui v1.7.2/go.mod h1:gGFKvKH+nmlPVXBc5S1/sUThCi2f+cthHaY2MfsWlAM=
 github.com/swaggest/usecase v1.2.1 h1:XYVdK9tK2KCPglTflUi7aWBrVwIyb58D5mvGWED7pNs=
 github.com/swaggest/usecase v1.2.1/go.mod h1:5ccwVsLJ9eQpU4m0AGTM444pdqSPQBiocIwMmdRH9lQ=
 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.46.0 h1:6ZRhrFg8zBXTRYY6vdzbFhqsBd7FVv123pV2m9V87U4=
-github.com/valyala/fasthttp v1.46.0/go.mod h1:k2zXd82h/7UZc3VOdJ2WaUqt1uZ/XpXAfE9i+HBC3lA=
+github.com/valyala/fasthttp v1.48.0 h1:oJWvHb9BIZToTQS3MuQ2R3bJZiNSa2KiNdeI8A+79Tc=
+github.com/valyala/fasthttp v1.48.0/go.mod h1:k2zXd82h/7UZc3VOdJ2WaUqt1uZ/XpXAfE9i+HBC3lA=
 github.com/vearutop/dynhist-go v1.1.0 h1:kufNShpt45I97L2eO3bEYTSPKBSued0prheukJUYRok=
 github.com/vearutop/dynhist-go v1.1.0/go.mod h1:mjWlwr64oGBtouxh8s/9YOXV73MYP0Q9tfKIAp8/xto=
-github.com/vearutop/statigz v1.3.0 h1:RoIbvbMilsT8gXtPflWLcVlce89l5qZP9SHlKhXtEsg=
-github.com/vearutop/statigz v1.3.0/go.mod h1:jqlOPvLAdiQktMtYAkyguI3Ee0FA26iXKeEx2pS5l88=
+github.com/vearutop/statigz v1.4.0 h1:RQL0KG3j/uyA/PFpHeZ/L6l2ta920/MxlOAIGEOuwmU=
+github.com/vearutop/statigz v1.4.0/go.mod h1:LYTolBLiz9oJISwiVKnOQoIwhO1LWX1A7OECawGS8XE=
 github.com/yosuke-furukawa/json5 v0.1.2-0.20201207051438-cf7bb3f354ff h1:7YqG491bE4vstXRz1lD38rbSgbXnirvROz1lZiOnPO8=
 github.com/yosuke-furukawa/json5 v0.1.2-0.20201207051438-cf7bb3f354ff/go.mod h1:sw49aWDqNdRJ6DYUtIQiaA3xyj2IL9tjeNYmX2ixwcU=
 github.com/yudai/gojsondiff v1.0.0 h1:27cbfqXLVEJ1o8I6v3y9lg8Ydm53EKqHXAOMxEGlCOA=
diff --git a/_examples/mount/main.go b/_examples/mount/main.go
index ccfc861..c03b56f 100644
--- a/_examples/mount/main.go
+++ b/_examples/mount/main.go
@@ -37,7 +37,7 @@ func sum() usecase.Interactor {
 
 func main() {
 	service := web.DefaultService()
-	service.OpenAPI.Info.Title = "Security and Mount Example"
+	service.OpenAPISchema().SetTitle("Security and Mount Example")
 
 	apiV1 := web.DefaultService()
 
diff --git a/_examples/task-api/internal/infra/nethttp/router.go b/_examples/task-api/internal/infra/nethttp/router.go
index 5a7551e..c6dc11a 100644
--- a/_examples/task-api/internal/infra/nethttp/router.go
+++ b/_examples/task-api/internal/infra/nethttp/router.go
@@ -6,7 +6,7 @@ import (
 
 	"github.com/go-chi/chi/v5"
 	"github.com/go-chi/chi/v5/middleware"
-	"github.com/swaggest/openapi-go/openapi3"
+	"github.com/swaggest/openapi-go"
 	"github.com/swaggest/rest"
 	"github.com/swaggest/rest/_examples/task-api/internal/infra/schema"
 	"github.com/swaggest/rest/_examples/task-api/internal/infra/service"
@@ -36,8 +36,8 @@ func NewRouter(locator *service.Locator) http.Handler {
 
 	// Unrestricted access.
 	s.Route("/dev", func(r chi.Router) {
-		r.Use(nethttp.AnnotateOpenAPI(s.OpenAPICollector, func(op *openapi3.Operation) error {
-			op.Tags = []string{"Dev Mode"}
+		r.Use(nethttp.OpenAPIAnnotationsMiddleware(s.OpenAPICollector, func(oc openapi.OperationContext) error {
+			oc.SetTags("Dev Mode")
 
 			return nil
 		}))
@@ -54,8 +54,8 @@ func NewRouter(locator *service.Locator) http.Handler {
 	// Endpoints with admin access.
 	s.Route("/admin", func(r chi.Router) {
 		r.Group(func(r chi.Router) {
-			r.Use(nethttp.AnnotateOpenAPI(s.OpenAPICollector, func(op *openapi3.Operation) error {
-				op.Tags = []string{"Admin Mode"}
+			r.Use(nethttp.OpenAPIAnnotationsMiddleware(s.OpenAPICollector, func(oc openapi.OperationContext) error {
+				oc.SetTags("Admin Mode")
 
 				return nil
 			}))
diff --git a/_examples/task-api/internal/infra/repository/task.go b/_examples/task-api/internal/infra/repository/task.go
index a58bcb3..250d4a7 100644
--- a/_examples/task-api/internal/infra/repository/task.go
+++ b/_examples/task-api/internal/infra/repository/task.go
@@ -154,8 +154,6 @@ func (tr *Task) Create(ctx context.Context, value task.Value) (task.Entity, erro
 }
 
 // FinishExpired closes expired tasks.
-//
-//	nolint:unused // False positive.
 func (tr *Task) FinishExpired(_ context.Context) error {
 	tr.mu.Lock()
 	defer tr.mu.Unlock()
diff --git a/_examples/task-api/main.go b/_examples/task-api/main.go
index b1fcf89..4daf306 100644
--- a/_examples/task-api/main.go
+++ b/_examples/task-api/main.go
@@ -4,6 +4,7 @@ import (
 	"fmt"
 	"log"
 	"net/http"
+	"time"
 
 	"github.com/kelseyhightower/envconfig"
 	"github.com/swaggest/rest/_examples/task-api/internal/infra"
@@ -26,7 +27,10 @@ func main() {
 	l.EnableGracefulShutdown()
 
 	// Initialize HTTP server.
-	srv := http.Server{Addr: fmt.Sprintf(":%d", cfg.HTTPPort), Handler: nethttp.NewRouter(l)}
+	srv := http.Server{
+		Addr: fmt.Sprintf(":%d", cfg.HTTPPort), Handler: nethttp.NewRouter(l),
+		ReadHeaderTimeout: time.Second,
+	}
 
 	// Start HTTP server.
 	log.Printf("starting HTTP server at http://localhost:%d/docs\n", cfg.HTTPPort)
diff --git a/go.mod b/go.mod
index 471e9a6..8b36238 100644
--- a/go.mod
+++ b/go.mod
@@ -8,12 +8,13 @@ require (
 	github.com/bool64/shared v0.1.5
 	github.com/cespare/xxhash/v2 v2.2.0
 	github.com/go-chi/chi/v5 v5.0.10
+	github.com/gorilla/mux v1.8.0
 	github.com/santhosh-tekuri/jsonschema/v3 v3.1.0
 	github.com/stretchr/testify v1.8.2
 	github.com/swaggest/assertjson v1.9.0
 	github.com/swaggest/form/v5 v5.1.1
-	github.com/swaggest/jsonschema-go v0.3.52
-	github.com/swaggest/openapi-go v0.2.33
+	github.com/swaggest/jsonschema-go v0.3.55
+	github.com/swaggest/openapi-go v0.2.37
 	github.com/swaggest/refl v1.2.0
 	github.com/swaggest/usecase v1.2.1
 )
diff --git a/go.sum b/go.sum
index 5b4e3de..b526793 100644
--- a/go.sum
+++ b/go.sum
@@ -1,9 +1,7 @@
-github.com/bool64/dev v0.2.16/go.mod h1:/csLrm+4oDSsKJRIVS0mrywAonLnYKFG8RvGT7Jh9b8=
 github.com/bool64/dev v0.2.17/go.mod h1:iJbh1y/HkunEPhgebWRNcs8wfGq7sjvJ6W5iabL8ACg=
 github.com/bool64/dev v0.2.24/go.mod h1:iJbh1y/HkunEPhgebWRNcs8wfGq7sjvJ6W5iabL8ACg=
 github.com/bool64/dev v0.2.25/go.mod h1:iJbh1y/HkunEPhgebWRNcs8wfGq7sjvJ6W5iabL8ACg=
 github.com/bool64/dev v0.2.27/go.mod h1:iJbh1y/HkunEPhgebWRNcs8wfGq7sjvJ6W5iabL8ACg=
-github.com/bool64/dev v0.2.28/go.mod h1:iJbh1y/HkunEPhgebWRNcs8wfGq7sjvJ6W5iabL8ACg=
 github.com/bool64/dev v0.2.29 h1:x+syGyh+0eWtOzQ1ItvLzOGIWyNWnyjXpHIcpF2HvL4=
 github.com/bool64/dev v0.2.29/go.mod h1:iJbh1y/HkunEPhgebWRNcs8wfGq7sjvJ6W5iabL8ACg=
 github.com/bool64/httpmock v0.1.13 h1:3QpRXQ5kwHLW8xnVT8+Ug7VS6RerhdEFV+RWYC61aVo=
@@ -31,6 +29,8 @@ github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw
 github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
 github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
 github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
+github.com/gorilla/mux v1.8.0 h1:i40aqfkR1h2SlN9hojwV5ZA91wcXFOvkdNIeFDP5koI=
+github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So=
 github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
 github.com/iancoleman/orderedmap v0.2.0/go.mod h1:N0Wam8K1arqPXNWjMo21EXnBPOPp36vB07FNRdD2geA=
 github.com/iancoleman/orderedmap v0.3.0 h1:5cbR2grmZR/DiVt+VJopEhtVs9YGInGIxAoMJn+Ichc=
@@ -76,11 +76,10 @@ github.com/swaggest/assertjson v1.9.0 h1:dKu0BfJkIxv/xe//mkCrK5yZbs79jL7OVf9Ija7
 github.com/swaggest/assertjson v1.9.0/go.mod h1:b+ZKX2VRiUjxfUIal0HDN85W0nHPAYUbYH5WkkSsFsU=
 github.com/swaggest/form/v5 v5.1.1 h1:ct6/rOQBGrqWUQ0FUv3vW5sHvTUb31AwTUWj947N6cY=
 github.com/swaggest/form/v5 v5.1.1/go.mod h1:X1hraaoONee20PMnGNLQpO32f9zbQ0Czfm7iZThuEKg=
-github.com/swaggest/jsonschema-go v0.3.52 h1:cFk0Ma34MuZvA+JJ8S5WuQMedwxzUa+9+qrGwtoE39U=
-github.com/swaggest/jsonschema-go v0.3.52/go.mod h1:sRly7iaIIvbheAqsyvnKU3H7ogWbF0wtUBXyGpRXNm4=
-github.com/swaggest/openapi-go v0.2.33 h1:5S9XCSuaex9KkLQVNiH0SYukiTEWCa2z66k7QQ+UUiQ=
-github.com/swaggest/openapi-go v0.2.33/go.mod h1:h7dk9ApzrU9yJ7O8TSoW+a1PyT7R+bnYHDw/NpexlYA=
-github.com/swaggest/refl v1.1.0/go.mod h1:g3Qa6ki0A/L2yxiuUpT+cuBURuRaltF5SDQpg1kMZSY=
+github.com/swaggest/jsonschema-go v0.3.55 h1:xbDQaLw9NxxkL3meYnUnX5f6Hhav2wNAEfb/We53CkM=
+github.com/swaggest/jsonschema-go v0.3.55/go.mod h1:5WFFGBBte5JAWAV8gDpNRJ/tlQnb1AHDdf/ghgsVUik=
+github.com/swaggest/openapi-go v0.2.37 h1:Fz894zoeV5SNo4mm/tpADDwqQAHws6UWsjZbjrvBgw0=
+github.com/swaggest/openapi-go v0.2.37/go.mod h1:iDIKsjFeVItoGOtb1jmLf+ULYg2yP0uqbBJ4OzE0nxM=
 github.com/swaggest/refl v1.2.0 h1:Qqqhfwi7REXF6/4cwJmj3gQMzl0Q0cYquxTYdD0kvi0=
 github.com/swaggest/refl v1.2.0/go.mod h1:CkC6g7h1PW33KprTuYRSw8UUOslRUt4lF3oe7tTIgNU=
 github.com/swaggest/usecase v1.2.1 h1:XYVdK9tK2KCPglTflUi7aWBrVwIyb58D5mvGWED7pNs=
diff --git a/gorillamux/collector.go b/gorillamux/collector.go
new file mode 100644
index 0000000..21093f6
--- /dev/null
+++ b/gorillamux/collector.go
@@ -0,0 +1,137 @@
+package gorillamux
+
+import (
+	"net/http"
+
+	"github.com/gorilla/mux"
+	oapi "github.com/swaggest/openapi-go"
+	"github.com/swaggest/openapi-go/openapi3"
+	"github.com/swaggest/rest/nethttp"
+	"github.com/swaggest/rest/openapi"
+)
+
+// OpenAPICollector is a wrapper for openapi.Collector tailored to walk gorilla/mux router.
+type OpenAPICollector struct {
+	// Collector is an actual OpenAPI collector.
+	Collector *openapi.Collector
+
+	// DefaultMethods list is used when handler serves all methods.
+	DefaultMethods []string
+
+	// OperationExtractor allows flexible extraction of OpenAPI information.
+	OperationExtractor func(h http.Handler) func(oc oapi.OperationContext) error
+
+	// Host filters routes by host, gorilla/mux can serve different handlers at
+	// same method, paths with different hosts. This can not be expressed with a single
+	// OpenAPI document.
+	Host string
+}
+
+// NewOpenAPICollector creates route walker for gorilla/mux, that collects OpenAPI operations.
+func NewOpenAPICollector(r oapi.Reflector) *OpenAPICollector {
+	c := openapi.NewCollector(r)
+
+	return &OpenAPICollector{
+		Collector: c,
+		DefaultMethods: []string{
+			http.MethodHead, http.MethodGet, http.MethodPost,
+			http.MethodPut, http.MethodPatch, http.MethodDelete,
+		},
+	}
+}
+
+// OpenAPIPreparer defines http.Handler with OpenAPI information.
+type OpenAPIPreparer interface {
+	SetupOpenAPIOperation(oc oapi.OperationContext) error
+}
+
+type preparerFunc func(oc oapi.OperationContext) error
+
+// Walker walks route tree and collects OpenAPI information.
+func (dc *OpenAPICollector) Walker(route *mux.Route, _ *mux.Router, _ []*mux.Route) error {
+	handler := route.GetHandler()
+
+	if handler == nil {
+		return nil
+	}
+
+	// Path is critical info, skipping route if there is a problem with path.
+	path, err := route.GetPathTemplate()
+	if err != nil && path == "" {
+		return nil
+	}
+
+	host, err := route.GetHostTemplate()
+	if (err == nil && host != dc.Host) || // There is host, but different.
+		(err != nil && dc.Host != "") { // There is no host, but should be.
+		return nil
+	}
+
+	methods, err := route.GetMethods()
+	if err != nil {
+		methods = dc.DefaultMethods
+	}
+
+	var (
+		openAPIPreparer OpenAPIPreparer
+		preparer        preparerFunc
+	)
+
+	if nethttp.HandlerAs(handler, &openAPIPreparer) {
+		preparer = openAPIPreparer.SetupOpenAPIOperation
+	} else if dc.OperationExtractor != nil {
+		preparer = dc.OperationExtractor(handler)
+	}
+
+	for _, method := range methods {
+		if err := dc.Collector.CollectOperation(method, path, dc.collect(method, path, preparer)); err != nil {
+			return err
+		}
+	}
+
+	return nil
+}
+
+func (dc *OpenAPICollector) collect(method, path string, preparer preparerFunc) preparerFunc {
+	return func(oc oapi.OperationContext) error {
+		// Do not apply default parameters to not conflict with custom preparer.
+		if preparer != nil {
+			return preparer(oc)
+		}
+
+		// Do not apply default parameters to not conflict with custom annotation.
+		if dc.Collector.HasAnnotation(method, path) {
+			return nil
+		}
+
+		_, _, pathItems, err := oapi.SanitizeMethodPath(method, path)
+		if err != nil {
+			return err
+		}
+
+		if len(pathItems) > 0 {
+			if o3, ok := oc.(openapi3.OperationExposer); ok {
+				op := o3.Operation()
+
+				for _, p := range pathItems {
+					param := openapi3.ParameterOrRef{}
+					param.WithParameter(openapi3.Parameter{
+						Name: p,
+						In:   openapi3.ParameterInPath,
+					})
+
+					op.Parameters = append(op.Parameters, param)
+				}
+			}
+		}
+
+		oc.SetDescription("Information about this operation was obtained using only HTTP method and path pattern. " +
+			"It may be incomplete and/or inaccurate.")
+		oc.SetTags("Incomplete")
+		oc.AddRespStructure(nil, func(cu *oapi.ContentUnit) {
+			cu.ContentType = "text/html"
+		})
+
+		return nil
+	}
+}
diff --git a/gorillamux/doc.go b/gorillamux/doc.go
new file mode 100644
index 0000000..6f0bedc
--- /dev/null
+++ b/gorillamux/doc.go
@@ -0,0 +1,2 @@
+// Package gorillamux provides OpenAPI docs collector for gorilla/mux web services.
+package gorillamux
diff --git a/gorillamux/path_decoder.go b/gorillamux/path_decoder.go
new file mode 100644
index 0000000..f5396b0
--- /dev/null
+++ b/gorillamux/path_decoder.go
@@ -0,0 +1,20 @@
+package gorillamux
+
+import (
+	"net/http"
+	"net/url"
+
+	"github.com/gorilla/mux"
+)
+
+// PathToURLValues is a decoder function for parameters in path.
+func PathToURLValues(r *http.Request) (url.Values, error) { //nolint:unparam // Matches request.DecoderFactory.SetDecoderFunc.
+	muxVars := mux.Vars(r)
+	res := make(url.Values, len(muxVars))
+
+	for k, v := range muxVars {
+		res.Set(k, v)
+	}
+
+	return res, nil
+}
diff --git a/jsonschema/validator.go b/jsonschema/validator.go
index dc9f945..74e5d1a 100644
--- a/jsonschema/validator.go
+++ b/jsonschema/validator.go
@@ -167,7 +167,7 @@ func (v *Validator) ValidateJSONBody(jsonBody []byte) error {
 	name := "body"
 
 	schema, found := v.inNamedSchemas[rest.ParamInBody][name]
-	if !found {
+	if !found || schema == nil {
 		return nil
 	}
 
diff --git a/nethttp/handler.go b/nethttp/handler.go
index a28f129..a9456ca 100644
--- a/nethttp/handler.go
+++ b/nethttp/handler.go
@@ -23,6 +23,11 @@ func NewHandler(useCase usecase.Interactor, options ...func(h *Handler)) *Handle
 		options: options,
 	}
 	h.HandleErrResponse = h.handleErrResponseDefault
+
+	for _, option := range h.options {
+		option(h)
+	}
+
 	h.SetUseCase(useCase)
 
 	return h
@@ -37,10 +42,6 @@ func (h *Handler) UseCase() usecase.Interactor {
 func (h *Handler) SetUseCase(useCase usecase.Interactor) {
 	h.useCase = useCase
 
-	for _, option := range h.options {
-		option(h)
-	}
-
 	h.setupInputBuffer()
 	h.setupOutputBuffer()
 }
diff --git a/nethttp/openapi.go b/nethttp/openapi.go
index db9651a..b276ae5 100644
--- a/nethttp/openapi.go
+++ b/nethttp/openapi.go
@@ -3,6 +3,7 @@ package nethttp
 import (
 	"net/http"
 
+	oapi "github.com/swaggest/openapi-go"
 	"github.com/swaggest/openapi-go/openapi3"
 	"github.com/swaggest/rest"
 	"github.com/swaggest/rest/openapi"
@@ -24,12 +25,11 @@ func OpenAPIMiddleware(s *openapi.Collector) func(http.Handler) http.Handler {
 			return h
 		}
 
-		err := s.Collect(
+		err := s.CollectUseCase(
 			withRoute.RouteMethod(),
 			withRoute.RoutePattern(),
 			handler.UseCase(),
 			handler.HandlerTrait,
-			handler.OperationAnnotations...,
 		)
 		if err != nil {
 			panic(err)
@@ -105,6 +105,8 @@ func HTTPBearerSecurityMiddleware(
 }
 
 // AnnotateOpenAPI applies OpenAPI annotation to relevant handlers.
+//
+// Deprecated: use OpenAPIAnnotationsMiddleware.
 func AnnotateOpenAPI(
 	s *openapi.Collector,
 	setup ...func(op *openapi3.Operation) error,
@@ -145,9 +147,33 @@ type MiddlewareConfig struct {
 	ResponseStatus int
 }
 
+// OpenAPIAnnotationsMiddleware applies OpenAPI annotations to handlers.
+func OpenAPIAnnotationsMiddleware(
+	s *openapi.Collector,
+	annotations ...func(oc oapi.OperationContext) error,
+) func(http.Handler) http.Handler {
+	return func(next http.Handler) http.Handler {
+		if IsWrapperChecker(next) {
+			return next
+		}
+
+		var withRoute rest.HandlerWithRoute
+
+		if HandlerAs(next, &withRoute) {
+			s.AnnotateOperation(
+				withRoute.RouteMethod(),
+				withRoute.RoutePattern(),
+				annotations...,
+			)
+		}
+
+		return next
+	}
+}
+
 func securityMiddleware(s *openapi.Collector, name string, cfg MiddlewareConfig) func(http.Handler) http.Handler {
-	return AnnotateOpenAPI(s, func(op *openapi3.Operation) error {
-		op.Security = append(op.Security, map[string][]string{name: {}})
+	return OpenAPIAnnotationsMiddleware(s, func(oc oapi.OperationContext) error {
+		oc.AddSecurity(name)
 
 		if cfg.ResponseStatus == 0 {
 			cfg.ResponseStatus = http.StatusUnauthorized
@@ -157,6 +183,10 @@ func securityMiddleware(s *openapi.Collector, name string, cfg MiddlewareConfig)
 			cfg.ResponseStructure = rest.ErrResponse{}
 		}
 
-		return s.Reflector().SetJSONResponse(op, cfg.ResponseStructure, cfg.ResponseStatus)
+		oc.AddRespStructure(cfg.ResponseStructure, func(cu *oapi.ContentUnit) {
+			cu.HTTPStatus = cfg.ResponseStatus
+		})
+
+		return nil
 	})
 }
diff --git a/nethttp/options.go b/nethttp/options.go
index f5892d4..6b0ef77 100644
--- a/nethttp/options.go
+++ b/nethttp/options.go
@@ -4,6 +4,7 @@ import (
 	"net/http"
 	"reflect"
 
+	"github.com/swaggest/openapi-go"
 	"github.com/swaggest/openapi-go/openapi3"
 	"github.com/swaggest/refl"
 	"github.com/swaggest/rest"
@@ -26,21 +27,39 @@ func OptionsMiddleware(options ...func(h *Handler)) func(h http.Handler) http.Ha
 	}
 }
 
+// AnnotateOpenAPIOperation allows customization of OpenAPI operation, that is reflected from the Handler.
+func AnnotateOpenAPIOperation(annotations ...func(oc openapi.OperationContext) error) func(h *Handler) {
+	return func(h *Handler) {
+		h.OpenAPIAnnotations = append(h.OpenAPIAnnotations, annotations...)
+	}
+}
+
 // AnnotateOperation allows customizations of prepared operations.
+//
+// Deprecated: use AnnotateOpenAPIOperation.
 func AnnotateOperation(annotations ...func(operation *openapi3.Operation) error) func(h *Handler) {
 	return func(h *Handler) {
-		h.OperationAnnotations = append(h.OperationAnnotations, annotations...)
+		for _, a := range annotations {
+			a := a
+
+			h.OpenAPIAnnotations = append(h.OpenAPIAnnotations, func(oc openapi.OperationContext) error {
+				if o3, ok := oc.(openapi3.OperationExposer); ok {
+					return a(o3.Operation())
+				}
+
+				return nil
+			})
+		}
 	}
 }
 
 // RequestBodyContent enables string request body with content type (e.g. text/plain).
 func RequestBodyContent(contentType string) func(h *Handler) {
 	return func(h *Handler) {
-		mt := openapi3.MediaType{}
-		mt.SchemaEns().SchemaEns().WithType(openapi3.SchemaTypeString)
-
-		h.OperationAnnotations = append(h.OperationAnnotations, func(op *openapi3.Operation) error {
-			op.RequestBodyEns().RequestBodyEns().WithContentItem(contentType, mt)
+		h.OpenAPIAnnotations = append(h.OpenAPIAnnotations, func(oc openapi.OperationContext) error {
+			oc.AddReqStructure(nil, func(cu *openapi.ContentUnit) {
+				cu.ContentType = contentType
+			})
 
 			return nil
 		})
diff --git a/openapi/collector.go b/openapi/collector.go
index c4b5dc6..fcc3117 100644
--- a/openapi/collector.go
+++ b/openapi/collector.go
@@ -4,13 +4,13 @@ package openapi
 import (
 	"context"
 	"encoding/json"
-	"errors"
 	"fmt"
 	"net/http"
 	"strconv"
 	"sync"
 
 	"github.com/swaggest/jsonschema-go"
+	"github.com/swaggest/openapi-go"
 	"github.com/swaggest/openapi-go/openapi3"
 	"github.com/swaggest/rest"
 	"github.com/swaggest/usecase"
@@ -35,21 +35,57 @@ type Collector struct {
 	// If empty, "application/json" is used.
 	DefaultErrorResponseContentType string
 
-	gen          *openapi3.Reflector
-	annotations  map[string][]func(*openapi3.Operation) error
-	operationIDs map[string]bool
+	gen *openapi3.Reflector
+	ref openapi.Reflector
+
+	ocAnnotations map[string][]func(oc openapi.OperationContext) error
+	annotations   map[string][]func(*openapi3.Operation) error
+	operationIDs  map[string]bool
+}
+
+// NewCollector creates an instance of OpenAPI Collector.
+func NewCollector(r openapi.Reflector) *Collector {
+	c := &Collector{
+		ref: r,
+	}
+
+	if r3, ok := r.(*openapi3.Reflector); ok {
+		c.gen = r3
+	}
+
+	return c
+}
+
+// SpecSchema returns OpenAPI specification schema.
+func (c *Collector) SpecSchema() openapi.SpecSchema {
+	return c.Refl().SpecSchema()
+}
+
+// Refl returns OpenAPI reflector.
+func (c *Collector) Refl() openapi.Reflector {
+	if c.ref != nil {
+		return c.ref
+	}
+
+	return c.Reflector()
 }
 
 // Reflector is an accessor to OpenAPI Reflector instance.
 func (c *Collector) Reflector() *openapi3.Reflector {
+	if c.ref != nil && c.gen == nil {
+		panic(fmt.Sprintf("conflicting OpenAPI reflector supplied: %T", c.ref))
+	}
+
 	if c.gen == nil {
-		c.gen = &openapi3.Reflector{}
+		c.gen = openapi3.NewReflector()
 	}
 
 	return c.gen
 }
 
 // Annotate adds OpenAPI operation configuration that is applied during collection.
+//
+// Deprecated: use AnnotateOperation.
 func (c *Collector) Annotate(method, pattern string, setup ...func(op *openapi3.Operation) error) {
 	c.mu.Lock()
 	defer c.mu.Unlock()
@@ -61,12 +97,32 @@ func (c *Collector) Annotate(method, pattern string, setup ...func(op *openapi3.
 	c.annotations[method+pattern] = append(c.annotations[method+pattern], setup...)
 }
 
-// Collect adds use case handler to documentation.
-func (c *Collector) Collect(
+// AnnotateOperation adds OpenAPI operation configuration that is applied during collection,
+// method can be empty to indicate any method.
+func (c *Collector) AnnotateOperation(method, pattern string, setup ...func(oc openapi.OperationContext) error) {
+	c.mu.Lock()
+	defer c.mu.Unlock()
+
+	if c.ocAnnotations == nil {
+		c.ocAnnotations = make(map[string][]func(oc openapi.OperationContext) error)
+	}
+
+	c.ocAnnotations[method+pattern] = append(c.ocAnnotations[method+pattern], setup...)
+}
+
+// HasAnnotation indicates if there is at least one annotation registered for this operation.
+func (c *Collector) HasAnnotation(method, pattern string) bool {
+	if len(c.ocAnnotations[method+pattern]) > 0 {
+		return true
+	}
+
+	return len(c.ocAnnotations[pattern]) > 0
+}
+
+// CollectOperation prepares and adds OpenAPI operation.
+func (c *Collector) CollectOperation(
 	method, pattern string,
-	u usecase.Interactor,
-	h rest.HandlerTrait,
-	annotations ...func(*openapi3.Operation) error,
+	annotations ...func(oc openapi.OperationContext) error,
 ) (err error) {
 	c.mu.Lock()
 	defer c.mu.Unlock()
@@ -77,31 +133,129 @@ func (c *Collector) Collect(
 		}
 	}()
 
-	reflector := c.Reflector()
+	reflector := c.Refl()
 
-	err = reflector.SpecEns().SetupOperation(method, pattern, func(op *openapi3.Operation) error {
-		oc := openapi3.OperationContext{
-			Operation:         op,
-			HTTPMethod:        method,
-			HTTPStatus:        h.SuccessStatus,
-			RespContentType:   h.SuccessContentType,
-			RespHeaderMapping: h.RespHeaderMapping,
+	oc, err := reflector.NewOperationContext(method, pattern)
+	if err != nil {
+		return err
+	}
+
+	for _, setup := range c.ocAnnotations[pattern] {
+		if err = setup(oc); err != nil {
+			return err
+		}
+	}
+
+	for _, setup := range c.ocAnnotations[method+pattern] {
+		if err = setup(oc); err != nil {
+			return err
+		}
+	}
+
+	for _, setup := range annotations {
+		if err = setup(oc); err != nil {
+			return err
 		}
+	}
+
+	return reflector.AddOperation(oc)
+}
 
-		err = c.setupInput(&oc, u, h)
+// CollectUseCase adds use case handler to documentation.
+func (c *Collector) CollectUseCase(
+	method, pattern string,
+	u usecase.Interactor,
+	h rest.HandlerTrait,
+	annotations ...func(oc openapi.OperationContext) error,
+) (err error) {
+	c.mu.Lock()
+	defer c.mu.Unlock()
+
+	defer func() {
 		if err != nil {
-			return fmt.Errorf("failed to setup request: %w", err)
+			err = fmt.Errorf("reflect API schema for %s %s: %w", method, pattern, err)
+		}
+	}()
+
+	reflector := c.Refl()
+
+	oc, err := reflector.NewOperationContext(method, pattern)
+	if err != nil {
+		return err
+	}
+
+	c.setupInput(oc, u, h)
+	c.setupOutput(oc, u, h)
+	c.processUseCase(oc, u, h)
+
+	an := append([]func(oc openapi.OperationContext) error(nil), c.ocAnnotations[method+pattern]...)
+	an = append(an, h.OpenAPIAnnotations...)
+	an = append(an, annotations...)
+
+	for _, setup := range an {
+		if err = setup(oc); err != nil {
+			return err
+		}
+	}
+
+	if o3, ok := oc.(openapi3.OperationExposer); ok {
+		op := o3.Operation()
+
+		for _, setup := range c.annotations[method+pattern] {
+			if err = setup(op); err != nil {
+				return err
+			}
+		}
+
+		//nolint:staticcheck // To be removed with deprecations cleanup.
+		for _, setup := range h.OperationAnnotations {
+			if err = setup(op); err != nil {
+				return err
+			}
 		}
+	}
 
-		err = c.setupOutput(&oc, u)
+	return reflector.AddOperation(oc)
+}
+
+// Collect adds use case handler to documentation.
+//
+// Deprecated: use CollectUseCase.
+func (c *Collector) Collect(
+	method, pattern string,
+	u usecase.Interactor,
+	h rest.HandlerTrait,
+	annotations ...func(*openapi3.Operation) error,
+) (err error) {
+	c.mu.Lock()
+	defer c.mu.Unlock()
+
+	defer func() {
 		if err != nil {
-			return fmt.Errorf("failed to setup response: %w", err)
+			err = fmt.Errorf("reflect API schema for %s %s: %w", method, pattern, err)
 		}
+	}()
+
+	reflector := c.Refl()
 
-		err = c.processUseCase(op, u, h)
+	oc, err := reflector.NewOperationContext(method, pattern)
+	if err != nil {
+		return err
+	}
+
+	c.setupInput(oc, u, h)
+	c.setupOutput(oc, u, h)
+	c.processUseCase(oc, u, h)
+
+	for _, setup := range c.ocAnnotations[method+pattern] {
+		err = setup(oc)
 		if err != nil {
 			return err
 		}
+	}
+
+	if o3, ok := oc.(openapi3.OperationExposer); ok {
+		op := o3.Operation()
 
 		for _, setup := range c.annotations[method+pattern] {
 			err = setup(op)
@@ -116,24 +270,24 @@ func (c *Collector) Collect(
 				return err
 			}
 		}
+	}
 
-		return nil
-	})
-
-	return err
+	return reflector.AddOperation(oc)
 }
 
-func (c *Collector) setupOutput(oc *openapi3.OperationContext, u usecase.Interactor) error {
+func (c *Collector) setupOutput(oc openapi.OperationContext, u usecase.Interactor, h rest.HandlerTrait) {
 	var (
-		hasOutput usecase.HasOutputPort
-		status    = http.StatusOK
-		noContent bool
+		hasOutput   usecase.HasOutputPort
+		status      = http.StatusOK
+		noContent   bool
+		output      interface{}
+		contentType = h.SuccessContentType
 	)
 
 	if usecase.As(u, &hasOutput) {
-		oc.Output = hasOutput.OutputPort()
+		output = hasOutput.OutputPort()
 
-		if rest.OutputHasNoContent(oc.Output) {
+		if rest.OutputHasNoContent(output) {
 			status = http.StatusNoContent
 			noContent = true
 		}
@@ -142,73 +296,58 @@ func (c *Collector) setupOutput(oc *openapi3.OperationContext, u usecase.Interac
 		noContent = true
 	}
 
-	if !noContent && oc.RespContentType == "" {
-		oc.RespContentType = c.DefaultSuccessResponseContentType
+	if !noContent && contentType == "" {
+		contentType = c.DefaultSuccessResponseContentType
 	}
 
-	if outputWithStatus, ok := oc.Output.(rest.OutputWithHTTPStatus); ok {
-		for _, status := range outputWithStatus.ExpectedHTTPStatuses() {
-			oc.HTTPStatus = status
-			if err := c.Reflector().SetupResponse(*oc); err != nil {
-				return err
-			}
-		}
-	} else {
-		if oc.HTTPStatus == 0 {
-			oc.HTTPStatus = status
-		}
-		err := c.Reflector().SetupResponse(*oc)
-		if err != nil {
-			return err
-		}
+	if oc.Method() == http.MethodHead {
+		output = nil
 	}
 
-	if oc.HTTPMethod == http.MethodHead {
-		for code, resp := range oc.Operation.Responses.MapOfResponseOrRefValues {
-			for contentType, cont := range resp.Response.Content {
-				cont.Schema = nil
-				resp.Response.Content[contentType] = cont
-			}
+	setupCU := func(cu *openapi.ContentUnit) {
+		cu.ContentType = contentType
+		cu.SetFieldMapping(openapi.InHeader, h.RespHeaderMapping)
+	}
 
-			oc.Operation.Responses.MapOfResponseOrRefValues[code] = resp
+	if outputWithStatus, ok := output.(rest.OutputWithHTTPStatus); ok {
+		for _, status := range outputWithStatus.ExpectedHTTPStatuses() {
+			oc.AddRespStructure(output, func(cu *openapi.ContentUnit) {
+				cu.HTTPStatus = status
+				setupCU(cu)
+			})
 		}
+	} else {
+		if h.SuccessStatus != 0 {
+			status = h.SuccessStatus
+		}
+		oc.AddRespStructure(output, func(cu *openapi.ContentUnit) {
+			cu.HTTPStatus = status
+			setupCU(cu)
+		})
 	}
-
-	return nil
 }
 
-func (c *Collector) setupInput(oc *openapi3.OperationContext, u usecase.Interactor, h rest.HandlerTrait) error {
-	var (
-		hasInput usecase.HasInputPort
-
-		err error
-	)
+func (c *Collector) setupInput(oc openapi.OperationContext, u usecase.Interactor, h rest.HandlerTrait) {
+	var hasInput usecase.HasInputPort
 
 	if usecase.As(u, &hasInput) {
-		oc.Input = hasInput.InputPort()
-
-		setRequestMapping(oc, h.ReqMapping)
-
-		err = c.Reflector().SetupRequest(*oc)
-		if err != nil {
-			return err
-		}
+		oc.AddReqStructure(hasInput.InputPort(), func(cu *openapi.ContentUnit) {
+			setFieldMapping(cu, h.ReqMapping)
+		})
 	}
-
-	return nil
 }
 
-func setRequestMapping(oc *openapi3.OperationContext, mapping rest.RequestMapping) {
+func setFieldMapping(cu *openapi.ContentUnit, mapping rest.RequestMapping) {
 	if mapping != nil {
-		oc.ReqQueryMapping = mapping[rest.ParamInQuery]
-		oc.ReqPathMapping = mapping[rest.ParamInPath]
-		oc.ReqHeaderMapping = mapping[rest.ParamInHeader]
-		oc.ReqCookieMapping = mapping[rest.ParamInCookie]
-		oc.ReqFormDataMapping = mapping[rest.ParamInFormData]
+		cu.SetFieldMapping(openapi.InQuery, mapping[rest.ParamInQuery])
+		cu.SetFieldMapping(openapi.InPath, mapping[rest.ParamInPath])
+		cu.SetFieldMapping(openapi.InHeader, mapping[rest.ParamInHeader])
+		cu.SetFieldMapping(openapi.InCookie, mapping[rest.ParamInCookie])
+		cu.SetFieldMapping(openapi.InFormData, mapping[rest.ParamInFormData])
 	}
 }
 
-func (c *Collector) processUseCase(op *openapi3.Operation, u usecase.Interactor, h rest.HandlerTrait) error {
+func (c *Collector) processUseCase(oc openapi.OperationContext, u usecase.Interactor, h rest.HandlerTrait) {
 	var (
 		hasName        usecase.HasName
 		hasTitle       usecase.HasTitle
@@ -235,7 +374,7 @@ func (c *Collector) processUseCase(op *openapi3.Operation, u usecase.Interactor,
 
 			c.operationIDs[idSuf] = true
 
-			op.WithID(idSuf)
+			oc.SetID(idSuf)
 		}
 	}
 
@@ -243,7 +382,7 @@ func (c *Collector) processUseCase(op *openapi3.Operation, u usecase.Interactor,
 		title := hasTitle.Title()
 
 		if title != "" {
-			op.WithSummary(hasTitle.Title())
+			oc.SetSummary(hasTitle.Title())
 		}
 	}
 
@@ -251,7 +390,7 @@ func (c *Collector) processUseCase(op *openapi3.Operation, u usecase.Interactor,
 		tags := hasTags.Tags()
 
 		if len(tags) > 0 {
-			op.WithTags(hasTags.Tags()...)
+			oc.SetTags(hasTags.Tags()...)
 		}
 	}
 
@@ -259,31 +398,27 @@ func (c *Collector) processUseCase(op *openapi3.Operation, u usecase.Interactor,
 		desc := hasDescription.Description()
 
 		if desc != "" {
-			op.WithDescription(hasDescription.Description())
+			oc.SetDescription(hasDescription.Description())
 		}
 	}
 
 	if usecase.As(u, &hasDeprecated) && hasDeprecated.IsDeprecated() {
-		op.WithDeprecated(true)
+		oc.SetIsDeprecated(true)
 	}
 
-	return c.processExpectedErrors(op, u, h)
+	c.processOCExpectedErrors(oc, u, h)
 }
 
-func (c *Collector) setJSONResponse(op *openapi3.Operation, output interface{}, statusCode int) error {
-	oc := openapi3.OperationContext{}
-	oc.Operation = op
-	oc.Output = output
-	oc.HTTPStatus = statusCode
-
-	if output != nil {
-		oc.RespContentType = c.DefaultErrorResponseContentType
-	}
-
-	return c.Reflector().SetupResponse(oc)
+func (c *Collector) setOCJSONResponse(oc openapi.OperationContext, output interface{}, statusCode int) {
+	oc.AddRespStructure(output, func(cu *openapi.ContentUnit) {
+		cu.HTTPStatus = statusCode
+		if output != nil {
+			cu.ContentType = c.DefaultErrorResponseContentType
+		}
+	})
 }
 
-func (c *Collector) processExpectedErrors(op *openapi3.Operation, u usecase.Interactor, h rest.HandlerTrait) error {
+func (c *Collector) processOCExpectedErrors(oc openapi.OperationContext, u usecase.Interactor, h rest.HandlerTrait) {
 	var (
 		errsByCode        = map[int][]interface{}{}
 		statusCodes       []int
@@ -291,7 +426,7 @@ func (c *Collector) processExpectedErrors(op *openapi3.Operation, u usecase.Inte
 	)
 
 	if !usecase.As(u, &hasExpectedErrors) {
-		return nil
+		return
 	}
 
 	for _, e := range hasExpectedErrors.ExpectedErrors() {
@@ -316,114 +451,36 @@ func (c *Collector) processExpectedErrors(op *openapi3.Operation, u usecase.Inte
 
 		errsByCode[statusCode] = append(errsByCode[statusCode], errResp)
 
-		if err := c.setJSONResponse(op, errResp, statusCode); err != nil {
-			return err
-		}
+		c.setOCJSONResponse(oc, errResp, statusCode)
 	}
 
-	return c.combineErrors(op, statusCodes, errsByCode)
+	c.combineOCErrors(oc, statusCodes, errsByCode)
 }
 
-func (c *Collector) combineErrors(op *openapi3.Operation, statusCodes []int, errsByCode map[int][]interface{}) error {
+func (c *Collector) combineOCErrors(oc openapi.OperationContext, statusCodes []int, errsByCode map[int][]interface{}) {
 	for _, statusCode := range statusCodes {
-		var (
-			errResps = errsByCode[statusCode]
-			err      error
-		)
+		errResps := errsByCode[statusCode]
 
 		if len(errResps) == 1 || c.CombineErrors == "" {
-			err = c.setJSONResponse(op, errResps[0], statusCode)
+			c.setOCJSONResponse(oc, errResps[0], statusCode)
 		} else {
 			switch c.CombineErrors {
 			case "oneOf":
-				err = c.setJSONResponse(op, jsonschema.OneOf(errResps...), statusCode)
+				c.setOCJSONResponse(oc, jsonschema.OneOf(errResps...), statusCode)
 			case "anyOf":
-				err = c.setJSONResponse(op, jsonschema.AnyOf(errResps...), statusCode)
+				c.setOCJSONResponse(oc, jsonschema.AnyOf(errResps...), statusCode)
 			default:
-				return errors.New("oneOf/anyOf expected for openapi.Collector.CombineErrors, " +
+				panic("oneOf/anyOf expected for openapi.Collector.CombineErrors, " +
 					c.CombineErrors + " received")
 			}
 		}
-
-		if err != nil {
-			return err
-		}
 	}
-
-	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 {
-		pp := p.Parameter
-
-		required := false
-		if pp.Required != nil && *pp.Required {
-			required = true
-		}
-
-		sc := paramSchema(pp)
-
-		if sc == nil {
-			if validator != nil {
-				err := validator.AddSchema(rest.ParamIn(pp.In), pp.Name, nil, required)
-				if err != nil {
-					return fmt.Errorf("failed to add validation schema for parameter (%s, %s): %w", pp.In, pp.Name, err)
-				}
-			}
-
-			continue
-		}
-
-		schema := sc.ToJSONSchema(c.Reflector().Spec)
-
-		var (
-			err        error
-			schemaData []byte
-		)
-
-		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)
-			}
-		}
-
-		if validator != nil {
-			err = validator.AddSchema(rest.ParamIn(pp.In), pp.Name, schemaData, required)
-			if err != nil {
-				return fmt.Errorf("failed to add validation schema for parameter (%s, %s): %w", pp.In, pp.Name, err)
-			}
-		}
-	}
-
-	return nil
-}
-
-func paramSchema(p *openapi3.Parameter) *openapi3.SchemaOrRef {
-	sc := p.Schema
-
-	if sc == nil {
-		if jsc, ok := p.Content["application/json"]; ok {
-			sc = jsc.Schema
-		}
-	}
-
-	return sc
-}
-
 // ProvideRequestJSONSchemas provides JSON Schemas for request structure.
 func (c *Collector) ProvideRequestJSONSchemas(
 	method string,
@@ -431,126 +488,26 @@ func (c *Collector) ProvideRequestJSONSchemas(
 	mapping rest.RequestMapping,
 	validator rest.JSONSchemaValidator,
 ) error {
-	op := openapi3.Operation{}
-	oc := openapi3.OperationContext{
-		Operation:  &op,
-		HTTPMethod: method,
-		Input:      input,
-	}
-
-	setRequestMapping(&oc, mapping)
-
-	err := c.Reflector().SetupRequest(oc)
-	if err != nil {
-		return err
-	}
-
-	err = c.provideParametersJSONSchemas(op, validator)
-	if err != nil {
-		return err
-	}
-
-	if op.RequestBody == nil || op.RequestBody.RequestBody == nil {
-		return nil
-	}
-
-	for ct, content := range op.RequestBody.RequestBody.Content {
-		schema := content.Schema.ToJSONSchema(c.Reflector().Spec)
-		if schema.IsTrivial(c.Reflector().ResolveJSONSchemaRef) {
-			continue
-		}
-
-		if ct == "application/json" {
-			schemaData, err := schema.JSONSchemaBytes()
-			if err != nil {
-				return fmt.Errorf("failed to build JSON Schema for request body: %w", err)
-			}
-
-			err = validator.AddSchema(rest.ParamInBody, "body", schemaData, false)
-			if err != nil {
-				return fmt.Errorf("failed to add validation schema for request body: %w", err)
-			}
-		}
-
-		if ct == "application/x-www-form-urlencoded" {
-			if err = provideFormDataSchemas(schema, validator); err != nil {
-				return err
-			}
-		}
-	}
-
-	return nil
-}
-
-func provideFormDataSchemas(schema jsonschema.SchemaOrBool, validator rest.JSONSchemaValidator) error {
-	for name, sch := range schema.TypeObject.Properties {
-		if sch.TypeObject != nil && len(schema.TypeObject.ExtraProperties) > 0 {
-			cp := *sch.TypeObject
-			sch.TypeObject = &cp
-			sch.TypeObject.ExtraProperties = schema.TypeObject.ExtraProperties
-		}
-
-		sb, err := sch.JSONSchemaBytes()
-		if err != nil {
-			return fmt.Errorf("failed to build JSON Schema for form data parameter %q: %w", name, err)
-		}
-
-		isRequired := false
-
-		for _, req := range schema.TypeObject.Required {
-			if req == name {
-				isRequired = true
-
-				break
-			}
-		}
-
-		err = validator.AddSchema(rest.ParamInFormData, name, sb, isRequired)
-		if err != nil {
-			return fmt.Errorf("failed to add validation schema for request body: %w", err)
-		}
-	}
-
-	return nil
-}
-
-func (c *Collector) provideHeaderSchemas(resp *openapi3.Response, validator rest.JSONSchemaValidator) error {
-	for name, h := range resp.Headers {
-		if h.Header.Schema == nil {
-			continue
-		}
-
-		hh := h.Header
-		schema := hh.Schema.ToJSONSchema(c.Reflector().Spec)
+	cu := openapi.ContentUnit{}
+	cu.Structure = input
+	setFieldMapping(&cu, mapping)
 
-		var (
-			err        error
-			schemaData []byte
-		)
-
-		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)
-			}
-		}
+	r := c.Refl()
 
-		required := false
-		if hh.Required != nil && *hh.Required {
-			required = true
+	err := r.WalkRequestJSONSchemas(method, cu, c.jsonSchemaCallback(validator, r), func(oc openapi.OperationContext) {
+		fv, ok := validator.(unknownFieldsValidator)
+		if !ok {
+			return
 		}
 
-		if validator != nil {
-			name = http.CanonicalHeaderKey(name)
-
-			err = validator.AddSchema(rest.ParamInHeader, name, schemaData, required)
-			if err != nil {
-				return fmt.Errorf("failed to add validation schema for response header (%s): %w", name, err)
+		for _, in := range []openapi.In{openapi.InQuery, openapi.InCookie, openapi.InHeader} {
+			if oc.UnknownParamsAreForbidden(in) {
+				fv.ForbidUnknownParams(rest.ParamIn(in), true)
 			}
 		}
-	}
+	})
 
-	return nil
+	return err
 }
 
 // ProvideResponseJSONSchemas provides JSON schemas for response structure.
@@ -561,58 +518,55 @@ func (c *Collector) ProvideResponseJSONSchemas(
 	headerMapping map[string]string,
 	validator rest.JSONSchemaValidator,
 ) error {
-	op := openapi3.Operation{}
-	oc := openapi3.OperationContext{
-		Operation:         &op,
-		HTTPStatus:        statusCode,
-		Output:            output,
-		RespHeaderMapping: headerMapping,
-		RespContentType:   contentType,
-	}
+	cu := openapi.ContentUnit{}
+	cu.Structure = output
+	cu.SetFieldMapping(openapi.InHeader, headerMapping)
+	cu.ContentType = contentType
+	cu.HTTPStatus = statusCode
 
-	if oc.RespContentType == "" {
-		oc.RespContentType = c.DefaultSuccessResponseContentType
+	if cu.ContentType == "" {
+		cu.ContentType = c.DefaultSuccessResponseContentType
 	}
 
-	if err := c.Reflector().SetupResponse(oc); err != nil {
-		return err
-	}
+	r := c.Refl()
+	err := r.WalkResponseJSONSchemas(cu, c.jsonSchemaCallback(validator, r), nil)
 
-	resp := op.Responses.MapOfResponseOrRefValues[strconv.Itoa(statusCode)].Response
-
-	if err := c.provideHeaderSchemas(resp, validator); err != nil {
-		return err
-	}
+	return err
+}
 
-	for _, cont := range resp.Content {
-		if cont.Schema == nil {
-			continue
+func (c *Collector) jsonSchemaCallback(validator rest.JSONSchemaValidator, r openapi.Reflector) openapi.JSONSchemaCallback {
+	return func(in openapi.In, paramName string, schema *jsonschema.SchemaOrBool, required bool) error {
+		loc := string(in) + "." + paramName
+		if loc == "body.body" {
+			loc = "body"
 		}
 
-		schema := cont.Schema.ToJSONSchema(c.Reflector().Spec)
+		if schema == nil || schema.IsTrivial(r.ResolveJSONSchemaRef) {
+			if err := validator.AddSchema(rest.ParamIn(in), paramName, nil, required); err != nil {
+				return fmt.Errorf("add validation schema %s: %w", loc, err)
+			}
 
-		if schema.IsTrivial(c.Reflector().ResolveJSONSchemaRef) {
-			continue
+			return nil
 		}
 
 		schemaData, err := schema.JSONSchemaBytes()
 		if err != nil {
-			return errors.New("failed to build JSON Schema for response body")
+			return fmt.Errorf("marshal schema %s: %w", loc, err)
 		}
 
-		if err := validator.AddSchema(rest.ParamInBody, "body", schemaData, false); err != nil {
-			return fmt.Errorf("failed to add validation schema for response body: %w", err)
+		if err = validator.AddSchema(rest.ParamIn(in), paramName, schemaData, required); err != nil {
+			return fmt.Errorf("add validation schema %s: %w", loc, err)
 		}
-	}
 
-	return nil
+		return nil
+	}
 }
 
 func (c *Collector) ServeHTTP(rw http.ResponseWriter, _ *http.Request) {
 	c.mu.Lock()
 	defer c.mu.Unlock()
 
-	document, err := json.MarshalIndent(c.Reflector().Spec, "", " ")
+	document, err := json.MarshalIndent(c.SpecSchema(), "", " ")
 	if err != nil {
 		http.Error(rw, err.Error(), http.StatusInternalServerError)
 	}
diff --git a/request/factory.go b/request/factory.go
index a669730..34b993d 100644
--- a/request/factory.go
+++ b/request/factory.go
@@ -11,7 +11,7 @@ import (
 	"strings"
 
 	"github.com/swaggest/form/v5"
-	"github.com/swaggest/openapi-go/openapi3"
+	"github.com/swaggest/openapi-go"
 	"github.com/swaggest/refl"
 	"github.com/swaggest/rest"
 	"github.com/swaggest/rest/nethttp"
@@ -126,7 +126,7 @@ func (df *DecoderFactory) MakeDecoder(
 
 	method = strings.ToUpper(method)
 
-	_, forceRequestBody := input.(openapi3.RequestBodyEnforcer)
+	_, forceRequestBody := input.(openapi.RequestBodyEnforcer)
 
 	if method != http.MethodPost && method != http.MethodPut && method != http.MethodPatch && !forceRequestBody {
 		return &d
diff --git a/trait.go b/trait.go
index e9c819d..33c28ca 100644
--- a/trait.go
+++ b/trait.go
@@ -6,6 +6,7 @@ import (
 	"net/http"
 	"reflect"
 
+	"github.com/swaggest/openapi-go"
 	"github.com/swaggest/openapi-go/openapi3"
 	"github.com/swaggest/refl"
 	"github.com/swaggest/usecase"
@@ -39,7 +40,12 @@ type HandlerTrait struct {
 	RespValidator Validator
 
 	// OperationAnnotations are called after operation setup and before adding operation to documentation.
+	//
+	// Deprecated: use OpenAPIAnnotations.
 	OperationAnnotations []func(op *openapi3.Operation) error
+
+	// OpenAPIAnnotations are called after operation setup and before adding operation to documentation.
+	OpenAPIAnnotations []func(oc openapi.OperationContext) error
 }
 
 // RestHandler is an accessor.
diff --git a/web/service.go b/web/service.go
index 4954532..c6f34b6 100644
--- a/web/service.go
+++ b/web/service.go
@@ -7,6 +7,7 @@ import (
 
 	"github.com/go-chi/chi/v5"
 	"github.com/go-chi/chi/v5/middleware"
+	oapi "github.com/swaggest/openapi-go"
 	"github.com/swaggest/openapi-go/openapi3"
 	"github.com/swaggest/rest"
 	"github.com/swaggest/rest/chirouter"
@@ -34,13 +35,15 @@ func DefaultService(options ...func(s *Service, initialized bool)) *Service {
 
 	// Init API documentation schema.
 	if s.OpenAPICollector == nil {
-		c := &openapi.Collector{}
+		r := openapi3.NewReflector()
+		r.Spec = s.OpenAPI
+
+		c := openapi.NewCollector(r)
 
 		c.DefaultSuccessResponseContentType = response.DefaultSuccessResponseContentType
 		c.DefaultErrorResponseContentType = response.DefaultErrorResponseContentType
 
 		s.OpenAPICollector = c
-		s.OpenAPICollector.Reflector().Spec = s.OpenAPI
 	}
 
 	if s.Wrapper == nil {
@@ -83,9 +86,12 @@ type Service struct {
 	*chirouter.Wrapper
 
 	PanicRecoveryMiddleware func(handler http.Handler) http.Handler // Default is middleware.Recoverer.
-	OpenAPI                 *openapi3.Spec
-	OpenAPICollector        *openapi.Collector
-	DecoderFactory          *request.DecoderFactory
+
+	// Deprecated: use openapi.Collector.
+	OpenAPI *openapi3.Spec
+
+	OpenAPICollector *openapi.Collector
+	DecoderFactory   *request.DecoderFactory
 
 	// Response validation is not enabled by default for its less justifiable performance impact.
 	// This field is populated so that response.ValidatorMiddleware(s.ResponseValidatorFactory) can be
@@ -93,6 +99,18 @@ type Service struct {
 	ResponseValidatorFactory rest.ResponseValidatorFactory
 }
 
+// OpenAPISchema returns OpenAPI schema.
+//
+// Returned value can be type asserted to *openapi3.Spec or marshaled.
+func (s *Service) OpenAPISchema() oapi.SpecSchema {
+	return s.OpenAPICollector.SpecSchema()
+}
+
+// OpenAPIReflector returns OpenAPI structure reflector for customizations.
+func (s *Service) OpenAPIReflector() oapi.Reflector {
+	return s.OpenAPICollector.Refl()
+}
+
 // Delete adds the route `pattern` that matches a DELETE http method to invoke use case interactor.
 func (s *Service) Delete(pattern string, uc usecase.Interactor, options ...func(h *nethttp.Handler)) {
 	s.Method(http.MethodDelete, pattern, nethttp.NewHandler(uc, options...))
@@ -137,6 +155,9 @@ func (s *Service) Trace(pattern string, uc usecase.Interactor, options ...func(h
 //
 // Swagger UI should be provided by `swgui` handler constructor, you can use one of these functions
 //
+//	github.com/swaggest/swgui/v5emb.New
+//	github.com/swaggest/swgui/v5cdn.New
+//	github.com/swaggest/swgui/v5.New
 //	github.com/swaggest/swgui/v4emb.New
 //	github.com/swaggest/swgui/v4cdn.New
 //	github.com/swaggest/swgui/v4.New


Test patch:
diff --git a/_examples/advanced-generic/_testdata/openapi.json b/_examples/advanced-generic/_testdata/openapi.json
index d9bb1f0..7e1081d 100644
--- a/_examples/advanced-generic/_testdata/openapi.json
+++ b/_examples/advanced-generic/_testdata/openapi.json
@@ -158,12 +158,7 @@
             "schema":{"type":"boolean","description":"Invokes internal decoding of compressed data."}
           }
         ],
-        "responses":{
-          "200":{
-            "description":"OK","headers":{"X-Header":{"style":"simple","schema":{"type":"string"}}},
-            "content":{"application/json":{}}
-          }
-        },
+        "responses":{"200":{"description":"OK","headers":{"X-Header":{"style":"simple","schema":{"type":"string"}}}}},
         "x-forbid-unknown-query":true
       }
     },
@@ -470,13 +465,9 @@
                 "style":"simple","description":"Receives req value of X-Foo reduced by 30.",
                 "schema":{"type":"integer","description":"Receives req value of X-Foo reduced by 30."}
               }
-            },
-            "content":{"application/json":{}}
+            }
           },
-          "500":{
-            "description":"Internal Server Error",
-            "content":{"application/json":{"schema":{"$ref":"#/components/schemas/AdvancedCustomErr"}}}
-          }
+          "500":{"description":"Internal Server Error"}
         }
       }
     },
diff --git a/_examples/advanced-generic/gzip_pass_through_test.go b/_examples/advanced-generic/gzip_pass_through_test.go
index 7d37fb9..c50bb04 100644
--- a/_examples/advanced-generic/gzip_pass_through_test.go
+++ b/_examples/advanced-generic/gzip_pass_through_test.go
@@ -20,6 +20,7 @@ func Test_directGzip(t *testing.T) {
 	require.NoError(t, err)
 
 	req.Header.Set("Accept-Encoding", "gzip")
+
 	rw := httptest.NewRecorder()
 
 	r.ServeHTTP(rw, req)
@@ -37,6 +38,7 @@ func Test_noDirectGzip(t *testing.T) {
 	require.NoError(t, err)
 
 	req.Header.Set("Accept-Encoding", "gzip")
+
 	rw := httptest.NewRecorder()
 
 	r.ServeHTTP(rw, req)
diff --git a/_examples/advanced/_testdata/openapi.json b/_examples/advanced/_testdata/openapi.json
index e6d9712..d46e050 100644
--- a/_examples/advanced/_testdata/openapi.json
+++ b/_examples/advanced/_testdata/openapi.json
@@ -140,12 +140,7 @@
             "schema":{"type":"boolean","description":"Invokes internal decoding of compressed data."}
           }
         ],
-        "responses":{
-          "200":{
-            "description":"OK","headers":{"X-Header":{"style":"simple","schema":{"type":"string"}}},
-            "content":{"application/dummy+json":{}}
-          }
-        },
+        "responses":{"200":{"description":"OK","headers":{"X-Header":{"style":"simple","schema":{"type":"string"}}}}},
         "x-forbid-unknown-query":true
       }
     },
@@ -358,8 +353,7 @@
                 "style":"simple","description":"Sample response header.",
                 "schema":{"type":"string","description":"Sample response header."}
               }
-            },
-            "content":{"application/dummy+json":{}}
+            }
           }
         }
       }
diff --git a/_examples/advanced/gzip_pass_through_test.go b/_examples/advanced/gzip_pass_through_test.go
index f7b4c9a..23bf6b6 100644
--- a/_examples/advanced/gzip_pass_through_test.go
+++ b/_examples/advanced/gzip_pass_through_test.go
@@ -18,6 +18,7 @@ func Test_directGzip(t *testing.T) {
 	require.NoError(t, err)
 
 	req.Header.Set("Accept-Encoding", "gzip")
+
 	rw := httptest.NewRecorder()
 
 	r.ServeHTTP(rw, req)
@@ -35,6 +36,7 @@ func Test_noDirectGzip(t *testing.T) {
 	require.NoError(t, err)
 
 	req.Header.Set("Accept-Encoding", "gzip")
+
 	rw := httptest.NewRecorder()
 
 	r.ServeHTTP(rw, req)
diff --git a/chirouter/example_test.go b/chirouter/example_test.go
new file mode 100644
index 0000000..d969e88
--- /dev/null
+++ b/chirouter/example_test.go
@@ -0,0 +1,57 @@
+package chirouter_test
+
+import (
+	"bytes"
+	"fmt"
+	"net/http"
+	"net/http/httptest"
+
+	"github.com/go-chi/chi/v5"
+	"github.com/swaggest/rest"
+	"github.com/swaggest/rest/chirouter"
+	"github.com/swaggest/rest/request"
+)
+
+func ExamplePathToURLValues() {
+	// Instantiate decoder factory with gorillamux.PathToURLValues.
+	// Single factory can be used to create multiple request decoders.
+	decoderFactory := request.NewDecoderFactory()
+	decoderFactory.ApplyDefaults = true
+	decoderFactory.SetDecoderFunc(rest.ParamInPath, chirouter.PathToURLValues)
+
+	// Define request structure for your HTTP handler.
+	type myRequest struct {
+		Query1    int     `query:"query1"`
+		Path1     string  `path:"path1"`
+		Path2     int     `path:"path2"`
+		Header1   float64 `header:"X-Header-1"`
+		FormData1 bool    `formData:"formData1"`
+		FormData2 string  `formData:"formData2"`
+	}
+
+	// Create decoder for that request structure.
+	dec := decoderFactory.MakeDecoder(http.MethodPost, myRequest{}, nil)
+
+	router := chi.NewRouter()
+
+	// Now in router handler you can decode *http.Request into a Go structure.
+	router.Handle("/foo/{path1}/bar/{path2}", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+		var in myRequest
+
+		_ = dec.Decode(r, &in, nil)
+
+		fmt.Printf("%+v\n", in)
+	}))
+
+	// Serving example URL.
+	w := httptest.NewRecorder()
+	req, _ := http.NewRequest(http.MethodPost, "/foo/abc/bar/123?query1=321",
+		bytes.NewBufferString("formData1=true&formData2=def"))
+
+	req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
+	req.Header.Set("X-Header-1", "1.23")
+
+	router.ServeHTTP(w, req)
+	// Output:
+	// {Query1:321 Path1:abc Path2:123 Header1:1.23 FormData1:true FormData2:def}
+}
diff --git a/chirouter/wrapper_test.go b/chirouter/wrapper_test.go
index d36ccd4..605e6af 100644
--- a/chirouter/wrapper_test.go
+++ b/chirouter/wrapper_test.go
@@ -263,7 +263,7 @@ func TestWrapper_Use_StripSlashes(t *testing.T) {
 
 func TestWrapper_Mount(t *testing.T) {
 	service := web.DefaultService()
-	service.OpenAPI.Info.Title = "Security and Mount Example"
+	service.OpenAPISchema().SetTitle("Security and Mount Example")
 
 	apiV1 := web.DefaultService()
 
@@ -354,7 +354,7 @@ func TestWrapper_Mount(t *testing.T) {
 		},
 		"securitySchemes":{"Admin":{"type":"http","scheme":"basic","description":"Admin access"}}
 	  }
-	}`), service.OpenAPI)
+	}`), service.OpenAPISchema())
 }
 
 func TestWrapper_With(t *testing.T) {
diff --git a/gorillamux/collector_test.go b/gorillamux/collector_test.go
new file mode 100644
index 0000000..e6a3154
--- /dev/null
+++ b/gorillamux/collector_test.go
@@ -0,0 +1,235 @@
+package gorillamux_test
+
+import (
+	"net/http"
+	"testing"
+
+	"github.com/gorilla/mux"
+	"github.com/stretchr/testify/assert"
+	"github.com/swaggest/assertjson"
+	"github.com/swaggest/openapi-go"
+	"github.com/swaggest/openapi-go/openapi3"
+	"github.com/swaggest/rest/gorillamux"
+	"github.com/swaggest/usecase"
+)
+
+type structuredHandler struct {
+	usecase.Info
+	usecase.WithInput
+	usecase.WithOutput
+}
+
+func (s structuredHandler) SetupOpenAPIOperation(oc openapi.OperationContext) error {
+	oc.AddReqStructure(s.Input)
+	oc.AddRespStructure(s.Output)
+
+	return nil
+}
+
+func newStructuredHandler(setup func(h *structuredHandler)) structuredHandler {
+	h := structuredHandler{}
+	setup(&h)
+
+	return h
+}
+
+func (s structuredHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {}
+
+func TestOpenAPICollector_Walker(t *testing.T) {
+	r := mux.NewRouter()
+
+	r.Use(func(handler http.Handler) http.Handler {
+		return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+			handler.ServeHTTP(w, r)
+		})
+	})
+
+	r.HandleFunc("/products", nil).Methods(http.MethodGet)
+	r.HandleFunc("/articles", nil).Methods(http.MethodGet)
+	r.Handle("/products/{key}",
+		newStructuredHandler(func(h *structuredHandler) {
+			h.Input = struct {
+				Key string `path:"key"`
+			}{}
+			h.Output = struct{}{}
+		})).
+		Methods(http.MethodGet).
+		Queries("key", "value")
+	r.Handle("/articles/{category}/",
+		newStructuredHandler(func(h *structuredHandler) {
+			h.Input = struct {
+				Filter   string `query:"filter"`
+				Category string `path:"category"`
+			}{}
+		})).
+		Methods(http.MethodGet).
+		Host("{subdomain:[a-z]+}.example.com")
+
+	s := r.Host("www.example.com").Subrouter()
+
+	s.Handle("/articles/{category}/{id:[0-9]+}", newStructuredHandler(func(h *structuredHandler) {
+		h.Input = struct {
+			Filter   string `query:"filter"`
+			Category string `path:"category"`
+			ID       string `path:"id"`
+		}{}
+	})).
+		Methods(http.MethodGet).
+		Headers("X-Requested-With", "XMLHttpRequest")
+
+	r.HandleFunc("/specific", nil).Methods(http.MethodPost)
+	r.PathPrefix("/").Handler(nil)
+
+	http.Handle("/", r)
+
+	rf := openapi3.NewReflector()
+	rf.Spec.Info.
+		WithTitle("Test Server").
+		WithVersion("v1.2.3").
+		WithDescription("Provides API over HTTP")
+
+	c := gorillamux.NewOpenAPICollector(rf)
+
+	assert.NoError(t, r.Walk(c.Walker))
+
+	assertjson.EqMarshal(t, `{
+	  "openapi":"3.0.3",
+	  "info":{
+		"title":"Test Server","description":"Provides API over HTTP",
+		"version":"v1.2.3"
+	  },
+	  "paths":{
+		"/articles":{
+		  "get":{
+			"tags":["Incomplete"],
+			"description":"Information about this operation was obtained using only HTTP method and path pattern. It may be incomplete and/or inaccurate.",
+			"responses":{
+			  "200":{
+				"description":"OK",
+				"content":{"text/html":{"schema":{"type":"string"}}}
+			  }
+			}
+		  }
+		},
+		"/products":{
+		  "get":{
+			"tags":["Incomplete"],
+			"description":"Information about this operation was obtained using only HTTP method and path pattern. It may be incomplete and/or inaccurate.",
+			"responses":{
+			  "200":{
+				"description":"OK",
+				"content":{"text/html":{"schema":{"type":"string"}}}
+			  }
+			}
+		  }
+		},
+		"/products/{key}":{
+		  "get":{
+			"parameters":[
+			  {
+				"name":"key","in":"path","required":true,"schema":{"type":"string"}
+			  }
+			],
+			"responses":{"200":{"description":"OK"}}
+		  }
+		},
+		"/specific":{
+		  "post":{
+			"tags":["Incomplete"],
+			"description":"Information about this operation was obtained using only HTTP method and path pattern. It may be incomplete and/or inaccurate.",
+			"responses":{
+			  "200":{
+				"description":"OK",
+				"content":{"text/html":{"schema":{"type":"string"}}}
+			  }
+			}
+		  }
+		}
+	  }
+	}`, rf.Spec)
+
+	rf = openapi3.NewReflector()
+	rf.Spec.Info.
+		WithTitle("Test Server (www.example.com)").
+		WithVersion("v1.2.3").
+		WithDescription("Provides API over HTTP")
+	rf.Spec.WithServers(openapi3.Server{
+		URL: "www.example.com",
+	})
+
+	c = gorillamux.NewOpenAPICollector(rf)
+	c.Host = "www.example.com"
+
+	assert.NoError(t, r.Walk(c.Walker))
+
+	assertjson.EqMarshal(t, `{
+	  "openapi":"3.0.3",
+	  "info":{
+		"title":"Test Server (www.example.com)",
+		"description":"Provides API over HTTP","version":"v1.2.3"
+	  },
+	  "servers":[{"url":"www.example.com"}],
+	  "paths":{
+		"/articles/{category}/{id}":{
+		  "get":{
+			"parameters":[
+			  {"name":"filter","in":"query","schema":{"type":"string"}},
+			  {
+				"name":"category","in":"path","required":true,
+				"schema":{"type":"string"}
+			  },
+			  {"name":"id","in":"path","required":true,"schema":{"type":"string"}}
+			],
+			"responses":{"200":{"description":"OK"}}
+		  }
+		}
+	  }
+	}`, rf.Spec)
+
+	rf = openapi3.NewReflector()
+	rf.Spec.Info.
+		WithTitle("Test Server ({subdomain}.example.com)").
+		WithVersion("v1.2.3").
+		WithDescription("Provides API over HTTP")
+	rf.Spec.WithServers(openapi3.Server{
+		URL: "{subdomain}.example.com",
+		Variables: map[string]openapi3.ServerVariable{
+			"subdomain": {
+				Default: "foo",
+			},
+		},
+	})
+
+	c = gorillamux.NewOpenAPICollector(rf)
+	c.Host = "{subdomain:[a-z]+}.example.com"
+
+	assert.NoError(t, r.Walk(c.Walker))
+
+	assertjson.EqMarshal(t, `{
+	  "openapi":"3.0.3",
+	  "info":{
+		"title":"Test Server ({subdomain}.example.com)",
+		"description":"Provides API over HTTP","version":"v1.2.3"
+	  },
+	  "servers":[
+		{
+		  "url":"{subdomain}.example.com",
+		  "variables":{"subdomain":{"default":"foo"}}
+		}
+	  ],
+	  "paths":{
+		"/articles/{category}/":{
+		  "get":{
+			"parameters":[
+			  {"name":"filter","in":"query","schema":{"type":"string"}},
+			  {
+				"name":"category","in":"path","required":true,
+				"schema":{"type":"string"}
+			  }
+			],
+			"responses":{"200":{"description":"OK"}}
+		  }
+		}
+	  }
+	}`, rf.Spec)
+}
diff --git a/gorillamux/example_openapi_collector_test.go b/gorillamux/example_openapi_collector_test.go
new file mode 100644
index 0000000..e4e1d87
--- /dev/null
+++ b/gorillamux/example_openapi_collector_test.go
@@ -0,0 +1,190 @@
+package gorillamux_test
+
+import (
+	"encoding/json"
+	"fmt"
+	"net/http"
+	"strconv"
+
+	"github.com/gorilla/mux"
+	"github.com/swaggest/openapi-go"
+	"github.com/swaggest/openapi-go/openapi3"
+	"github.com/swaggest/rest"
+	"github.com/swaggest/rest/gorillamux"
+	"github.com/swaggest/rest/nethttp"
+	"github.com/swaggest/rest/request"
+)
+
+// Define request structure for your HTTP handler.
+type myRequest struct {
+	Query1    int     `query:"query1"`
+	Path1     string  `path:"path1"`
+	Path2     int     `path:"path2"`
+	Header1   float64 `header:"X-Header-1"`
+	FormData1 bool    `formData:"formData1"`
+	FormData2 string  `formData:"formData2"`
+}
+
+type myResp struct {
+	Sum    float64 `json:"sum"`
+	Concat string  `json:"concat"`
+}
+
+func newMyHandler() *myHandler {
+	decoderFactory := request.NewDecoderFactory()
+	decoderFactory.ApplyDefaults = true
+	decoderFactory.SetDecoderFunc(rest.ParamInPath, gorillamux.PathToURLValues)
+
+	return &myHandler{
+		dec: decoderFactory.MakeDecoder(http.MethodPost, myRequest{}, nil),
+	}
+}
+
+type myHandler struct {
+	// Automated request decoding is not required to collect OpenAPI schema,
+	// but it is good to have to establish a single source of truth and to simplify request reading.
+	dec nethttp.RequestDecoder
+}
+
+func (m *myHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
+	var in myRequest
+
+	if err := m.dec.Decode(r, &in, nil); err != nil {
+		http.Error(w, err.Error(), http.StatusBadRequest)
+
+		return
+	}
+
+	// Serve request.
+	out := myResp{
+		Sum:    in.Header1 + float64(in.Path2) + float64(in.Query1),
+		Concat: in.Path1 + in.FormData2 + strconv.FormatBool(in.FormData1),
+	}
+
+	j, err := json.Marshal(out)
+	if err != nil {
+		http.Error(w, err.Error(), http.StatusInternalServerError)
+
+		return
+	}
+
+	_, _ = w.Write(j)
+}
+
+// SetupOpenAPIOperation declares OpenAPI schema for the handler.
+func (m *myHandler) SetupOpenAPIOperation(oc openapi.OperationContext) error {
+	oc.SetTags("My Tag")
+	oc.SetSummary("My Summary")
+	oc.SetDescription("This endpoint aggregates request in structured way.")
+
+	oc.AddReqStructure(myRequest{})
+	oc.AddRespStructure(myResp{})
+	oc.AddRespStructure(nil, openapi.WithContentType("text/html"), openapi.WithHTTPStatus(http.StatusBadRequest))
+	oc.AddRespStructure(nil, openapi.WithContentType("text/html"), openapi.WithHTTPStatus(http.StatusInternalServerError))
+
+	return nil
+}
+
+func ExampleNewOpenAPICollector() {
+	// Your router does not need special instrumentation.
+	router := mux.NewRouter()
+
+	// If handler implements gorillamux.OpenAPIPreparer, it will contribute detailed information to OpenAPI document.
+	router.Handle("/foo/{path1}/bar/{path2}", newMyHandler()).Methods(http.MethodGet)
+
+	// If handler does not implement gorillamux.OpenAPIPreparer, it will be exposed as incomplete.
+	router.Handle("/uninstrumented-handler/{path-item}",
+		http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {})).Methods(http.MethodPost)
+
+	// Setup OpenAPI schema.
+	refl := openapi3.NewReflector()
+	refl.SpecSchema().SetTitle("Sample API")
+	refl.SpecSchema().SetVersion("v1.2.3")
+	refl.SpecSchema().SetDescription("This is an example.")
+
+	// Walk the router with OpenAPI collector.
+	c := gorillamux.NewOpenAPICollector(refl)
+
+	_ = router.Walk(c.Walker)
+
+	// Get the resulting schema.
+	yml, _ := refl.Spec.MarshalYAML()
+	fmt.Println(string(yml))
+
+	// Output:
+	// openapi: 3.0.3
+	// info:
+	//   description: This is an example.
+	//   title: Sample API
+	//   version: v1.2.3
+	// paths:
+	//   /foo/{path1}/bar/{path2}:
+	//     get:
+	//       description: This endpoint aggregates request in structured way.
+	//       parameters:
+	//       - in: query
+	//         name: query1
+	//         schema:
+	//           type: integer
+	//       - in: path
+	//         name: path1
+	//         required: true
+	//         schema:
+	//           type: string
+	//       - in: path
+	//         name: path2
+	//         required: true
+	//         schema:
+	//           type: integer
+	//       - in: header
+	//         name: X-Header-1
+	//         schema:
+	//           type: number
+	//       responses:
+	//         "200":
+	//           content:
+	//             application/json:
+	//               schema:
+	//                 $ref: '#/components/schemas/GorillamuxTestMyResp'
+	//           description: OK
+	//         "400":
+	//           content:
+	//             text/html:
+	//               schema:
+	//                 type: string
+	//           description: Bad Request
+	//         "500":
+	//           content:
+	//             text/html:
+	//               schema:
+	//                 type: string
+	//           description: Internal Server Error
+	//       summary: My Summary
+	//       tags:
+	//       - My Tag
+	//   /uninstrumented-handler/{path-item}:
+	//     post:
+	//       description: Information about this operation was obtained using only HTTP method
+	//         and path pattern. It may be incomplete and/or inaccurate.
+	//       parameters:
+	//       - in: path
+	//         name: path-item
+	//       responses:
+	//         "200":
+	//           content:
+	//             text/html:
+	//               schema:
+	//                 type: string
+	//           description: OK
+	//       tags:
+	//       - Incomplete
+	// components:
+	//   schemas:
+	//     GorillamuxTestMyResp:
+	//       properties:
+	//         concat:
+	//           type: string
+	//         sum:
+	//           type: number
+	//       type: object
+}
diff --git a/gorillamux/example_test.go b/gorillamux/example_test.go
new file mode 100644
index 0000000..068be32
--- /dev/null
+++ b/gorillamux/example_test.go
@@ -0,0 +1,57 @@
+package gorillamux_test
+
+import (
+	"bytes"
+	"fmt"
+	"net/http"
+	"net/http/httptest"
+
+	"github.com/gorilla/mux"
+	"github.com/swaggest/rest"
+	"github.com/swaggest/rest/gorillamux"
+	"github.com/swaggest/rest/request"
+)
+
+func ExamplePathToURLValues() {
+	// Instantiate decoder factory with gorillamux.PathToURLValues.
+	// Single factory can be used to create multiple request decoders.
+	decoderFactory := request.NewDecoderFactory()
+	decoderFactory.ApplyDefaults = true
+	decoderFactory.SetDecoderFunc(rest.ParamInPath, gorillamux.PathToURLValues)
+
+	// Define request structure for your HTTP handler.
+	type myRequest struct {
+		Query1    int     `query:"query1"`
+		Path1     string  `path:"path1"`
+		Path2     int     `path:"path2"`
+		Header1   float64 `header:"X-Header-1"`
+		FormData1 bool    `formData:"formData1"`
+		FormData2 string  `formData:"formData2"`
+	}
+
+	// Create decoder for that request structure.
+	dec := decoderFactory.MakeDecoder(http.MethodPost, myRequest{}, nil)
+
+	router := mux.NewRouter()
+
+	// Now in router handler you can decode *http.Request into a Go structure.
+	router.Handle("/foo/{path1}/bar/{path2}", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+		var in myRequest
+
+		_ = dec.Decode(r, &in, nil)
+
+		fmt.Printf("%+v\n", in)
+	}))
+
+	// Serving example URL.
+	w := httptest.NewRecorder()
+	req, _ := http.NewRequest(http.MethodPost, "/foo/abc/bar/123?query1=321",
+		bytes.NewBufferString("formData1=true&formData2=def"))
+
+	req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
+	req.Header.Set("X-Header-1", "1.23")
+
+	router.ServeHTTP(w, req)
+	// Output:
+	// {Query1:321 Path1:abc Path2:123 Header1:1.23 FormData1:true FormData2:def}
+}
diff --git a/nethttp/example_test.go b/nethttp/example_test.go
index 6d8db9f..b6c0fb5 100644
--- a/nethttp/example_test.go
+++ b/nethttp/example_test.go
@@ -57,7 +57,7 @@ func ExampleSecurityMiddleware() {
 		With(serviceTokenAuth, serviceTokenDoc). // Apply a pair of middlewares: actual security and documentation.
 		Method(http.MethodGet, "/foo", nethttp.NewHandler(u))
 
-	schema, _ := assertjson.MarshalIndentCompact(apiSchema.Reflector().Spec, "", " ", 120)
+	schema, _ := assertjson.MarshalIndentCompact(apiSchema.SpecSchema(), "", " ", 120)
 	fmt.Println(string(schema))
 
 	// Output:
diff --git a/nethttp/openapi_test.go b/nethttp/openapi_test.go
index 4055ea5..f009181 100644
--- a/nethttp/openapi_test.go
+++ b/nethttp/openapi_test.go
@@ -67,7 +67,7 @@ func TestOpenAPIMiddleware(t *testing.T) {
 		assert.True(t, nethttp.MiddlewareIsWrapper(w), i)
 	}
 
-	sp, err := assertjson.MarshalIndentCompact(c.Reflector().Spec, "", " ", 100)
+	sp, err := assertjson.MarshalIndentCompact(c.SpecSchema(), "", " ", 100)
 	require.NoError(t, err)
 
 	assertjson.Equal(t, []byte(`{
diff --git a/nethttp/options_test.go b/nethttp/options_test.go
index ed38066..9a3e105 100644
--- a/nethttp/options_test.go
+++ b/nethttp/options_test.go
@@ -1,23 +1,64 @@
 package nethttp_test
 
 import (
+	"context"
+	"net/http"
 	"testing"
 
 	"github.com/stretchr/testify/require"
 	"github.com/swaggest/assertjson"
 	"github.com/swaggest/openapi-go/openapi3"
 	"github.com/swaggest/rest/nethttp"
+	"github.com/swaggest/rest/web"
+	"github.com/swaggest/usecase"
 )
 
 func TestRequestBodyContent(t *testing.T) {
 	h := &nethttp.Handler{}
-	op := openapi3.Operation{}
+
+	r := openapi3.NewReflector()
+	oc, err := r.NewOperationContext(http.MethodPost, "/")
+	require.NoError(t, err)
 
 	nethttp.RequestBodyContent("text/plain")(h)
-	require.Len(t, h.OperationAnnotations, 1)
-	require.NoError(t, h.OperationAnnotations[0](&op))
-	assertjson.EqualMarshal(t, []byte(`{
-	  "requestBody":{"content":{"text/plain":{"schema":{"type":"string"}}}},
-	  "responses":{}
-	}`), op)
+	require.Len(t, h.OpenAPIAnnotations, 1)
+	require.NoError(t, h.OpenAPIAnnotations[0](oc))
+
+	require.NoError(t, r.AddOperation(oc))
+
+	assertjson.EqMarshal(t, `{
+	  "openapi":"3.0.3","info":{"title":"","version":""},
+	  "paths":{
+		"/":{
+		  "post":{
+			"requestBody":{"content":{"text/plain":{"schema":{"type":"string"}}}},
+			"responses":{"204":{"description":"No Content"}}
+		  }
+		}
+	  }
+	}`, r.SpecSchema())
+}
+
+func TestRequestBodyContent_webService(t *testing.T) {
+	s := web.DefaultService()
+
+	u := usecase.NewIOI(new(string), nil, func(ctx context.Context, input, output interface{}) error {
+		return nil
+	})
+
+	s.Post("/text-req-body", u, nethttp.RequestBodyContent("text/csv"))
+
+	assertjson.EqMarshal(t, `{
+	  "openapi":"3.0.3","info":{"title":"","version":""},
+	  "paths":{
+		"/text-req-body":{
+		  "post":{
+			"summary":"Test Request Body Content _ web Service",
+			"operationId":"rest/nethttp_test.TestRequestBodyContent_webService",
+			"requestBody":{"content":{"text/csv":{"schema":{"type":"string"}}}},
+			"responses":{"204":{"description":"No Content"}}
+		  }
+		}
+	  }
+	}`, s.OpenAPISchema())
 }
diff --git a/openapi/collector_test.go b/openapi/collector_test.go
index 6854dc1..12675c0 100644
--- a/openapi/collector_test.go
+++ b/openapi/collector_test.go
@@ -97,7 +97,7 @@ func TestCollector_Collect(t *testing.T) {
 		ReqValidator: &jsonschema.Validator{},
 	}))
 
-	j, err := json.MarshalIndent(c.Reflector().Spec, "", " ")
+	j, err := json.MarshalIndent(c.SpecSchema(), "", " ")
 	require.NoError(t, err)
 
 	rw := httptest.NewRecorder()
@@ -223,7 +223,7 @@ func TestCollector_Collect_requestMapping(t *testing.T) {
 		  }
 		}
 	  }
-	}`, collector.Reflector().SpecEns())
+	}`, collector.SpecSchema())
 
 	val := validatorMock{
 		AddSchemaFunc: func(in rest.ParamIn, name string, schemaData []byte, required bool) error {
@@ -321,7 +321,7 @@ func TestCollector_Collect_CombineErrors(t *testing.T) {
 		  }
 		}
 	  }
-	}`, collector.Reflector().SpecEns())
+	}`, collector.SpecSchema())
 }
 
 // Output that implements OutputWithHTTPStatus interface.
@@ -397,7 +397,7 @@ func TestCollector_Collect_multipleHttpStatuses(t *testing.T) {
 		  }
 		}
 	  }
-	}`, c.Reflector().SpecEns())
+	}`, c.SpecSchema())
 }
 
 func TestCollector_Collect_queryObject(t *testing.T) {
@@ -465,5 +465,57 @@ func TestCollector_Collect_queryObject(t *testing.T) {
 		  "QueryOpenapiTestJsonFilter":{"type":"object","properties":{"foo":{"type":"string"}}}
 		}
 	  }
-	}`, c.Reflector().SpecEns())
+	}`, c.SpecSchema())
+}
+
+func TestCollector_Collect_head_no_response(t *testing.T) {
+	c := openapi.Collector{}
+	u := usecase.IOInteractor{}
+
+	type resp struct {
+		Foo string `json:"foo"`
+		Bar string `header:"X-Bar"`
+	}
+
+	u.Output = new(resp)
+
+	require.NoError(t, c.Collect(http.MethodHead, "/foo", u, rest.HandlerTrait{
+		ReqValidator: &jsonschema.Validator{},
+	}))
+
+	require.NoError(t, c.Collect(http.MethodGet, "/foo", u, rest.HandlerTrait{
+		ReqValidator: &jsonschema.Validator{},
+	}))
+
+	assertjson.EqMarshal(t, `{
+	  "openapi":"3.0.3","info":{"title":"","version":""},
+	  "paths":{
+		"/foo":{
+		  "get":{
+			"responses":{
+			  "200":{
+				"description":"OK",
+				"headers":{"X-Bar":{"style":"simple","schema":{"type":"string"}}},
+				"content":{
+				  "application/json":{"schema":{"$ref":"#/components/schemas/OpenapiTestResp"}}
+				}
+			  }
+			}
+		  },
+		  "head":{
+			"responses":{
+			  "200":{
+				"description":"OK",
+				"headers":{"X-Bar":{"style":"simple","schema":{"type":"string"}}}
+			  }
+			}
+		  }
+		}
+	  },
+	  "components":{
+		"schemas":{
+		  "OpenapiTestResp":{"type":"object","properties":{"foo":{"type":"string"}}}
+		}
+	  }
+	}`, c.SpecSchema())
 }
diff --git a/request/file_test.go b/request/file_test.go
index 09b776f..5bf68bb 100644
--- a/request/file_test.go
+++ b/request/file_test.go
@@ -12,6 +12,7 @@ import (
 	"github.com/go-chi/chi/v5"
 	"github.com/stretchr/testify/assert"
 	"github.com/stretchr/testify/require"
+	"github.com/swaggest/openapi-go/openapi3"
 	"github.com/swaggest/rest"
 	"github.com/swaggest/rest/chirouter"
 	"github.com/swaggest/rest/jsonschema"
@@ -62,14 +63,14 @@ func TestDecoder_Decode_fileUploadOptional(t *testing.T) {
 
 func TestDecoder_Decode_fileUploadTag(t *testing.T) {
 	r := chirouter.NewWrapper(chi.NewRouter())
-	apiSchema := openapi.Collector{}
+	apiSchema := openapi.NewCollector(openapi3.NewReflector())
 	decoderFactory := request.NewDecoderFactory()
-	validatorFactory := jsonschema.NewFactory(&apiSchema, &apiSchema)
+	validatorFactory := jsonschema.NewFactory(apiSchema, apiSchema)
 
 	decoderFactory.SetDecoderFunc(rest.ParamInPath, chirouter.PathToURLValues)
 
 	ws := []func(handler http.Handler) http.Handler{
-		nethttp.OpenAPIMiddleware(&apiSchema),
+		nethttp.OpenAPIMiddleware(apiSchema),
 		request.DecoderMiddleware(decoderFactory),
 		request.ValidatorMiddleware(validatorFactory),
 		response.EncoderMiddleware,
diff --git a/web/_testdata/openapi.json b/web/_testdata/openapi.json
index 76033e8..6ab353f 100644
--- a/web/_testdata/openapi.json
+++ b/web/_testdata/openapi.json
@@ -139,7 +139,7 @@
             "name":"id","in":"path","required":true,"schema":{"type":"integer"}
           }
         ],
-        "responses":{"200":{"description":"OK","content":{"application/json":{}}}}
+        "responses":{"200":{"description":"OK"}}
       }
     }
   },