Repository: swaggest/rest
Pull request #85 (2022-06-20 23:31:20)

Few changes

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

| Language  | Files |   Lines    |    Code    | Comments |   Blanks   | Complexity |     Bytes      |
|-----------|-------|------------|------------|----------|------------|------------|----------------|
| Go        | 99    | 6819 (+50) | 4948 (+37) | 457 (+2) | 1414 (+11) | 800 (+10)  | 165.3K (+975B) |
| Go (test) | 44    | 4637 (+57) | 3564 (+49) | 145 (+5) | 928 (+3)   | 120        | 128.7K (+1.2K) |
| JSON      | 3     | 1319 (-7)  | 1319 (-7)  | 0        | 0          | 0          | 54K (-461B)    |
| Markdown  | 3     | 454 (+35)  | 348 (+24)  | 0        | 106 (+11)  | 0          | 15.8K (+1.1K)  |
<!-- Sticky Pull Request CommentLOC -->
github-actions[bot]: ### Unit Test Coverage
total:								(statements)			83.2%
changed lines: (statements) 94.7%
<details><summary>Coverage of changed lines</summary>

|           File           |      Function      | Coverage |
|--------------------------|--------------------|----------|
| Total                    |                    | 94.7%    |
| openapi/collector.go     |                    | 100.0%   |
| openapi/collector.go:188 | processUseCase     | 100.0%   |
| request/factory.go       |                    | 100.0%   |
| request/factory.go:93    | MakeDecoder        | 100.0%   |
| trait.go                 |                    | 87.5%    |
| trait.go:54              | OutputHasNoContent | 87.5%    |

</details>

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

|               File                |      Function      | Base Coverage | Current Coverage |
|-----------------------------------|--------------------|---------------|------------------|
| Total                             |                    | 82.9%         | 83.2% (+0.3%)    |
| github.com/swaggest/rest/trait.go | OutputHasNoContent | 87.5%         | 88.9% (+1.4%)    |

</details>
<!-- Sticky Pull Request Commentunit-test -->
github-actions[bot]: ### Go API Changes

<pre>
# summary
Inferred base version: v0.2.28
Suggested version: v0.3.0
</pre>
<!-- Sticky Pull Request Commentgorelease -->
codecov[bot]: # [Codecov](https://codecov.io/gh/swaggest/rest/pull/85?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=swaggest) Report
> Merging [#85](https://codecov.io/gh/swaggest/rest/pull/85?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=swaggest) (0fcf91e) into [master](https://codecov.io/gh/swaggest/rest/commit/d3230eb14c8e51d9f331836032dfd73c1c62c4e2?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=swaggest) (d3230eb) will **increase** coverage by `0.24%`.
> The diff coverage is `90.24%`.

```diff
@@            Coverage Diff             @@
##           master      #85      +/-   ##
==========================================
+ Coverage   76.96%   77.21%   +0.24%     
==========================================
  Files          27       27              
  Lines        1385     1413      +28     
==========================================
+ Hits         1066     1091      +25     
- Misses        206      207       +1     
- Partials      113      115       +2     
```

| Flag | Coverage Δ | |
|---|---|---|
| unittests | `77.21% <90.24%> (+0.24%)` | :arrow_up: |

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.

| [Impacted Files](https://codecov.io/gh/swaggest/rest/pull/85?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=swaggest) | Coverage Δ | |
|---|---|---|
| [nethttp/openapi.go](https://codecov.io/gh/swaggest/rest/pull/85/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=swaggest#diff-bmV0aHR0cC9vcGVuYXBpLmdv) | `93.84% <ø> (ø)` | |
| [trait.go](https://codecov.io/gh/swaggest/rest/pull/85/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=swaggest#diff-dHJhaXQuZ28=) | `85.18% <80.00%> (-8.15%)` | :arrow_down: |
| [openapi/collector.go](https://codecov.io/gh/swaggest/rest/pull/85/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=swaggest#diff-b3BlbmFwaS9jb2xsZWN0b3IuZ28=) | `63.43% <100.00%> (+2.16%)` | :arrow_up: |
| [request/factory.go](https://codecov.io/gh/swaggest/rest/pull/85/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=swaggest#diff-cmVxdWVzdC9mYWN0b3J5Lmdv) | `86.60% <100.00%> (+0.12%)` | :arrow_up: |

:mega: Codecov can now indicate which changes are the most critical in Pull Requests. [Learn more](https://about.codecov.io/product/feature/runtime-insights/?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.26µs ± 0%    1.30µs ± 2%  +2.92%  (p=0.016 n=4+5)
pkg:github.com/swaggest/rest/request goos:linux goarch:amd64
Decoder_Decode-2                           700ns ± 4%     687ns ± 1%    ~     (p=0.151 n=5+5)
DecoderFunc_Decode-2                      1.99µs ± 1%    1.96µs ± 1%  -1.54%  (p=0.016 n=5+4)
Decoder_Decode_json-2                     21.5µs ± 1%    21.2µs ± 1%  -1.52%  (p=0.016 n=5+5)
Decoder_Decode_queryObject-2              4.73µs ± 0%    4.70µs ± 1%    ~     (p=0.056 n=5+5)
Decoder_Decode_jsonParam-2                1.78µs ± 1%    1.76µs ± 0%  -0.84%  (p=0.024 n=5+5)
DecoderFactory_SetDecoderFunc-2           1.63µs ± 0%    1.62µs ± 0%    ~     (p=0.079 n=5+5)
pkg:github.com/swaggest/rest/response/gzip goos:linux goarch:amd64
Middleware-2                              11.0µs ± 1%    11.0µs ± 0%    ~     (p=0.897 n=5+5)
Middleware_control-2                      3.15µs ± 1%    3.13µs ± 3%    ~     (p=0.310 n=5+5)

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.17kB ± 2%    1.18kB ± 4%    ~     (p=0.841 n=5+5)
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.30µs ± 2%
pkg:github.com/swaggest/rest/request goos:linux goarch:amd64
Decoder_Decode-2                         687ns ± 1%
DecoderFunc_Decode-2                    1.96µs ± 1%
Decoder_Decode_json-2                   21.2µs ± 1%
Decoder_Decode_queryObject-2            4.70µs ± 1%
Decoder_Decode_jsonParam-2              1.76µs ± 0%
DecoderFactory_SetDecoderFunc-2         1.62µs ± 0%
pkg:github.com/swaggest/rest/response/gzip goos:linux goarch:amd64
Middleware-2                            11.0µs ± 0%
Middleware_control-2                    3.13µ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.18kB ± 4%
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.2µs ± 4%    22.6µs ± 1%    ~     (p=0.310 n=5+5)
_directGzipHead-2            22.6µs ± 6%    22.8µs ± 2%    ~     (p=0.421 n=5+5)
_noDirectGzip-2               129µs ± 2%     135µs ± 9%    ~     (p=0.222 n=5+5)
_directGzip_decode-2          441µs ± 5%     449µs ± 9%    ~     (p=0.548 n=5+5)
_noDirectGzip_decode-2        128µs ± 2%     131µs ± 3%    ~     (p=0.222 n=5+5)
_jsonBody-2                  42.5µs ± 5%    42.1µs ± 4%    ~     (p=0.548 n=5+5)
_jsonBodyValidation-2        48.1µs ± 5%    48.4µs ± 3%    ~     (p=0.690 n=5+5)
_outputHeaders-2             22.2µs ± 2%    21.9µs ± 2%    ~     (p=0.310 n=5+5)
_requestResponseMapping-2    41.7µs ± 3%    41.5µs ± 1%    ~     (p=1.000 n=5+5)
_validation-2                43.1µs ± 1%    45.7µs ± 4%  +5.94%  (p=0.008 n=5+5)
_noValidation-2              31.9µs ± 3%    34.0µs ± 4%  +6.51%  (p=0.008 n=5+5)
pkg:github.com/swaggest/rest/_examples/advanced-generic goos:linux goarch:amd64
_directGzip-2                23.3µs ± 8%    23.1µs ± 4%    ~     (p=0.841 n=5+5)
_directGzipHead-2            22.5µs ± 1%    22.7µs ± 3%    ~     (p=0.278 n=5+5)
_noDirectGzip-2               140µs ± 5%     134µs ± 6%    ~     (p=0.151 n=5+5)
_directGzip_decode-2          458µs ± 4%     444µs ± 3%    ~     (p=0.151 n=5+5)
_noDirectGzip_decode-2        131µs ± 3%     131µs ± 5%    ~     (p=0.730 n=5+4)
_jsonBodyManual-2            25.7µs ± 1%    26.1µs ± 3%    ~     (p=0.151 n=5+5)
_jsonBody-2                  37.2µs ± 2%    37.9µs ± 2%    ~     (p=0.095 n=5+5)
_jsonBodyValidation-2        48.0µs ± 2%    48.6µs ± 4%    ~     (p=0.690 n=5+5)
_outputHeaders-2             22.0µs ± 2%    22.3µs ± 2%    ~     (p=0.222 n=5+5)
_requestResponseMapping-2    44.6µs ± 3%    41.2µs ± 1%  -7.56%  (p=0.008 n=5+5)
_validation-2                45.3µs ± 4%    44.5µs ± 1%    ~     (p=0.151 n=5+5)
_noValidation-2              32.4µs ± 4%    33.8µs ± 4%    ~     (p=0.095 n=5+5)
pkg:github.com/swaggest/rest/_examples/task-api/internal/infra/nethttp goos:linux goarch:amd64
_notFoundSrv-2               26.8µs ±12%    26.6µs ±14%    ~     (p=0.690 n=5+5)
_ok-2                        27.4µs ± 4%    25.9µs ± 2%  -5.36%  (p=0.016 n=5+5)
_invalidBody-2               38.6µs ± 7%    36.3µs ± 2%    ~     (p=0.095 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                   639 ± 0%       639 ± 0%    ~     (all equal)
_directGzipHead-2               183 ± 0%       183 ± 0%    ~     (all equal)
_noDirectGzip-2               1.04k ± 0%     1.04k ± 0%    ~     (all equal)
_directGzip_decode-2            639 ± 0%       639 ± 0%    ~     (all equal)
_noDirectGzip_decode-2        1.04k ± 0%     1.04k ± 0%    ~     (all equal)
_jsonBody-2                     208 ± 0%       208 ± 0%    ~     (all equal)
_jsonBodyValidation-2           194 ± 0%       194 ± 0%    ~     (all equal)
_outputHeaders-2                155 ± 0%       155 ± 0%    ~     (all equal)
_requestResponseMapping-2      94.0 ± 0%      94.0 ± 0%    ~     (all equal)
_validation-2                   177 ± 0%       177 ± 0%    ~     (all equal)
_noValidation-2                 177 ± 0%       177 ± 0%    ~     (all equal)
pkg:github.com/swaggest/rest/_examples/advanced-generic goos:linux goarch:amd64
_directGzip-2                   653 ± 0%       653 ± 0%    ~     (all equal)
_directGzipHead-2               197 ± 0%       197 ± 0%    ~     (all equal)
_noDirectGzip-2               1.06k ± 0%     1.06k ± 0%    ~     (all equal)
_directGzip_decode-2            653 ± 0%       653 ± 0%    ~     (all equal)
_noDirectGzip_decode-2        1.06k ± 0%     1.06k ± 0%    ~     (all equal)
_jsonBodyManual-2               222 ± 0%       222 ± 0%    ~     (all equal)
_jsonBody-2                     222 ± 0%       222 ± 0%    ~     (all equal)
_jsonBodyValidation-2           208 ± 0%       208 ± 0%    ~     (all equal)
_outputHeaders-2                169 ± 0%       169 ± 0%    ~     (all equal)
_requestResponseMapping-2       108 ± 0%       108 ± 0%    ~     (all equal)
_validation-2                   191 ± 0%       191 ± 0%    ~     (all equal)
_noValidation-2                 191 ± 0%       191 ± 0%    ~     (all equal)
pkg:github.com/swaggest/rest/_examples/task-api/internal/infra/nethttp goos:linux goarch:amd64
_notFoundSrv-2                  337 ± 0%       337 ± 0%    ~     (all equal)
_ok-2                           359 ± 0%       359 ± 0%    ~     (p=0.444 n=5+5)
_invalidBody-2                  435 ± 0%       435 ± 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)
_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               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/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                 43.2k ± 4%     44.2k ± 1%    ~     (p=0.310 n=5+5)
_directGzipHead-2             44.3k ± 6%     43.9k ± 2%    ~     (p=0.421 n=5+5)
_noDirectGzip-2               7.73k ± 2%     7.42k ± 9%    ~     (p=0.222 n=5+5)
_directGzip_decode-2          2.27k ± 5%     2.23k ± 8%    ~     (p=0.548 n=5+5)
_noDirectGzip_decode-2        7.79k ± 2%     7.62k ± 3%    ~     (p=0.222 n=5+5)
_jsonBody-2                   23.6k ± 5%     23.8k ± 4%    ~     (p=0.548 n=5+5)
_jsonBodyValidation-2         20.8k ± 5%     20.6k ± 3%    ~     (p=0.690 n=5+5)
_outputHeaders-2              45.1k ± 2%     45.7k ± 2%    ~     (p=0.310 n=5+5)
_requestResponseMapping-2     24.0k ± 3%     24.1k ± 1%    ~     (p=1.000 n=5+5)
_validation-2                 23.2k ± 1%     21.9k ± 4%  -5.52%  (p=0.008 n=5+5)
_noValidation-2               31.3k ± 3%     29.4k ± 5%  -6.08%  (p=0.008 n=5+5)
pkg:github.com/swaggest/rest/_examples/advanced-generic goos:linux goarch:amd64
_directGzip-2                 43.0k ± 8%     43.3k ± 4%    ~     (p=0.841 n=5+5)
_directGzipHead-2             44.5k ± 1%     44.1k ± 3%    ~     (p=0.278 n=5+5)
_noDirectGzip-2               7.15k ± 5%     7.48k ± 5%    ~     (p=0.151 n=5+5)
_directGzip_decode-2          2.18k ± 4%     2.25k ± 3%    ~     (p=0.151 n=5+5)
_noDirectGzip_decode-2        7.62k ± 3%     7.03k ±34%    ~     (p=1.000 n=5+5)
_jsonBodyManual-2             38.9k ± 1%     38.4k ± 3%    ~     (p=0.151 n=5+5)
_jsonBody-2                   26.8k ± 2%     26.4k ± 2%    ~     (p=0.095 n=5+5)
_jsonBodyValidation-2         20.8k ± 2%     20.6k ± 4%    ~     (p=0.690 n=5+5)
_outputHeaders-2              45.5k ± 2%     44.9k ± 2%    ~     (p=0.222 n=5+5)
_requestResponseMapping-2     22.4k ± 3%     24.3k ± 1%  +8.13%  (p=0.008 n=5+5)
_validation-2                 22.1k ± 4%     22.5k ± 1%    ~     (p=0.151 n=5+5)
_noValidation-2               30.8k ± 4%     29.6k ± 4%    ~     (p=0.095 n=5+5)
pkg:github.com/swaggest/rest/_examples/task-api/internal/infra/nethttp goos:linux goarch:amd64
_notFoundSrv-2                37.4k ±11%     37.7k ±13%    ~     (p=0.690 n=5+5)
_ok-2                         36.5k ± 4%     38.6k ± 2%  +5.60%  (p=0.016 n=5+5)
_invalidBody-2                25.9k ± 8%     27.5k ± 2%    ~     (p=0.095 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.92kB ± 0%    3.92kB ± 0%    ~     (p=0.317 n=4+5)
_directGzipHead-2            3.92kB ± 0%    3.92kB ± 0%    ~     (p=0.238 n=5+5)
_noDirectGzip-2              6.85kB ±21%    6.46kB ±19%    ~     (p=0.310 n=5+5)
_directGzip_decode-2          403kB ± 0%     403kB ± 0%    ~     (p=1.000 n=5+5)
_noDirectGzip_decode-2       5.51kB ±13%    5.86kB ± 6%    ~     (p=0.222 n=5+5)
_jsonBody-2                  13.2kB ± 0%    13.2kB ± 0%    ~     (p=0.603 n=5+5)
_jsonBodyValidation-2        19.0kB ± 0%    19.0kB ± 0%    ~     (p=0.802 n=5+5)
_outputHeaders-2             3.71kB ± 0%    3.71kB ± 0%    ~     (p=0.968 n=4+5)
_requestResponseMapping-2    16.7kB ± 0%    16.7kB ± 0%    ~     (p=0.317 n=5+5)
_validation-2                16.6kB ± 0%    16.6kB ± 0%    ~     (p=0.056 n=5+5)
_noValidation-2              7.88kB ± 0%    7.88kB ± 0%    ~     (p=0.127 n=5+5)
pkg:github.com/swaggest/rest/_examples/advanced-generic goos:linux goarch:amd64
_directGzip-2                3.97kB ± 0%    3.97kB ± 0%    ~     (p=1.000 n=5+5)
_directGzipHead-2            3.97kB ± 0%    3.97kB ± 0%    ~     (p=0.643 n=5+5)
_noDirectGzip-2              7.00kB ±15%    6.83kB ±18%    ~     (p=0.841 n=5+5)
_directGzip_decode-2          403kB ± 0%     403kB ± 0%    ~     (p=0.548 n=5+5)
_noDirectGzip_decode-2       5.85kB ± 7%    5.93kB ±11%    ~     (p=0.690 n=5+5)
_jsonBodyManual-2            4.64kB ± 0%    4.64kB ± 0%    ~     (p=1.000 n=5+5)
_jsonBody-2                  10.4kB ± 0%    10.4kB ± 0%    ~     (p=0.794 n=4+5)
_jsonBodyValidation-2        19.1kB ± 0%    19.1kB ± 0%    ~     (p=0.230 n=5+5)
_outputHeaders-2             3.74kB ± 0%    3.74kB ± 0%    ~     (p=0.381 n=5+5)
_requestResponseMapping-2    16.7kB ± 0%    16.7kB ± 0%    ~     (p=0.865 n=5+5)
_validation-2                16.7kB ± 0%    16.7kB ± 0%    ~     (p=0.484 n=5+5)
_noValidation-2              7.94kB ± 0%    7.94kB ± 0%    ~     (p=0.524 n=5+5)
pkg:github.com/swaggest/rest/_examples/task-api/internal/infra/nethttp goos:linux goarch:amd64
_notFoundSrv-2               4.88kB ± 0%    4.88kB ± 0%    ~     (p=0.643 n=5+5)
_ok-2                        4.78kB ± 0%    4.78kB ± 0%    ~     (p=0.400 n=4+4)
_invalidBody-2               8.67kB ± 0%    8.67kB ± 0%    ~     (p=0.343 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.8 ± 4%    ~     (p=1.000 n=5+5)
_directGzip_decode-2            501 ± 0%       501 ± 0%    ~     (p=0.968 n=4+5)
_noDirectGzip_decode-2         50.0 ± 0%      50.0 ± 0%    ~     (all equal)
_jsonBody-2                     130 ± 0%       130 ± 0%    ~     (all equal)
_jsonBodyValidation-2           186 ± 0%       186 ± 0%    ~     (all equal)
_outputHeaders-2               37.0 ± 0%      37.0 ± 0%    ~     (all equal)
_requestResponseMapping-2       126 ± 0%       126 ± 0%    ~     (all equal)
_validation-2                   155 ± 0%       155 ± 0%    ~     (all equal)
_noValidation-2                92.0 ± 0%      92.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.6 ± 3%      51.4 ± 3%    ~     (p=1.000 n=5+5)
_directGzip_decode-2            503 ± 0%       503 ± 0%    ~     (p=1.000 n=5+5)
_noDirectGzip_decode-2         52.0 ± 0%      52.0 ± 0%    ~     (all equal)
_jsonBodyManual-2              51.0 ± 0%      51.0 ± 0%    ~     (all equal)
_jsonBody-2                     102 ± 0%       102 ± 0%    ~     (all equal)
_jsonBodyValidation-2           188 ± 0%       188 ± 0%    ~     (all equal)
_outputHeaders-2               38.0 ± 0%      38.0 ± 0%    ~     (all equal)
_requestResponseMapping-2       128 ± 0%       128 ± 0%    ~     (all equal)
_validation-2                   157 ± 0%       157 ± 0%    ~     (all equal)
_noValidation-2                94.0 ± 0%      94.0 ± 0%    ~     (all equal)
pkg:github.com/swaggest/rest/_examples/task-api/internal/infra/nethttp goos:linux goarch:amd64
_notFoundSrv-2                 54.0 ± 0%      54.0 ± 0%    ~     (all equal)
_ok-2                          50.0 ± 0%      50.0 ± 0%    ~     (all equal)
_invalidBody-2                  100 ± 0%       100 ± 0%    ~     (all equal)
```
</details>
<!-- Sticky Pull Request Commentbench-examples -->

Patch:
diff --git a/README.md b/README.md
index c8a6f12..021e63a 100644
--- a/README.md
+++ b/README.md
@@ -198,6 +198,41 @@ u.Interactor = usecase.Interact(func(ctx context.Context, input, output interfac
 })
 ```
 
+### Initializing Web Service
+
+[Web Service](https://pkg.go.dev/github.com/swaggest/rest/web#DefaultService) is an instrumented facade in front of 
+router, it simplifies configuration and provides more compact API to add use cases.
+
+```go
+// Service initializes router with required middlewares.
+service := web.DefaultService()
+
+// It allows OpenAPI configuration.
+service.OpenAPI.Info.Title = "Albums API"
+service.OpenAPI.Info.WithDescription("This service provides API to manage albums.")
+service.OpenAPI.Info.Version = "v1.0.0"
+
+// Additional middlewares can be added.
+service.Use(
+    middleware.StripSlashes,
+
+    // cors.AllowAll().Handler, // "github.com/rs/cors", 3rd-party CORS middleware can also be configured here.
+)
+
+// Use cases can be mounted using short syntax .<Method>(...).
+service.Post("/albums", postAlbums(), nethttp.SuccessStatus(http.StatusCreated))
+
+log.Println("Starting service at http://localhost:8080")
+
+if err := http.ListenAndServe("localhost:8080", service); err != nil {
+    log.Fatal(err)
+}
+
+```
+
+Usually, `web.Service` API is sufficient, but if it is not, router can be configured manually, please check 
+the documentation below.
+
 ### Adding use case to router
 
 ```go
@@ -280,7 +315,7 @@ adminSecuritySchema := nethttp.HTTPBasicSecurityMiddleware(apiSchema, "Admin", "
 // Endpoints with admin access.
 r.Route("/admin", func(r chi.Router) {
     r.Group(func(r chi.Router) {
-        r.Use(adminAuth, adminSecuritySchema) // Add both middlewares to routing group to enforce and document security.
+        r.Wrap(adminAuth, adminSecuritySchema) // Add both middlewares to routing group to enforce and document security.
         r.Method(http.MethodPut, "/hello/{name}", nethttp.NewHandler(u))
     })
 })
diff --git a/_examples/advanced-generic/router.go b/_examples/advanced-generic/router.go
index 1cfe0c9..f613284 100644
--- a/_examples/advanced-generic/router.go
+++ b/_examples/advanced-generic/router.go
@@ -53,6 +53,8 @@ func NewRouter() http.Handler {
 		}
 	})
 
+	// 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.
 	s.OpenAPICollector.CombineErrors = "anyOf"
 
 	s.Wrap(
diff --git a/_examples/go.mod b/_examples/go.mod
index 1255339..d1eaf19 100644
--- a/_examples/go.mod
+++ b/_examples/go.mod
@@ -35,7 +35,7 @@ require (
 	github.com/santhosh-tekuri/jsonschema/v3 v3.1.0 // indirect
 	github.com/sergi/go-diff v1.2.0 // indirect
 	github.com/swaggest/form/v5 v5.0.1 // indirect
-	github.com/swaggest/refl v1.0.2 // indirect
+	github.com/swaggest/refl v1.1.0 // indirect
 	github.com/valyala/bytebufferpool v1.0.0 // indirect
 	github.com/vearutop/statigz v1.1.5 // indirect
 	github.com/yosuke-furukawa/json5 v0.1.2-0.20201207051438-cf7bb3f354ff // indirect
diff --git a/_examples/go.sum b/_examples/go.sum
index 9549507..9fc1509 100644
--- a/_examples/go.sum
+++ b/_examples/go.sum
@@ -60,8 +60,8 @@ github.com/swaggest/jsonschema-go v0.3.35 h1:LW5DC0WgR5YdQXyTRc5e8gLdKT0wkACg4aV
 github.com/swaggest/jsonschema-go v0.3.35/go.mod h1:JAF1nm+uIaMOXktuQepmkiRcgQ5yJk4Ccwx9HVt2cXw=
 github.com/swaggest/openapi-go v0.2.18 h1:9dTzNe91MoepI5PtHNUgby3R0ZNgTQte+pDh3X5XcDw=
 github.com/swaggest/openapi-go v0.2.18/go.mod h1:xUrd0cNiIfhkSIFwxmMmiw3FLegY/MQzSek3Byp4YjU=
-github.com/swaggest/refl v1.0.2 h1:VmP8smuDS1EzUPn31++TzMi13CAaVJdlWpIxzj0up88=
-github.com/swaggest/refl v1.0.2/go.mod h1:DoiPoBJPYHU6Z9fIA6zXQ9uI6VRL6M8BFX5YFT+ym9g=
+github.com/swaggest/refl v1.1.0 h1:a+9a75Kv6ciMozPjVbOfcVTEQe81t2R3emvaD9oGQGc=
+github.com/swaggest/refl v1.1.0/go.mod h1:g3Qa6ki0A/L2yxiuUpT+cuBURuRaltF5SDQpg1kMZSY=
 github.com/swaggest/swgui v1.4.5 h1:9kw3Lt+4qNdDwnRgpNSsx3s3BjoW/Inhbn7/38DX6Qk=
 github.com/swaggest/swgui v1.4.5/go.mod h1:qfAwdL49ikzxahZrLrPJYAruNuL1Nt6do8jtC9luAlI=
 github.com/swaggest/usecase v1.1.3 h1:SGnmV07jyDhdg+gqEAv/NNc8R18JQqJUs4wVq+LWa5g=
diff --git a/_examples/task-api/internal/infra/nethttp/router.go b/_examples/task-api/internal/infra/nethttp/router.go
index 8994e7d..2509796 100644
--- a/_examples/task-api/internal/infra/nethttp/router.go
+++ b/_examples/task-api/internal/infra/nethttp/router.go
@@ -8,40 +8,24 @@ import (
 	"github.com/go-chi/chi/v5/middleware"
 	"github.com/swaggest/openapi-go/openapi3"
 	"github.com/swaggest/rest"
-	"github.com/swaggest/rest/_examples/task-api/internal/infra/log"
 	"github.com/swaggest/rest/_examples/task-api/internal/infra/schema"
 	"github.com/swaggest/rest/_examples/task-api/internal/infra/service"
 	"github.com/swaggest/rest/_examples/task-api/internal/usecase"
-	"github.com/swaggest/rest/chirouter"
-	"github.com/swaggest/rest/jsonschema"
 	"github.com/swaggest/rest/nethttp"
-	"github.com/swaggest/rest/request"
-	"github.com/swaggest/rest/response"
+	"github.com/swaggest/rest/web"
 	swgui "github.com/swaggest/swgui/v4emb"
 )
 
 // NewRouter creates HTTP router.
 func NewRouter(locator *service.Locator) http.Handler {
-	apiSchema := schema.NewOpenAPICollector()
-	validatorFactory := jsonschema.NewFactory(apiSchema, apiSchema)
-	decoderFactory := request.NewDecoderFactory()
-	decoderFactory.SetDecoderFunc(rest.ParamInPath, chirouter.PathToURLValues)
+	s := web.DefaultService()
 
-	r := chirouter.NewWrapper(chi.NewRouter())
-
-	r.Wrap(
-		middleware.Recoverer, // Panic recovery.
-		nethttp.UseCaseMiddlewares(log.UseCaseMiddleware()), // Sample use case middleware.
-		nethttp.OpenAPIMiddleware(apiSchema),                // Documentation collector.
-		request.DecoderMiddleware(decoderFactory),           // Decoder setup.
-		request.ValidatorMiddleware(validatorFactory),       // Validator setup.
-		response.EncoderMiddleware,                          // Encoder setup.
-	)
+	schema.SetupOpenAPICollector(s.OpenAPICollector)
 
 	adminAuth := middleware.BasicAuth("Admin Access", map[string]string{"admin": "admin"})
 	userAuth := middleware.BasicAuth("User Access", map[string]string{"user": "user"})
 
-	r.Wrap(
+	s.Wrap(
 		middleware.NoCache,
 		middleware.Timeout(time.Second),
 	)
@@ -51,8 +35,8 @@ func NewRouter(locator *service.Locator) http.Handler {
 	}
 
 	// Unrestricted access.
-	r.Route("/dev", func(r chi.Router) {
-		r.Use(nethttp.AnnotateOpenAPI(apiSchema, func(op *openapi3.Operation) error {
+	s.Route("/dev", func(r chi.Router) {
+		r.Use(nethttp.AnnotateOpenAPI(s.OpenAPICollector, func(op *openapi3.Operation) error {
 			op.Tags = []string{"Dev Mode"}
 
 			return nil
@@ -68,33 +52,31 @@ func NewRouter(locator *service.Locator) http.Handler {
 	})
 
 	// Endpoints with admin access.
-	r.Route("/admin", func(r chi.Router) {
+	s.Route("/admin", func(r chi.Router) {
 		r.Group(func(r chi.Router) {
-			r.Use(nethttp.AnnotateOpenAPI(apiSchema, func(op *openapi3.Operation) error {
+			r.Use(nethttp.AnnotateOpenAPI(s.OpenAPICollector, func(op *openapi3.Operation) error {
 				op.Tags = []string{"Admin Mode"}
 
 				return nil
 			}))
-			r.Use(adminAuth, nethttp.HTTPBasicSecurityMiddleware(apiSchema, "Admin", "Admin access"))
+			r.Use(adminAuth, nethttp.HTTPBasicSecurityMiddleware(s.OpenAPICollector, "Admin", "Admin access"))
 			r.Method(http.MethodPut, "/tasks/{id}", nethttp.NewHandler(usecase.UpdateTask(locator), ff))
 		})
 	})
 
 	// Endpoints with user access.
-	r.Route("/user", func(r chi.Router) {
+	s.Route("/user", func(r chi.Router) {
 		r.Group(func(r chi.Router) {
-			r.Use(userAuth, nethttp.HTTPBasicSecurityMiddleware(apiSchema, "User", "User access"))
+			r.Use(userAuth, nethttp.HTTPBasicSecurityMiddleware(s.OpenAPICollector, "User", "User access"))
 			r.Method(http.MethodPost, "/tasks", nethttp.NewHandler(usecase.CreateTask(locator),
 				nethttp.SuccessStatus(http.StatusCreated)))
 		})
 	})
 
 	// Swagger UI endpoint at /docs.
-	r.Method(http.MethodGet, "/docs/openapi.json", apiSchema)
-	r.Mount("/docs", swgui.NewHandler(apiSchema.Reflector().Spec.Info.Title,
-		"/docs/openapi.json", "/docs"))
+	s.Docs("/docs", swgui.New)
 
-	r.Mount("/debug", middleware.Profiler())
+	s.Mount("/debug", middleware.Profiler())
 
-	return r
+	return s
 }
diff --git a/_examples/task-api/internal/infra/schema/openapi.go b/_examples/task-api/internal/infra/schema/openapi.go
index 9838713..733693d 100644
--- a/_examples/task-api/internal/infra/schema/openapi.go
+++ b/_examples/task-api/internal/infra/schema/openapi.go
@@ -6,9 +6,8 @@ import (
 	"github.com/swaggest/rest/openapi"
 )
 
-// NewOpenAPICollector creates API documentation collector.
-func NewOpenAPICollector() *openapi.Collector {
-	apiSchema := openapi.Collector{}
+// SetupOpenAPICollector sets up API documentation collector.
+func SetupOpenAPICollector(apiSchema *openapi.Collector) {
 	serviceInfo := openapi3.Info{}
 	serviceInfo.
 		WithTitle("Tasks Service").
@@ -16,6 +15,4 @@ func NewOpenAPICollector() *openapi.Collector {
 		WithVersion("1.2.3")
 
 	apiSchema.Reflector().SpecEns().WithInfo(serviceInfo)
-
-	return &apiSchema
 }
diff --git a/_examples/task-api/cmd/task-api/main.go b/_examples/task-api/main.go
similarity index 100%
rename from _examples/task-api/cmd/task-api/main.go
rename to _examples/task-api/main.go
diff --git a/go.mod b/go.mod
index e85617c..d832ff4 100644
--- a/go.mod
+++ b/go.mod
@@ -14,7 +14,7 @@ require (
 	github.com/swaggest/form/v5 v5.0.1
 	github.com/swaggest/jsonschema-go v0.3.35
 	github.com/swaggest/openapi-go v0.2.18
-	github.com/swaggest/refl v1.0.2
+	github.com/swaggest/refl v1.1.0
 	github.com/swaggest/usecase v1.1.3
 )
 
diff --git a/go.sum b/go.sum
index 69c8f3f..8011432 100644
--- a/go.sum
+++ b/go.sum
@@ -79,8 +79,9 @@ github.com/swaggest/jsonschema-go v0.3.35 h1:LW5DC0WgR5YdQXyTRc5e8gLdKT0wkACg4aV
 github.com/swaggest/jsonschema-go v0.3.35/go.mod h1:JAF1nm+uIaMOXktuQepmkiRcgQ5yJk4Ccwx9HVt2cXw=
 github.com/swaggest/openapi-go v0.2.18 h1:9dTzNe91MoepI5PtHNUgby3R0ZNgTQte+pDh3X5XcDw=
 github.com/swaggest/openapi-go v0.2.18/go.mod h1:xUrd0cNiIfhkSIFwxmMmiw3FLegY/MQzSek3Byp4YjU=
-github.com/swaggest/refl v1.0.2 h1:VmP8smuDS1EzUPn31++TzMi13CAaVJdlWpIxzj0up88=
 github.com/swaggest/refl v1.0.2/go.mod h1:DoiPoBJPYHU6Z9fIA6zXQ9uI6VRL6M8BFX5YFT+ym9g=
+github.com/swaggest/refl v1.1.0 h1:a+9a75Kv6ciMozPjVbOfcVTEQe81t2R3emvaD9oGQGc=
+github.com/swaggest/refl v1.1.0/go.mod h1:g3Qa6ki0A/L2yxiuUpT+cuBURuRaltF5SDQpg1kMZSY=
 github.com/swaggest/usecase v1.1.3 h1:SGnmV07jyDhdg+gqEAv/NNc8R18JQqJUs4wVq+LWa5g=
 github.com/swaggest/usecase v1.1.3/go.mod h1:gLSjsqHiDmHOIf081asqys7UUndFrTWrfNa2opxEt7k=
 github.com/yosuke-furukawa/json5 v0.1.2-0.20201207051438-cf7bb3f354ff h1:7YqG491bE4vstXRz1lD38rbSgbXnirvROz1lZiOnPO8=
diff --git a/nethttp/openapi.go b/nethttp/openapi.go
index 9d946e2..8ef8f18 100644
--- a/nethttp/openapi.go
+++ b/nethttp/openapi.go
@@ -120,7 +120,7 @@ func AnnotateOpenAPI(
 	}
 }
 
-// SecurityResponse is an security middleware option to customize response structure and status.
+// SecurityResponse is a security middleware option to customize response structure and status.
 func SecurityResponse(structure interface{}, httpStatus int) func(config *MiddlewareConfig) {
 	return func(config *MiddlewareConfig) {
 		config.ResponseStructure = structure
diff --git a/openapi/collector.go b/openapi/collector.go
index 41f6a40..6b0574e 100644
--- a/openapi/collector.go
+++ b/openapi/collector.go
@@ -27,8 +27,9 @@ type Collector struct {
 	// of multiple responses with same HTTP status code.
 	CombineErrors string
 
-	gen         *openapi3.Reflector
-	annotations map[string][]func(*openapi3.Operation) error
+	gen          *openapi3.Reflector
+	annotations  map[string][]func(*openapi3.Operation) error
+	operationIDs map[string]bool
 }
 
 // Reflector is an accessor to OpenAPI Reflector instance.
@@ -194,19 +195,49 @@ func (c *Collector) processUseCase(op *openapi3.Operation, u usecase.Interactor,
 	)
 
 	if usecase.As(u, &hasName) {
-		op.WithID(hasName.Name())
+		id := hasName.Name()
+
+		if id != "" {
+			if c.operationIDs == nil {
+				c.operationIDs = make(map[string]bool)
+			}
+
+			idSuf := id
+			suf := 1
+
+			for c.operationIDs[idSuf] {
+				suf++
+				idSuf = id + strconv.Itoa(suf)
+			}
+
+			c.operationIDs[idSuf] = true
+
+			op.WithID(idSuf)
+		}
 	}
 
 	if usecase.As(u, &hasTitle) {
-		op.WithSummary(hasTitle.Title())
+		title := hasTitle.Title()
+
+		if title != "" {
+			op.WithSummary(hasTitle.Title())
+		}
 	}
 
 	if usecase.As(u, &hasTags) {
-		op.WithTags(hasTags.Tags()...)
+		tags := hasTags.Tags()
+
+		if len(tags) > 0 {
+			op.WithTags(hasTags.Tags()...)
+		}
 	}
 
 	if usecase.As(u, &hasDescription) {
-		op.WithDescription(hasDescription.Description())
+		desc := hasDescription.Description()
+
+		if desc != "" {
+			op.WithDescription(hasDescription.Description())
+		}
 	}
 
 	if usecase.As(u, &hasDeprecated) && hasDeprecated.IsDeprecated() {
diff --git a/request/factory.go b/request/factory.go
index 7205882..3e78143 100644
--- a/request/factory.go
+++ b/request/factory.go
@@ -131,7 +131,8 @@ func (df *DecoderFactory) MakeDecoder(
 	}
 
 	// Checking for body tags.
-	if refl.HasTaggedFields(input, jsonTag) || refl.FindEmbeddedSliceOrMap(input) != nil {
+	if refl.HasTaggedFields(input, jsonTag) || refl.FindEmbeddedSliceOrMap(input) != nil ||
+		refl.IsSliceOrMap(input) || refl.IsScalar(input) {
 		if df.JSONReader != nil {
 			m.decoders = append(m.decoders, decodeJSONBody(df.JSONReader))
 		} else {
diff --git a/trait.go b/trait.go
index 1c04671..c197605 100644
--- a/trait.go
+++ b/trait.go
@@ -61,14 +61,30 @@ func OutputHasNoContent(output interface{}) bool {
 
 	rv := reflect.ValueOf(output)
 
-	if !withWriter && !noContent &&
-		!refl.HasTaggedFields(output, "json") &&
-		!refl.IsSliceOrMap(output) &&
-		refl.FindEmbeddedSliceOrMap(output) == nil &&
-		!refl.As(output, new(json.Marshaler)) &&
-		(rv.Kind() != reflect.Ptr || rv.Elem().Kind() != reflect.Interface) {
-		return true
+	kind := rv.Kind()
+	elemKind := reflect.Invalid
+
+	if kind == reflect.Ptr {
+		elemKind = rv.Elem().Kind()
+	}
+
+	hasTaggedFields := refl.HasTaggedFields(output, "json")
+	isSliceOrMap := refl.IsSliceOrMap(output)
+	hasEmbeddedSliceOrMap := refl.FindEmbeddedSliceOrMap(output) != nil
+	isJSONMarshaler := refl.As(output, new(json.Marshaler))
+	isPtrToInterface := elemKind == reflect.Interface
+	isScalar := refl.IsScalar(output)
+
+	if withWriter ||
+		noContent ||
+		hasTaggedFields ||
+		isSliceOrMap ||
+		hasEmbeddedSliceOrMap ||
+		isJSONMarshaler ||
+		isPtrToInterface ||
+		isScalar {
+		return false
 	}
 
-	return false
+	return true
 }


Test patch:
diff --git a/_examples/advanced-generic/_testdata/openapi.json b/_examples/advanced-generic/_testdata/openapi.json
index bd006ed..3a15b91 100644
--- a/_examples/advanced-generic/_testdata/openapi.json
+++ b/_examples/advanced-generic/_testdata/openapi.json
@@ -41,7 +41,7 @@
     },
     "/file-multi-upload":{
       "post":{
-        "summary":"Files Uploads With 'multipart/form-data'","description":"",
+        "summary":"Files Uploads With 'multipart/form-data'",
         "operationId":"_examples/advanced-generic.fileMultiUploader",
         "parameters":[
           {
@@ -61,8 +61,7 @@
     },
     "/file-upload":{
       "post":{
-        "summary":"File Upload With 'multipart/form-data'","description":"",
-        "operationId":"_examples/advanced-generic.fileUploader",
+        "summary":"File Upload With 'multipart/form-data'","operationId":"_examples/advanced-generic.fileUploader",
         "parameters":[
           {
             "name":"in_query","in":"query","description":"Simple scalar value in query.",
@@ -78,7 +77,7 @@
     },
     "/gzip-pass-through":{
       "get":{
-        "summary":"Direct Gzip","description":"","operationId":"_examples/advanced-generic.directGzip",
+        "summary":"Direct Gzip","operationId":"_examples/advanced-generic.directGzip",
         "parameters":[
           {
             "name":"plainStruct","in":"query","description":"Output plain structure instead of gzip container.",
@@ -98,7 +97,7 @@
         "x-forbid-unknown-query":true
       },
       "head":{
-        "summary":"Direct Gzip","description":"","operationId":"_examples/advanced-generic.directGzip",
+        "summary":"Direct Gzip","operationId":"_examples/advanced-generic.directGzip2",
         "parameters":[
           {
             "name":"plainStruct","in":"query","description":"Output plain structure instead of gzip container.",
@@ -263,8 +262,7 @@
     },
     "/json-slice-body":{
       "post":{
-        "summary":"Request With JSON Array In Body","description":"",
-        "operationId":"_examples/advanced-generic.jsonSliceBody",
+        "summary":"Request With JSON Array In Body","operationId":"_examples/advanced-generic.jsonSliceBody",
         "parameters":[
           {
             "name":"in_query","in":"query","description":"Simple scalar value in query.",
@@ -348,7 +346,7 @@
       },
       "head":{
         "summary":"Output With Headers","description":"Output with headers.",
-        "operationId":"_examples/advanced-generic.outputHeaders",
+        "operationId":"_examples/advanced-generic.outputHeaders2",
         "responses":{
           "200":{
             "description":"OK",
@@ -365,8 +363,7 @@
     },
     "/query-object":{
       "get":{
-        "summary":"Request With Object As Query Parameter","description":"",
-        "operationId":"_examples/advanced-generic.queryObject",
+        "summary":"Request With Object As Query Parameter","operationId":"_examples/advanced-generic.queryObject",
         "parameters":[
           {
             "name":"in_query","in":"query","description":"Object value in query.","style":"deepObject","explode":true,
@@ -606,4 +603,4 @@
       "TextprotoMIMEHeader":{"type":"object","additionalProperties":{"type":"array","items":{"type":"string"}},"nullable":true}
     }
   }
-}
\ No newline at end of file
+}
diff --git a/_examples/advanced/_testdata/openapi.json b/_examples/advanced/_testdata/openapi.json
index 4badc8d..2d5222d 100644
--- a/_examples/advanced/_testdata/openapi.json
+++ b/_examples/advanced/_testdata/openapi.json
@@ -41,8 +41,7 @@
     },
     "/file-multi-upload":{
       "post":{
-        "summary":"Files Uploads With 'multipart/form-data'","description":"",
-        "operationId":"_examples/advanced.fileMultiUploader",
+        "summary":"Files Uploads With 'multipart/form-data'","operationId":"_examples/advanced.fileMultiUploader",
         "parameters":[
           {
             "name":"in_query","in":"query","description":"Simple scalar value in query.",
@@ -61,8 +60,7 @@
     },
     "/file-upload":{
       "post":{
-        "summary":"File Upload With 'multipart/form-data'","description":"",
-        "operationId":"_examples/advanced.fileUploader",
+        "summary":"File Upload With 'multipart/form-data'","operationId":"_examples/advanced.fileUploader",
         "parameters":[
           {
             "name":"in_query","in":"query","description":"Simple scalar value in query.",
@@ -78,7 +76,7 @@
     },
     "/gzip-pass-through":{
       "get":{
-        "summary":"Direct Gzip","description":"","operationId":"_examples/advanced.directGzip",
+        "summary":"Direct Gzip","operationId":"_examples/advanced.directGzip",
         "parameters":[
           {
             "name":"plainStruct","in":"query","description":"Output plain structure instead of gzip container.",
@@ -98,7 +96,7 @@
         "x-forbid-unknown-query":true
       },
       "head":{
-        "summary":"Direct Gzip","description":"","operationId":"_examples/advanced.directGzip",
+        "summary":"Direct Gzip","operationId":"_examples/advanced.directGzip2",
         "parameters":[
           {
             "name":"plainStruct","in":"query","description":"Output plain structure instead of gzip container.",
@@ -122,7 +120,6 @@
       "post":{
         "summary":"Request With JSON Body and non-trivial validation",
         "description":"Request with JSON body and query/header/path params, response with JSON body and data from request.",
-        "operationId":"",
         "parameters":[
           {
             "name":"in_query","in":"query","description":"Simple scalar value in query.",
@@ -234,7 +231,7 @@
     },
     "/json-slice-body":{
       "post":{
-        "summary":"Request With JSON Array In Body","description":"","operationId":"_examples/advanced.jsonSliceBody",
+        "summary":"Request With JSON Array In Body","operationId":"_examples/advanced.jsonSliceBody",
         "parameters":[
           {
             "name":"in_query","in":"query","description":"Simple scalar value in query.",
@@ -279,7 +276,7 @@
     },
     "/output-csv-writer":{
       "get":{
-        "summary":"Output With Stream Writer","description":"Output with stream writer.","operationId":"",
+        "summary":"Output With Stream Writer","description":"Output with stream writer.",
         "responses":{
           "200":{
             "description":"OK",
@@ -300,7 +297,7 @@
     },
     "/output-headers":{
       "get":{
-        "summary":"Output With Headers","description":"Output with headers.","operationId":"",
+        "summary":"Output With Headers","description":"Output with headers.",
         "responses":{
           "200":{
             "description":"OK",
@@ -315,7 +312,7 @@
         }
       },
       "head":{
-        "summary":"Output With Headers","description":"Output with headers.","operationId":"",
+        "summary":"Output With Headers","description":"Output with headers.",
         "responses":{
           "200":{
             "description":"OK",
@@ -332,8 +329,7 @@
     },
     "/query-object":{
       "get":{
-        "summary":"Request With Object As Query Parameter","description":"",
-        "operationId":"_examples/advanced.queryObject",
+        "summary":"Request With Object As Query Parameter","operationId":"_examples/advanced.queryObject",
         "parameters":[
           {
             "name":"in_query","in":"query","description":"Object value in query.","style":"deepObject","explode":true,
diff --git a/_examples/task-api/tools_test.go b/_examples/task-api/dev_test.go
similarity index 61%
rename from _examples/task-api/tools_test.go
rename to _examples/task-api/dev_test.go
index f8eac66..60b759c 100644
--- a/_examples/task-api/tools_test.go
+++ b/_examples/task-api/dev_test.go
@@ -1,3 +1,3 @@
-package taskapi_test
+package main_test
 
 import _ "github.com/bool64/dev"
diff --git a/_examples/task-api/internal/infra/nethttp/integration_test.go b/_examples/task-api/internal/infra/nethttp/integration_test.go
index 9aa7a59..bc37f8a 100644
--- a/_examples/task-api/internal/infra/nethttp/integration_test.go
+++ b/_examples/task-api/internal/infra/nethttp/integration_test.go
@@ -95,4 +95,15 @@ func Test_taskLifeSpan(t *testing.T) {
 	assert.NoError(t, rc.ExpectOtherResponsesStatus(http.StatusBadRequest))
 	assert.NoError(t, rc.ExpectOtherResponsesBody([]byte(`{"status":"FAILED_PRECONDITION",`+
 		`"error":"failed precondition: task is already closed"}`)))
+
+	rc.Reset().WithMethod(http.MethodPost).WithURI("/user/tasks").
+		WithBody([]byte(`{"deadline": "2022-06-20T23:49:10.227Z","goal": "goal!"}`)).Concurrently()
+	assert.NoError(t, rc.ExpectResponseStatus(http.StatusUnauthorized))
+	assert.NoError(t, rc.ExpectResponseBody(nil))
+
+	rc.Reset().WithMethod(http.MethodPost).WithURI("/user/tasks").
+		WithHeader("Authorization", "Basic dXNlcjp1c2Vy"). // user:user.
+		WithBody([]byte(`{"deadline": "2022-06-20T23:49:10.227Z","goal": "goal!"}`)).Concurrently()
+	assert.NoError(t, rc.ExpectResponseStatus(http.StatusCreated))
+	assert.NoError(t, rc.ExpectResponseBody([]byte(`{"id":"<ignore-diff>","goal":"goal!","deadline":"2022-06-20T23:49:10.227Z","createdAt":"<ignore-diff>"}`)))
 }
diff --git a/openapi/collector_test.go b/openapi/collector_test.go
index db1dcf6..ae38929 100644
--- a/openapi/collector_test.go
+++ b/openapi/collector_test.go
@@ -145,41 +145,85 @@ func TestCollector_Collect_requestMapping(t *testing.T) {
 	collector := openapi.Collector{}
 
 	require.NoError(t, collector.Collect(http.MethodPost, "/test/{in-path}", u, h))
+	require.NoError(t, collector.Collect(http.MethodPut, "/test/{in-path}", u, h))
 
 	assertjson.EqualMarshal(t, []byte(`{
-  "openapi":"3.0.3","info":{"title":"","version":""},
-  "paths":{
-	"/test/{in-path}":{
-	  "post":{
-		"summary":"Title","description":"","operationId":"name",
-		"parameters":[
-		  {"name":"in_query","in":"query","schema":{"type":"string","format":"date"}},
-		  {"name":"in-path","in":"path","required":true,"schema":{"type":"boolean"}},
-		  {"name":"in_cookie","in":"cookie","schema":{"type":"string","format":"date-time"}},
-		  {"name":"X-In-Header","in":"header","schema":{"minLength":2,"type":"string"}}
-		],
-		"requestBody":{
-		  "content":{
-			"multipart/form-data":{"schema":{"$ref":"#/components/schemas/FormDataOpenapiTestInput"}}
+	  "openapi":"3.0.3","info":{"title":"","version":""},
+	  "paths":{
+		"/test/{in-path}":{
+		  "post":{
+			"summary":"Title","operationId":"name",
+			"parameters":[
+			  {
+				"name":"in_query","in":"query",
+				"schema":{"type":"string","format":"date"}
+			  },
+			  {
+				"name":"in-path","in":"path","required":true,
+				"schema":{"type":"boolean"}
+			  },
+			  {
+				"name":"in_cookie","in":"cookie",
+				"schema":{"type":"string","format":"date-time"}
+			  },
+			  {
+				"name":"X-In-Header","in":"header",
+				"schema":{"minLength":2,"type":"string"}
+			  }
+			],
+			"requestBody":{
+			  "content":{
+				"multipart/form-data":{
+				  "schema":{"$ref":"#/components/schemas/FormDataOpenapiTestInput"}
+				}
+			  }
+			},
+			"responses":{"204":{"description":"No Content"}},"deprecated":true
+		  },
+		  "put":{
+			"summary":"Title","operationId":"name2",
+			"parameters":[
+			  {
+				"name":"in_query","in":"query",
+				"schema":{"type":"string","format":"date"}
+			  },
+			  {
+				"name":"in-path","in":"path","required":true,
+				"schema":{"type":"boolean"}
+			  },
+			  {
+				"name":"in_cookie","in":"cookie",
+				"schema":{"type":"string","format":"date-time"}
+			  },
+			  {
+				"name":"X-In-Header","in":"header",
+				"schema":{"minLength":2,"type":"string"}
+			  }
+			],
+			"requestBody":{
+			  "content":{
+				"multipart/form-data":{
+				  "schema":{"$ref":"#/components/schemas/FormDataOpenapiTestInput"}
+				}
+			  }
+			},
+			"responses":{"204":{"description":"No Content"}},"deprecated":true
+		  }
+		}
+	  },
+	  "components":{
+		"schemas":{
+		  "FormDataMultipartFile":{"type":"string","format":"binary","nullable":true},
+		  "FormDataOpenapiTestInput":{
+			"type":"object",
+			"properties":{
+			  "in_form_data":{"type":"string","format":"date-time"},
+			  "upload":{"$ref":"#/components/schemas/FormDataMultipartFile"}
+			}
 		  }
-		},
-		"responses":{"204":{"description":"No Content"}},"deprecated":true
-	  }
-	}
-  },
-  "components":{
-	"schemas":{
-	  "FormDataMultipartFile":{"type":"string","format":"binary","nullable":true},
-	  "FormDataOpenapiTestInput":{
-		"type":"object",
-		"properties":{
-		  "in_form_data":{"type":"string","format":"date-time"},
-		  "upload":{"$ref":"#/components/schemas/FormDataMultipartFile"}
 		}
 	  }
-	}
-  }
-}`), collector.Reflector().SpecEns())
+	}`), collector.Reflector().SpecEns())
 
 	val := validatorMock{
 		AddSchemaFunc: func(in rest.ParamIn, name string, schemaData []byte, required bool) error {
@@ -228,7 +272,7 @@ func TestCollector_Collect_CombineErrors(t *testing.T) {
 	  "paths":{
 		"/test":{
 		  "post":{
-			"summary":"Title","description":"","operationId":"name",
+			"summary":"Title","operationId":"name",
 			"responses":{
 			  "204":{"description":"No Content"},
 			  "400":{
diff --git a/web/_testdata/openapi.json b/web/_testdata/openapi.json
index 0398d42..76033e8 100644
--- a/web/_testdata/openapi.json
+++ b/web/_testdata/openapi.json
@@ -8,8 +8,8 @@
   "paths":{
     "/albums":{
       "options":{
-        "tags":["Album"],"summary":"Post Albums","description":"",
-        "operationId":"rest/web_test.postAlbums",
+        "tags":["Album"],"summary":"Post Albums",
+        "operationId":"rest/web_test.postAlbums5",
         "parameters":[{"name":"locale","in":"query","schema":{"type":"string"}}],
         "requestBody":{
           "content":{
@@ -26,8 +26,8 @@
         }
       },
       "patch":{
-        "tags":["Album"],"summary":"Post Albums","description":"",
-        "operationId":"rest/web_test.postAlbums",
+        "tags":["Album"],"summary":"Post Albums",
+        "operationId":"rest/web_test.postAlbums2",
         "parameters":[{"name":"locale","in":"query","schema":{"type":"string"}}],
         "requestBody":{
           "content":{
@@ -44,7 +44,7 @@
         }
       },
       "post":{
-        "tags":["Album"],"summary":"Post Albums","description":"",
+        "tags":["Album"],"summary":"Post Albums",
         "operationId":"rest/web_test.postAlbums",
         "parameters":[{"name":"locale","in":"query","schema":{"type":"string"}}],
         "requestBody":{
@@ -62,8 +62,8 @@
         }
       },
       "put":{
-        "tags":["Album"],"summary":"Post Albums","description":"",
-        "operationId":"rest/web_test.postAlbums",
+        "tags":["Album"],"summary":"Post Albums",
+        "operationId":"rest/web_test.postAlbums3",
         "parameters":[{"name":"locale","in":"query","schema":{"type":"string"}}],
         "requestBody":{
           "content":{
@@ -80,8 +80,8 @@
         }
       },
       "trace":{
-        "tags":["Album"],"summary":"Post Albums","description":"",
-        "operationId":"rest/web_test.postAlbums",
+        "tags":["Album"],"summary":"Post Albums",
+        "operationId":"rest/web_test.postAlbums4",
         "parameters":[{"name":"locale","in":"query","schema":{"type":"string"}}],
         "responses":{
           "201":{
@@ -95,7 +95,7 @@
     },
     "/albums/{id}":{
       "delete":{
-        "tags":["Album"],"summary":"Album By ID","description":"",
+        "tags":["Album"],"summary":"Album By ID",
         "operationId":"rest/web_test.albumByID",
         "parameters":[
           {"name":"locale","in":"query","schema":{"type":"string"}},
@@ -113,8 +113,8 @@
         }
       },
       "get":{
-        "tags":["Album"],"summary":"Album By ID","description":"",
-        "operationId":"rest/web_test.albumByID",
+        "tags":["Album"],"summary":"Album By ID",
+        "operationId":"rest/web_test.albumByID3",
         "parameters":[
           {"name":"locale","in":"query","schema":{"type":"string"}},
           {
@@ -131,8 +131,8 @@
         }
       },
       "head":{
-        "tags":["Album"],"summary":"Album By ID","description":"",
-        "operationId":"rest/web_test.albumByID",
+        "tags":["Album"],"summary":"Album By ID",
+        "operationId":"rest/web_test.albumByID2",
         "parameters":[
           {"name":"locale","in":"query","schema":{"type":"string"}},
           {
diff --git a/web/example_test.go b/web/example_test.go
index 9fc65ec..b102801 100644
--- a/web/example_test.go
+++ b/web/example_test.go
@@ -5,6 +5,7 @@ import (
 	"log"
 	"net/http"
 
+	"github.com/go-chi/chi/v5/middleware"
 	"github.com/swaggest/rest/nethttp"
 	"github.com/swaggest/rest/web"
 	"github.com/swaggest/usecase"
@@ -31,12 +32,24 @@ func postAlbums() usecase.Interactor {
 }
 
 func ExampleDefaultService() {
+	// Service initializes router with required middlewares.
 	service := web.DefaultService()
 
+	// It allows OpenAPI configuration.
 	service.OpenAPI.Info.Title = "Albums API"
 	service.OpenAPI.Info.WithDescription("This service provides API to manage albums.")
 	service.OpenAPI.Info.Version = "v1.0.0"
 
+	// Additional middlewares can be added.
+	service.Use(
+		middleware.StripSlashes,
+
+		// cors.AllowAll().Handler, // "github.com/rs/cors", 3rd-party CORS middleware can also be configured here.
+	)
+
+	service.Wrap()
+
+	// Use cases can be mounted using short syntax .<Method>(...).
 	service.Post("/albums", postAlbums(), nethttp.SuccessStatus(http.StatusCreated))
 
 	log.Println("Starting service at http://localhost:8080")