Repository: swaggest/rest
Pull request #222 (2025-02-15 00:57:39)

Add contentType fields support
This PR adds support for decoding and encoding request/response body by a matching content type in a struct like
```go
type MyPort struct {
	TextBody string `contentType:"text/plain"`
	CSVBody  string `contentType:"text/csv"`
}
```

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

| Language  | Files |   Lines    |    Code    | Comments |   Blanks   | Complexity |     Bytes      |
|-----------|-------|------------|------------|----------|------------|------------|----------------|
| Go        | 114   | 9000 (+91) | 6501 (+69) | 673 (+2) | 1826 (+20) | 1072 (+19) | 219.8K (+2.1K) |
| Go (test) | 56    | 6937 (+58) | 5312 (+39) | 295      | 1330 (+19) | 196        | 189.2K (+1.4K) |
| Markdown  | 7     | 643 (+2)   | 486 (+2)   | 0        | 157        | 0          | 21.3K (+153B)  |
<!-- Sticky Pull Request CommentLOC -->
github-actions[bot]: ### Go API Changes

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

|          File           |          Function           | Coverage |
|-------------------------|-----------------------------|----------|
| Total                   |                             | 75.7%    |
| request/decoder.go      |                             | 75.0%    |
| request/decoder.go:194  | contentTypeBodyToURLValues  | 85.7%    |
| request/factory.go      |                             | 100.0%   |
| request/factory.go:57   | NewDecoderFactory           | 100.0%   |
| response/encoder.go     |                             | 66.7%    |
| response/encoder.go:233 | SetupOutput                 | 100.0%   |
| response/encoder.go:413 | WriteSuccessfulResponse     | 100.0%   |
| response/encoder.go:479 | writeHeader                 | 100.0%   |
| response/encoder.go:519 | writeRawResponse            | 68.6%    |
| response/encoder.go:124 | setupContentTypeBodyEncoder | 65.9%    |
| response/encoder.go:171 | setupCookiesEncoder         | 16.7%    |
| trait.go                |                             | 100.0%   |
| trait.go:62             | OutputHasNoContent          | 100.0%   |

</details>

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

|        File         |          Function           | Base Coverage | Current Coverage |
|---------------------|-----------------------------|---------------|------------------|
| Total               |                             | 80.8%         | 80.5% (-0.3%)    |
| request/decoder.go  | contentTypeBodyToURLValues  | no function   | 75.0%            |
| response/encoder.go | SetupOutput                 | 85.2%         | 85.7% (+0.5%)    |
| response/encoder.go | WriteSuccessfulResponse     | 54.2%         | 57.7% (+3.5%)    |
| response/encoder.go | setupContentTypeBodyEncoder | no function   | 68.0%            |
| response/encoder.go | whiteHeader                 | 80.0%         | no function      |
| response/encoder.go | writeHeader                 | no function   | 80.0%            |
| response/encoder.go | writeRawResponse            | no function   | 63.6%            |
| trait.go            | OutputHasNoContent          | 88.9%         | 89.5% (+0.6%)    |

</details>
<!-- Sticky Pull Request Commentunit-test -->
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-4    1.19µs ± 1%    1.11µs ± 0%  -6.43%  (p=0.008 n=5+5)
pkg:github.com/swaggest/rest/request goos:linux goarch:amd64
Decoder_Decode-4                           601ns ± 0%     605ns ± 0%  +0.68%  (p=0.016 n=5+4)
DecoderFunc_Decode-4                      1.72µs ± 1%    1.69µs ± 1%  -1.82%  (p=0.008 n=5+5)
Decoder_Decode_json-4                     18.7µs ± 1%    18.5µs ± 1%    ~     (p=0.056 n=5+5)
Decoder_Decode_queryObject-4              4.02µs ± 2%    4.02µs ± 1%    ~     (p=0.222 n=5+5)
Decoder_Decode_jsonParam-4                1.55µs ± 0%    1.52µs ± 1%  -2.02%  (p=0.008 n=5+5)
DecoderFactory_SetDecoderFunc-4           1.37µs ± 2%    1.36µs ± 0%    ~     (p=0.341 n=5+5)
pkg:github.com/swaggest/rest/response/gzip goos:linux goarch:amd64
Middleware-4                              11.2µs ± 0%    11.3µs ± 1%    ~     (p=0.151 n=5+5)
Middleware_control-4                      2.70µs ± 2%    2.71µs ± 1%    ~     (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-4    2.46kB ± 0%    2.46kB ± 0%    ~     (all equal)
pkg:github.com/swaggest/rest/request goos:linux goarch:amd64
Decoder_Decode-4                            440B ± 0%      440B ± 0%    ~     (all equal)
DecoderFunc_Decode-4                      1.53kB ± 0%    1.53kB ± 0%    ~     (all equal)
Decoder_Decode_json-4                     12.4kB ± 0%    12.4kB ± 0%    ~     (all equal)
Decoder_Decode_queryObject-4              2.01kB ± 0%    2.01kB ± 0%    ~     (all equal)
Decoder_Decode_jsonParam-4                  736B ± 0%      736B ± 0%    ~     (all equal)
DecoderFactory_SetDecoderFunc-4           1.02kB ± 0%    1.02kB ± 0%    ~     (all equal)
pkg:github.com/swaggest/rest/response/gzip goos:linux goarch:amd64
Middleware-4                              1.15kB ± 5%    1.16kB ± 3%    ~     (p=0.746 n=5+5)
Middleware_control-4                      11.1kB ± 0%    11.1kB ± 0%    ~     (all equal)

name                                    old allocs/op  new allocs/op  delta
pkg:github.com/swaggest/rest/jsonschema goos:linux goarch:amd64
RequestValidator_ValidateRequestData-4      8.00 ± 0%      8.00 ± 0%    ~     (all equal)
pkg:github.com/swaggest/rest/request goos:linux goarch:amd64
Decoder_Decode-4                            4.00 ± 0%      4.00 ± 0%    ~     (all equal)
DecoderFunc_Decode-4                        12.0 ± 0%      12.0 ± 0%    ~     (all equal)
Decoder_Decode_json-4                        177 ± 0%       177 ± 0%    ~     (all equal)
Decoder_Decode_queryObject-4                38.0 ± 0%      38.0 ± 0%    ~     (all equal)
Decoder_Decode_jsonParam-4                  12.0 ± 0%      12.0 ± 0%    ~     (all equal)
DecoderFactory_SetDecoderFunc-4             16.0 ± 0%      16.0 ± 0%    ~     (all equal)
pkg:github.com/swaggest/rest/response/gzip goos:linux goarch:amd64
Middleware-4                                11.0 ± 0%      11.0 ± 0%    ~     (all equal)
Middleware_control-4                        8.00 ± 0%      8.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-4  1.11µs ± 0%
pkg:github.com/swaggest/rest/request goos:linux goarch:amd64
Decoder_Decode-4                         605ns ± 0%
DecoderFunc_Decode-4                    1.69µs ± 1%
Decoder_Decode_json-4                   18.5µs ± 1%
Decoder_Decode_queryObject-4            4.02µs ± 1%
Decoder_Decode_jsonParam-4              1.52µs ± 1%
DecoderFactory_SetDecoderFunc-4         1.36µs ± 0%
pkg:github.com/swaggest/rest/response/gzip goos:linux goarch:amd64
Middleware-4                            11.3µs ± 1%
Middleware_control-4                    2.71µs ± 1%

name                                    alloc/op
pkg:github.com/swaggest/rest/jsonschema goos:linux goarch:amd64
RequestValidator_ValidateRequestData-4  2.46kB ± 0%
pkg:github.com/swaggest/rest/request goos:linux goarch:amd64
Decoder_Decode-4                          440B ± 0%
DecoderFunc_Decode-4                    1.53kB ± 0%
Decoder_Decode_json-4                   12.4kB ± 0%
Decoder_Decode_queryObject-4            2.01kB ± 0%
Decoder_Decode_jsonParam-4                736B ± 0%
DecoderFactory_SetDecoderFunc-4         1.02kB ± 0%
pkg:github.com/swaggest/rest/response/gzip goos:linux goarch:amd64
Middleware-4                            1.16kB ± 3%
Middleware_control-4                    11.1kB ± 0%

name                                    allocs/op
pkg:github.com/swaggest/rest/jsonschema goos:linux goarch:amd64
RequestValidator_ValidateRequestData-4    8.00 ± 0%
pkg:github.com/swaggest/rest/request goos:linux goarch:amd64
Decoder_Decode-4                          4.00 ± 0%
DecoderFunc_Decode-4                      12.0 ± 0%
Decoder_Decode_json-4                      177 ± 0%
Decoder_Decode_queryObject-4              38.0 ± 0%
Decoder_Decode_jsonParam-4                12.0 ± 0%
DecoderFactory_SetDecoderFunc-4           16.0 ± 0%
pkg:github.com/swaggest/rest/response/gzip goos:linux goarch:amd64
Middleware-4                              11.0 ± 0%
Middleware_control-4                      8.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-4                17.1µs ± 7%    16.7µs ± 1%     ~     (p=0.095 n=5+5)
_directGzipHead-4            16.7µs ± 1%    16.5µs ± 0%     ~     (p=0.111 n=5+4)
_noDirectGzip-4              95.4µs ± 2%    93.5µs ± 1%   -1.93%  (p=0.032 n=5+5)
_directGzip_decode-4          337µs ± 1%     338µs ± 5%     ~     (p=0.310 n=5+5)
_noDirectGzip_decode-4       94.4µs ± 1%    94.1µs ± 1%     ~     (p=0.548 n=5+5)
_jsonBody-4                  36.8µs ± 3%    35.8µs ± 2%   -2.75%  (p=0.008 n=5+5)
_jsonBodyValidation-4        42.0µs ± 1%    40.8µs ± 1%   -2.84%  (p=0.008 n=5+5)
_outputHeaders-4             17.0µs ± 1%    16.8µs ± 1%   -1.39%  (p=0.008 n=5+5)
_requestResponseMapping-4    34.8µs ± 2%    35.2µs ± 1%     ~     (p=0.095 n=5+5)
_validation-4                37.6µs ± 2%    37.7µs ± 2%     ~     (p=1.000 n=5+5)
_noValidation-4              26.9µs ± 1%    26.6µs ± 2%     ~     (p=0.151 n=5+5)
pkg:github.com/swaggest/rest/_examples/advanced-generic-openapi31 goos:linux goarch:amd64
_formOrJSON/form-4           20.4µs ± 6%    20.3µs ± 6%     ~     (p=0.460 n=5+5)
_formOrJSON/json-4           24.0µs ± 1%    23.4µs ± 0%   -2.30%  (p=0.008 n=5+5)
_directGzip-4                17.5µs ± 1%    17.2µs ± 1%   -1.33%  (p=0.032 n=5+5)
_directGzipHead-4            17.4µs ± 2%    17.6µs ± 4%     ~     (p=0.548 n=5+5)
_noDirectGzip-4              94.7µs ± 1%    94.4µs ± 0%     ~     (p=0.421 n=5+5)
_htmlResponse-4              30.5µs ± 0%    30.5µs ± 2%     ~     (p=0.548 n=5+5)
_jsonBodyManual-4            21.4µs ± 1%    21.2µs ± 1%     ~     (p=0.151 n=5+5)
_jsonBody-4                  32.0µs ± 2%    32.1µs ± 1%     ~     (p=0.548 n=5+5)
_jsonBodyValidation-4        40.2µs ± 1%    39.8µs ± 2%     ~     (p=0.548 n=5+5)
_outputHeaders-4             29.2µs ± 2%    29.2µs ± 2%     ~     (p=1.000 n=5+5)
_requestResponseMapping-4    33.7µs ± 2%    33.1µs ± 0%     ~     (p=0.421 n=5+5)
_validation-4                36.4µs ± 3%    36.0µs ± 2%     ~     (p=0.516 n=5+5)
_noValidation-4              26.1µs ± 2%    26.1µs ± 2%     ~     (p=1.000 n=5+5)
pkg:github.com/swaggest/rest/_examples/task-api/internal/infra/nethttp goos:linux goarch:amd64
_notFoundSrv-4               19.5µs ± 1%    19.5µs ± 0%     ~     (p=1.000 n=4+5)
_ok-4                        19.5µs ± 1%    19.6µs ± 1%     ~     (p=0.413 n=5+4)
_invalidBody-4               28.0µs ± 5%    28.1µs ± 5%     ~     (p=1.000 n=5+5)

name                       old 50%:ms     new 50%:ms     delta
pkg:github.com/swaggest/rest/_examples/advanced goos:linux goarch:amd64
_directGzip-4                  0.75 ± 5%      0.71 ± 6%     ~     (p=0.056 n=5+5)
_directGzipHead-4              0.73 ± 5%      0.73 ± 8%     ~     (p=0.690 n=5+5)
_noDirectGzip-4                4.38 ± 1%      4.29 ± 9%     ~     (p=0.690 n=5+5)
_directGzip_decode-4           12.0 ± 3%      10.7 ±12%     ~     (p=0.056 n=5+5)
_noDirectGzip_decode-4         4.30 ± 5%      4.31 ± 3%     ~     (p=1.000 n=5+5)
_jsonBody-4                    1.53 ± 5%      1.52 ± 5%     ~     (p=0.841 n=5+5)
_jsonBodyValidation-4          1.74 ± 4%      1.69 ± 4%     ~     (p=0.095 n=5+5)
_outputHeaders-4               0.73 ± 5%      0.72 ± 6%     ~     (p=0.841 n=5+5)
_requestResponseMapping-4      1.42 ± 4%      1.40 ± 4%     ~     (p=0.802 n=5+5)
_validation-4                  1.57 ± 8%      1.58 ± 5%     ~     (p=0.841 n=5+5)
_noValidation-4                1.11 ± 3%      1.10 ± 8%     ~     (p=0.548 n=5+5)
pkg:github.com/swaggest/rest/_examples/advanced-generic-openapi31 goos:linux goarch:amd64
_formOrJSON/form-4             0.85 ± 3%      0.87 ± 6%     ~     (p=0.310 n=5+5)
_formOrJSON/json-4             1.01 ± 3%      0.99 ±10%     ~     (p=0.310 n=5+5)
_directGzip-4                  0.76 ± 1%      0.76 ± 4%     ~     (p=0.690 n=5+5)
_directGzipHead-4              0.75 ± 6%      0.76 ± 6%     ~     (p=1.000 n=5+5)
_noDirectGzip-4                4.37 ± 3%      4.35 ± 9%     ~     (p=0.548 n=5+5)
_htmlResponse-4                1.26 ± 1%      1.33 ± 4%   +5.36%  (p=0.008 n=5+5)
_jsonBodyManual-4              0.95 ± 6%      0.90 ± 2%   -5.93%  (p=0.016 n=5+5)
_jsonBody-4                    1.33 ± 6%      1.32 ± 4%     ~     (p=1.000 n=5+5)
_jsonBodyValidation-4          1.71 ± 5%      1.63 ± 8%     ~     (p=0.222 n=5+5)
_outputHeaders-4               1.20 ± 6%      1.18 ± 4%     ~     (p=0.897 n=5+5)
_requestResponseMapping-4      1.35 ± 4%      1.38 ± 5%     ~     (p=0.421 n=5+5)
_validation-4                  1.56 ± 4%      1.49 ± 9%     ~     (p=0.095 n=5+5)
_noValidation-4                1.08 ± 4%      1.11 ± 8%     ~     (p=0.421 n=5+5)
pkg:github.com/swaggest/rest/_examples/task-api/internal/infra/nethttp goos:linux goarch:amd64
_notFoundSrv-4                 0.89 ± 8%      0.85 ± 2%     ~     (p=0.056 n=5+5)
_ok-4                          0.83 ± 3%      0.85 ± 7%     ~     (p=0.548 n=5+5)
_invalidBody-4                 1.18 ± 3%      1.18 ± 7%     ~     (p=0.937 n=5+5)

name                       old 90%:ms     new 90%:ms     delta
pkg:github.com/swaggest/rest/_examples/advanced goos:linux goarch:amd64
_directGzip-4                  1.54 ± 8%      1.52 ± 4%     ~     (p=0.421 n=5+5)
_directGzipHead-4              1.52 ± 7%      1.56 ± 6%     ~     (p=0.421 n=5+5)
_noDirectGzip-4                8.95 ± 5%      8.72 ± 5%     ~     (p=0.310 n=5+5)
_directGzip_decode-4           32.2 ± 4%      33.3 ± 9%     ~     (p=0.421 n=5+5)
_noDirectGzip_decode-4         8.81 ± 4%      8.81 ± 5%     ~     (p=1.000 n=5+5)
_jsonBody-4                    3.40 ± 6%      3.22 ± 6%     ~     (p=0.095 n=5+5)
_jsonBodyValidation-4          3.75 ± 2%      3.71 ± 2%     ~     (p=0.222 n=5+5)
_outputHeaders-4               1.51 ± 2%      1.50 ± 3%     ~     (p=0.548 n=5+5)
_requestResponseMapping-4      3.21 ± 6%      3.22 ± 4%     ~     (p=1.000 n=5+5)
_validation-4                  3.43 ± 2%      3.44 ± 4%     ~     (p=0.841 n=5+5)
_noValidation-4                2.52 ± 2%      2.48 ± 7%     ~     (p=0.548 n=5+5)
pkg:github.com/swaggest/rest/_examples/advanced-generic-openapi31 goos:linux goarch:amd64
_formOrJSON/form-4             1.87 ± 7%      1.86 ± 5%     ~     (p=0.841 n=5+5)
_formOrJSON/json-4             2.22 ± 2%      2.17 ± 5%     ~     (p=0.421 n=5+5)
_directGzip-4                  1.62 ± 5%      1.60 ± 1%     ~     (p=0.690 n=5+5)
_directGzipHead-4              1.61 ± 6%      1.60 ± 4%     ~     (p=0.841 n=5+5)
_noDirectGzip-4                8.82 ± 4%      8.86 ± 4%     ~     (p=0.897 n=5+5)
_htmlResponse-4                2.87 ± 5%      2.82 ± 6%     ~     (p=0.548 n=5+5)
_jsonBodyManual-4              1.94 ± 4%      1.88 ± 1%     ~     (p=0.222 n=5+5)
_jsonBody-4                    3.02 ± 4%      3.02 ± 4%     ~     (p=1.000 n=5+5)
_jsonBodyValidation-4          3.62 ± 2%      3.61 ± 3%     ~     (p=0.690 n=5+5)
_outputHeaders-4               2.78 ± 3%      2.70 ± 3%     ~     (p=0.056 n=5+5)
_requestResponseMapping-4      3.09 ± 0%      3.14 ± 4%     ~     (p=0.190 n=4+5)
_validation-4                  3.39 ± 3%      3.34 ± 5%     ~     (p=0.690 n=5+5)
_noValidation-4                2.46 ± 3%      2.44 ± 5%     ~     (p=0.841 n=5+5)
pkg:github.com/swaggest/rest/_examples/task-api/internal/infra/nethttp goos:linux goarch:amd64
_notFoundSrv-4                 1.79 ± 7%      1.76 ± 2%     ~     (p=0.690 n=5+5)
_ok-4                          1.79 ± 7%      1.80 ± 9%     ~     (p=0.690 n=5+5)
_invalidBody-4                 2.69 ± 5%      2.77 ± 6%     ~     (p=0.175 n=5+5)

name                       old 99%:ms     new 99%:ms     delta
pkg:github.com/swaggest/rest/_examples/advanced goos:linux goarch:amd64
_directGzip-4                  2.75 ±45%      2.41 ± 4%     ~     (p=0.548 n=5+5)
_directGzipHead-4              2.55 ± 5%      2.43 ± 3%   -4.56%  (p=0.032 n=5+4)
_noDirectGzip-4                14.2 ± 9%      13.4 ± 4%     ~     (p=0.095 n=5+5)
_directGzip_decode-4           58.1 ± 7%      64.6 ±18%     ~     (p=0.310 n=5+5)
_noDirectGzip_decode-4         13.8 ± 3%      13.5 ± 2%     ~     (p=0.690 n=5+5)
_jsonBody-4                    5.29 ± 9%      4.97 ± 2%   -6.10%  (p=0.008 n=5+5)
_jsonBodyValidation-4          6.23 ± 8%      5.86 ± 4%   -5.93%  (p=0.032 n=5+5)
_outputHeaders-4               2.61 ± 4%      2.63 ± 4%     ~     (p=0.587 n=5+5)
_requestResponseMapping-4      5.09 ± 5%      5.36 ± 8%     ~     (p=0.151 n=5+5)
_validation-4                  5.40 ± 4%      5.68 ± 8%     ~     (p=0.222 n=5+5)
_noValidation-4                4.07 ± 6%      4.16 ± 4%     ~     (p=0.310 n=5+5)
pkg:github.com/swaggest/rest/_examples/advanced-generic-openapi31 goos:linux goarch:amd64
_formOrJSON/form-4             3.15 ±27%      2.79 ± 2%  -11.38%  (p=0.016 n=5+4)
_formOrJSON/json-4             3.65 ± 1%      3.53 ± 3%   -3.32%  (p=0.032 n=5+5)
_directGzip-4                  2.75 ± 5%      2.60 ± 2%   -5.53%  (p=0.032 n=5+5)
_directGzipHead-4              2.57 ± 6%      2.65 ± 8%     ~     (p=0.310 n=5+5)
_noDirectGzip-4                13.7 ± 5%      13.3 ± 4%     ~     (p=0.143 n=5+5)
_htmlResponse-4                4.61 ± 2%      4.66 ± 4%     ~     (p=0.841 n=5+5)
_jsonBodyManual-4              3.32 ± 1%      3.27 ± 5%     ~     (p=0.278 n=5+5)
_jsonBody-4                    4.92 ± 7%      4.90 ± 6%     ~     (p=1.000 n=5+5)
_jsonBodyValidation-4          5.94 ± 1%      5.85 ± 7%     ~     (p=1.000 n=5+5)
_outputHeaders-4               4.62 ± 6%      4.40 ± 4%     ~     (p=0.087 n=5+5)
_requestResponseMapping-4      4.87 ± 3%      4.72 ± 6%     ~     (p=0.421 n=5+5)
_validation-4                  5.35 ± 5%      5.18 ± 7%     ~     (p=0.421 n=5+5)
_noValidation-4                4.17 ±14%      4.11 ± 4%     ~     (p=0.841 n=5+5)
pkg:github.com/swaggest/rest/_examples/task-api/internal/infra/nethttp goos:linux goarch:amd64
_notFoundSrv-4                 2.78 ± 3%      2.73 ± 3%     ~     (p=0.286 n=4+5)
_ok-4                          2.81 ± 6%      3.19 ±40%     ~     (p=0.421 n=5+5)
_invalidBody-4                 4.55 ± 9%      4.53 ± 5%     ~     (p=1.000 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-4                  4.37 ±56%      3.48 ± 8%     ~     (p=0.310 n=5+5)
_directGzipHead-4              4.04 ±19%      4.01 ±53%     ~     (p=0.310 n=5+5)
_noDirectGzip-4                21.0 ±25%      17.4 ±25%     ~     (p=0.056 n=5+5)
_directGzip_decode-4           82.8 ±16%      93.2 ±21%     ~     (p=0.421 n=5+5)
_noDirectGzip_decode-4         19.1 ±17%      18.3 ± 4%     ~     (p=0.730 n=5+5)
_jsonBody-4                    8.07 ±21%      7.29 ± 8%     ~     (p=0.222 n=5+5)
_jsonBodyValidation-4          8.90 ± 6%      8.17 ±10%     ~     (p=0.056 n=5+5)
_outputHeaders-4               4.26 ± 8%      4.56 ± 3%     ~     (p=0.095 n=5+5)
_requestResponseMapping-4      8.15 ±48%      7.81 ±15%     ~     (p=0.548 n=5+5)
_validation-4                  8.02 ±16%      8.55 ±15%     ~     (p=0.222 n=5+5)
_noValidation-4                5.70 ±21%      6.00 ± 9%     ~     (p=0.548 n=5+5)
pkg:github.com/swaggest/rest/_examples/advanced-generic-openapi31 goos:linux goarch:amd64
_formOrJSON/form-4             4.43 ±46%      4.64 ±46%     ~     (p=0.421 n=5+5)
_formOrJSON/json-4             4.97 ±10%      4.80 ± 1%     ~     (p=0.159 n=5+4)
_directGzip-4                  4.77 ± 7%      3.75 ± 3%  -21.51%  (p=0.008 n=5+5)
_directGzipHead-4              3.92 ±13%      4.12 ±19%     ~     (p=0.548 n=5+5)
_noDirectGzip-4                19.0 ±17%      18.0 ±10%     ~     (p=0.548 n=5+5)
_htmlResponse-4                6.50 ± 6%      6.73 ± 8%     ~     (p=0.310 n=5+5)
_jsonBodyManual-4              5.16 ±13%      5.08 ± 9%     ~     (p=0.952 n=5+5)
_jsonBody-4                    6.98 ±14%      6.78 ± 7%     ~     (p=1.000 n=5+5)
_jsonBodyValidation-4          8.50 ±17%      8.29 ±19%     ~     (p=0.841 n=5+5)
_outputHeaders-4               6.28 ±11%      6.24 ± 7%     ~     (p=0.841 n=5+5)
_requestResponseMapping-4      6.85 ±11%      6.63 ± 9%     ~     (p=0.548 n=5+5)
_validation-4                  7.49 ±10%      7.51 ±10%     ~     (p=0.841 n=5+5)
_noValidation-4                5.83 ±21%      5.88 ±14%     ~     (p=1.000 n=5+5)
pkg:github.com/swaggest/rest/_examples/task-api/internal/infra/nethttp goos:linux goarch:amd64
_notFoundSrv-4                 4.57 ±61%      4.15 ±18%     ~     (p=0.841 n=5+5)
_ok-4                          3.97 ±15%      4.83 ±52%     ~     (p=0.841 n=5+5)
_invalidBody-4                 6.28 ±13%      6.47 ± 9%     ~     (p=0.548 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-4                   630 ± 0%       630 ± 0%     ~     (all equal)
_directGzipHead-4               174 ± 0%       174 ± 0%     ~     (all equal)
_noDirectGzip-4               1.03k ± 0%     1.03k ± 0%     ~     (all equal)
_directGzip_decode-4            630 ± 0%       630 ± 0%     ~     (all equal)
_noDirectGzip_decode-4        1.03k ± 0%     1.03k ± 0%     ~     (all equal)
_jsonBody-4                     199 ± 0%       199 ± 0%     ~     (all equal)
_jsonBodyValidation-4           185 ± 0%       185 ± 0%     ~     (all equal)
_outputHeaders-4                146 ± 0%       146 ± 0%     ~     (all equal)
_requestResponseMapping-4      94.0 ± 0%      94.0 ± 0%     ~     (all equal)
_validation-4                   168 ± 0%       168 ± 0%     ~     (all equal)
_noValidation-4                 168 ± 0%       168 ± 0%     ~     (all equal)
pkg:github.com/swaggest/rest/_examples/advanced-generic-openapi31 goos:linux goarch:amd64
_formOrJSON/form-4              155 ± 0%       155 ± 0%     ~     (all equal)
_formOrJSON/json-4              156 ± 0%       156 ± 0%     ~     (all equal)
_directGzip-4                   638 ± 0%       638 ± 0%     ~     (all equal)
_directGzipHead-4               182 ± 0%       182 ± 0%     ~     (all equal)
_noDirectGzip-4               1.04k ± 0%     1.04k ± 0%     ~     (all equal)
_htmlResponse-4                 355 ± 0%       355 ± 0%     ~     (all equal)
_jsonBodyManual-4               207 ± 0%       207 ± 0%     ~     (all equal)
_jsonBody-4                     207 ± 0%       207 ± 0%     ~     (all equal)
_jsonBodyValidation-4           193 ± 0%       193 ± 0%     ~     (all equal)
_outputHeaders-4                214 ± 0%       214 ± 0%     ~     (all equal)
_requestResponseMapping-4       108 ± 0%       108 ± 0%     ~     (all equal)
_validation-4                   176 ± 0%       176 ± 0%     ~     (all equal)
_noValidation-4                 176 ± 0%       176 ± 0%     ~     (all equal)
pkg:github.com/swaggest/rest/_examples/task-api/internal/infra/nethttp goos:linux goarch:amd64
_notFoundSrv-4                  322 ± 0%       322 ± 0%     ~     (all equal)
_ok-4                           344 ± 0%       344 ± 0%     ~     (all equal)
_invalidBody-4                  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-4                   103 ± 0%       103 ± 0%     ~     (all equal)
_directGzipHead-4               104 ± 0%       104 ± 0%     ~     (all equal)
_noDirectGzip-4                 117 ± 0%       117 ± 0%     ~     (all equal)
_directGzip_decode-4            116 ± 0%       116 ± 0%     ~     (all equal)
_noDirectGzip_decode-4          130 ± 0%       130 ± 0%     ~     (all equal)
_jsonBody-4                     188 ± 0%       188 ± 0%     ~     (all equal)
_jsonBodyValidation-4           192 ± 0%       192 ± 0%     ~     (all equal)
_outputHeaders-4               77.0 ± 0%      77.0 ± 0%     ~     (all equal)
_requestResponseMapping-4       169 ± 0%       169 ± 0%     ~     (all equal)
_validation-4                   170 ± 0%       170 ± 0%     ~     (all equal)
_noValidation-4                 173 ± 0%       173 ± 0%     ~     (all equal)
pkg:github.com/swaggest/rest/_examples/advanced-generic-openapi31 goos:linux goarch:amd64
_formOrJSON/form-4              170 ± 0%       170 ± 0%     ~     (all equal)
_formOrJSON/json-4              162 ± 0%       162 ± 0%     ~     (all equal)
_directGzip-4                   103 ± 0%       103 ± 0%     ~     (all equal)
_directGzipHead-4               104 ± 0%       104 ± 0%     ~     (all equal)
_noDirectGzip-4                 117 ± 0%       117 ± 0%     ~     (all equal)
_htmlResponse-4                 108 ± 0%       108 ± 0%     ~     (all equal)
_jsonBodyManual-4               195 ± 0%       195 ± 0%     ~     (all equal)
_jsonBody-4                     188 ± 0%       188 ± 0%     ~     (all equal)
_jsonBodyValidation-4           192 ± 0%       192 ± 0%     ~     (all equal)
_outputHeaders-4               88.0 ± 0%      88.0 ± 0%     ~     (all equal)
_requestResponseMapping-4       169 ± 0%       169 ± 0%     ~     (all equal)
_validation-4                   170 ± 0%       170 ± 0%     ~     (all equal)
_noValidation-4                 173 ± 0%       173 ± 0%     ~     (all equal)
pkg:github.com/swaggest/rest/_examples/task-api/internal/infra/nethttp goos:linux goarch:amd64
_notFoundSrv-4                 74.0 ± 0%      74.0 ± 0%     ~     (all equal)
_ok-4                          74.0 ± 0%      74.0 ± 0%     ~     (all equal)
_invalidBody-4                  137 ± 0%       137 ± 0%     ~     (all equal)

name                       old rps        new rps        delta
pkg:github.com/swaggest/rest/_examples/advanced goos:linux goarch:amd64
_directGzip-4                 58.4k ± 6%     59.9k ± 1%     ~     (p=0.095 n=5+5)
_directGzipHead-4             59.8k ± 1%     60.6k ± 0%     ~     (p=0.111 n=5+4)
_noDirectGzip-4               10.5k ± 2%     10.7k ± 1%   +1.96%  (p=0.032 n=5+5)
_directGzip_decode-4          2.97k ± 1%     2.96k ± 5%     ~     (p=0.310 n=5+5)
_noDirectGzip_decode-4        10.6k ± 1%     10.6k ± 1%     ~     (p=0.548 n=5+5)
_jsonBody-4                   27.2k ± 3%     28.0k ± 2%   +2.81%  (p=0.008 n=5+5)
_jsonBodyValidation-4         23.8k ± 1%     24.5k ± 1%   +2.93%  (p=0.008 n=5+5)
_outputHeaders-4              58.7k ± 1%     59.5k ± 1%   +1.41%  (p=0.008 n=5+5)
_requestResponseMapping-4     28.7k ± 2%     28.4k ± 1%     ~     (p=0.095 n=5+5)
_validation-4                 26.6k ± 2%     26.6k ± 2%     ~     (p=1.000 n=5+5)
_noValidation-4               37.1k ± 1%     37.6k ± 2%     ~     (p=0.151 n=5+5)
pkg:github.com/swaggest/rest/_examples/advanced-generic-openapi31 goos:linux goarch:amd64
_formOrJSON/form-4            49.0k ± 6%     49.2k ± 6%     ~     (p=0.548 n=5+5)
_formOrJSON/json-4            41.7k ± 1%     42.7k ± 0%   +2.36%  (p=0.008 n=5+5)
_directGzip-4                 57.2k ± 1%     58.0k ± 1%   +1.35%  (p=0.032 n=5+5)
_directGzipHead-4             57.6k ± 2%     56.7k ± 4%     ~     (p=0.548 n=5+5)
_noDirectGzip-4               10.6k ± 1%     10.6k ± 0%     ~     (p=0.421 n=5+5)
_htmlResponse-4               32.8k ± 0%     32.8k ± 2%     ~     (p=0.548 n=5+5)
_jsonBodyManual-4             46.7k ± 1%     47.1k ± 1%     ~     (p=0.151 n=5+5)
_jsonBody-4                   31.2k ± 2%     31.1k ± 1%     ~     (p=0.548 n=5+5)
_jsonBodyValidation-4         24.8k ± 1%     25.1k ± 2%     ~     (p=0.548 n=5+5)
_outputHeaders-4              34.2k ± 2%     34.3k ± 2%     ~     (p=1.000 n=5+5)
_requestResponseMapping-4     29.7k ± 2%     30.2k ± 0%     ~     (p=0.421 n=5+5)
_validation-4                 27.5k ± 3%     27.8k ± 2%     ~     (p=0.548 n=5+5)
_noValidation-4               38.3k ± 2%     38.4k ± 2%     ~     (p=1.000 n=5+5)
pkg:github.com/swaggest/rest/_examples/task-api/internal/infra/nethttp goos:linux goarch:amd64
_notFoundSrv-4                51.3k ± 1%     51.2k ± 0%     ~     (p=1.000 n=4+5)
_ok-4                         51.2k ± 1%     49.9k ± 8%     ~     (p=0.222 n=5+5)
_invalidBody-4                35.7k ± 5%     35.7k ± 5%     ~     (p=1.000 n=5+5)

name                       old alloc/op   new alloc/op   delta
pkg:github.com/swaggest/rest/_examples/advanced goos:linux goarch:amd64
_directGzip-4                3.99kB ± 0%    4.04kB ± 0%   +1.22%  (p=0.008 n=5+5)
_directGzipHead-4            3.99kB ± 0%    4.04kB ± 0%   +1.21%  (p=0.008 n=5+5)
_noDirectGzip-4              7.50kB ±25%    6.99kB ±18%     ~     (p=0.690 n=5+5)
_directGzip_decode-4          399kB ± 0%     399kB ± 0%     ~     (p=0.841 n=5+5)
_noDirectGzip_decode-4       6.64kB ± 7%    6.99kB ± 7%     ~     (p=0.310 n=5+5)
_jsonBody-4                  13.7kB ± 0%    13.6kB ± 0%     ~     (p=0.151 n=5+5)
_jsonBodyValidation-4        19.5kB ± 0%    19.5kB ± 0%     ~     (p=0.984 n=5+5)
_outputHeaders-4             3.71kB ± 0%    3.71kB ± 0%     ~     (p=0.651 n=5+5)
_requestResponseMapping-4    16.8kB ± 0%    16.8kB ± 0%     ~     (p=0.063 n=5+5)
_validation-4                16.7kB ± 0%    16.7kB ± 0%     ~     (p=0.508 n=5+5)
_noValidation-4              7.92kB ± 0%    7.92kB ± 0%     ~     (p=0.706 n=5+5)
pkg:github.com/swaggest/rest/_examples/advanced-generic-openapi31 goos:linux goarch:amd64
_formOrJSON/form-4           6.25kB ± 0%    6.25kB ± 0%     ~     (p=0.889 n=5+5)
_formOrJSON/json-4           6.64kB ± 0%    6.64kB ± 0%     ~     (p=0.087 n=5+5)
_directGzip-4                4.01kB ± 0%    4.06kB ± 0%   +1.20%  (p=0.029 n=4+4)
_directGzipHead-4            4.00kB ± 0%    4.05kB ± 0%   +1.22%  (p=0.008 n=5+5)
_noDirectGzip-4              7.76kB ±13%    7.96kB ±19%     ~     (p=1.000 n=5+5)
_htmlResponse-4              8.54kB ± 0%    8.54kB ± 0%     ~     (p=0.389 n=5+5)
_jsonBodyManual-4            4.96kB ± 0%    4.96kB ± 0%     ~     (p=1.000 n=5+5)
_jsonBody-4                  10.7kB ± 0%    10.7kB ± 0%     ~     (p=0.452 n=5+5)
_jsonBodyValidation-4        19.5kB ± 0%    19.5kB ± 0%     ~     (p=0.595 n=5+5)
_outputHeaders-4             10.5kB ± 0%    10.5kB ± 0%     ~     (p=0.651 n=5+5)
_requestResponseMapping-4    16.8kB ± 0%    16.8kB ± 0%     ~     (p=0.452 n=5+5)
_validation-4                16.7kB ± 0%    16.7kB ± 0%     ~     (p=0.984 n=5+5)
_noValidation-4              7.95kB ± 0%    7.95kB ± 0%     ~     (p=0.817 n=5+5)
pkg:github.com/swaggest/rest/_examples/task-api/internal/infra/nethttp goos:linux goarch:amd64
_notFoundSrv-4               5.24kB ± 0%    5.24kB ± 0%     ~     (p=0.984 n=5+5)
_ok-4                        5.15kB ± 0%    5.15kB ± 0%     ~     (p=0.886 n=4+4)
_invalidBody-4               8.83kB ± 0%    8.82kB ± 0%     ~     (p=0.886 n=4+4)

name                       old allocs/op  new allocs/op  delta
pkg:github.com/swaggest/rest/_examples/advanced goos:linux goarch:amd64
_directGzip-4                  42.0 ± 0%      43.0 ± 0%   +2.38%  (p=0.008 n=5+5)
_directGzipHead-4              42.0 ± 0%      43.0 ± 0%   +2.38%  (p=0.008 n=5+5)
_noDirectGzip-4                49.2 ± 4%      50.2 ± 4%     ~     (p=0.151 n=5+5)
_directGzip_decode-4            486 ± 0%       487 ± 0%     ~     (p=0.365 n=5+5)
_noDirectGzip_decode-4         50.0 ± 0%      51.0 ± 0%   +2.00%  (p=0.008 n=5+5)
_jsonBody-4                     130 ± 0%       130 ± 0%     ~     (p=0.444 n=5+5)
_jsonBodyValidation-4           187 ± 0%       187 ± 0%     ~     (all equal)
_outputHeaders-4               36.0 ± 0%      36.0 ± 0%     ~     (all equal)
_requestResponseMapping-4       124 ± 0%       124 ± 0%     ~     (all equal)
_validation-4                   155 ± 0%       155 ± 0%     ~     (all equal)
_noValidation-4                91.0 ± 0%      91.0 ± 0%     ~     (all equal)
pkg:github.com/swaggest/rest/_examples/advanced-generic-openapi31 goos:linux goarch:amd64
_formOrJSON/form-4             60.0 ± 0%      60.0 ± 0%     ~     (all equal)
_formOrJSON/json-4             66.0 ± 0%      66.0 ± 0%     ~     (all equal)
_directGzip-4                  43.0 ± 0%      44.0 ± 0%   +2.33%  (p=0.029 n=4+4)
_directGzipHead-4              43.0 ± 0%      44.0 ± 0%   +2.33%  (p=0.008 n=5+5)
_noDirectGzip-4                50.2 ± 4%      51.2 ± 4%     ~     (p=0.151 n=5+5)
_htmlResponse-4                 146 ± 0%       146 ± 0%     ~     (all equal)
_jsonBodyManual-4              49.0 ± 0%      49.0 ± 0%     ~     (all equal)
_jsonBody-4                     100 ± 0%       100 ± 0%     ~     (all equal)
_jsonBodyValidation-4           188 ± 0%       188 ± 0%     ~     (all equal)
_outputHeaders-4                112 ± 0%       112 ± 0%     ~     (all equal)
_requestResponseMapping-4       125 ± 0%       125 ± 0%     ~     (all equal)
_validation-4                   155 ± 0%       155 ± 0%     ~     (all equal)
_noValidation-4                92.0 ± 0%      92.0 ± 0%     ~     (all equal)
pkg:github.com/swaggest/rest/_examples/task-api/internal/infra/nethttp goos:linux goarch:amd64
_notFoundSrv-4                 54.0 ± 0%      54.0 ± 0%     ~     (all equal)
_ok-4                          50.0 ± 0%      50.0 ± 0%     ~     (all equal)
_invalidBody-4                 97.0 ± 0%      97.0 ± 0%     ~     (all equal)
```
</details>
<!-- Sticky Pull Request Commentbench-examples -->

Patch:
diff --git a/README.md b/README.md
index 571187a..a666b86 100644
--- a/README.md
+++ b/README.md
@@ -70,7 +70,8 @@ Input data can be located in:
 * `form` parameter acts as `formData` or `query`,
 * `json` parameter in request body with `application/json` content,
 * `cookie` parameter in request cookie,
-* `header` parameter in request header.
+* `header` parameter in request header,
+* `contentType` of matching raw request body.
 
 For more explicit separation of concerns between use case and transport it is possible to provide request mapping 
 separately when initializing handler (please note, such mapping is [not applied](https://github.com/swaggest/rest/issues/61#issuecomment-1059851553) to `json` body).
@@ -152,7 +153,8 @@ type helloOutput struct {
 Output data can be located in:
 * `json` for response body with `application/json` content,
 * `header` for values in response header,
-* `cookie` for cookie values, cookie fields can have configuration in field tag (same as in actual cookie, but with comma separation).
+* `cookie` for cookie values, cookie fields can have configuration in field tag (same as in actual cookie, but with comma separation),
+* `contentType` for a non-empty string value that should be used as response body with given content type.
 
 For more explicit separation of concerns between use case and transport it is possible to provide response header mapping 
 separately when initializing handler.
diff --git a/_examples/go.mod b/_examples/go.mod
index cb1e82b..a851083 100644
--- a/_examples/go.mod
+++ b/_examples/go.mod
@@ -10,7 +10,7 @@ require (
 	github.com/bool64/httpmock v0.1.15
 	github.com/bool64/httptestbench v0.1.4
 	github.com/gin-gonic/gin v1.10.0
-	github.com/go-chi/chi/v5 v5.2.0
+	github.com/go-chi/chi/v5 v5.2.1
 	github.com/go-chi/jwtauth/v5 v5.3.1
 	github.com/google/uuid v1.6.0
 	github.com/kelseyhightower/envconfig v1.4.0
@@ -18,7 +18,7 @@ require (
 	github.com/stretchr/testify v1.9.0
 	github.com/swaggest/assertjson v1.9.0
 	github.com/swaggest/jsonschema-go v0.3.73
-	github.com/swaggest/openapi-go v0.2.54
+	github.com/swaggest/openapi-go v0.2.55
 	github.com/swaggest/rest v0.0.0-00010101000000-000000000000
 	github.com/swaggest/swgui v1.8.2
 	github.com/swaggest/usecase v1.3.1
diff --git a/_examples/go.sum b/_examples/go.sum
index e22a915..1bd04dc 100644
--- a/_examples/go.sum
+++ b/_examples/go.sum
@@ -36,8 +36,8 @@ github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE
 github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI=
 github.com/gin-gonic/gin v1.10.0 h1:nTuyha1TYqgedzytsKYqna+DfLos46nTv2ygFy86HFU=
 github.com/gin-gonic/gin v1.10.0/go.mod h1:4PMNQiOhvDRa013RKVbsiNwoyezlm2rm0uX/T7kzp5Y=
-github.com/go-chi/chi/v5 v5.2.0 h1:Aj1EtB0qR2Rdo2dG4O94RIU35w2lvQSj6BRA4+qwFL0=
-github.com/go-chi/chi/v5 v5.2.0/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8=
+github.com/go-chi/chi/v5 v5.2.1 h1:KOIHODQj58PmL80G2Eak4WdvUzjSJSm0vG72crDCqb8=
+github.com/go-chi/chi/v5 v5.2.1/go.mod h1:L2yAIGWB3H+phAw1NxKwWM+7eUH/lU8pOMm5hHcoops=
 github.com/go-chi/jwtauth/v5 v5.3.1 h1:1ePWrjVctvp1tyBq5b/2ER8Th/+RbYc7x4qNsc5rh5A=
 github.com/go-chi/jwtauth/v5 v5.3.1/go.mod h1:6Fl2RRmWXs3tJYE1IQGX81FsPoGqDwq9c15j52R5q80=
 github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=
@@ -131,8 +131,8 @@ github.com/swaggest/form/v5 v5.1.1 h1:ct6/rOQBGrqWUQ0FUv3vW5sHvTUb31AwTUWj947N6c
 github.com/swaggest/form/v5 v5.1.1/go.mod h1:X1hraaoONee20PMnGNLQpO32f9zbQ0Czfm7iZThuEKg=
 github.com/swaggest/jsonschema-go v0.3.73 h1:gU1pBzF3pkZ1GDD3dRMdQoCjrA0sldJ+QcM7aSSPgvc=
 github.com/swaggest/jsonschema-go v0.3.73/go.mod h1:qp+Ym2DIXHlHzch3HKz50gPf2wJhKOrAB/VYqLS2oJU=
-github.com/swaggest/openapi-go v0.2.54 h1:WnFKIHAgR2RIOiYys3qvSuYmsFd2a17MIoC9Tcvog5c=
-github.com/swaggest/openapi-go v0.2.54/go.mod h1:2Q7NpuG9NgpGeTaNOo852GSR6cCzSP4IznA9DNdUTQw=
+github.com/swaggest/openapi-go v0.2.55 h1:PI9r7E8l0iKHriqQ6QxLbTjkZyUIHA8rlpzatYZff3c=
+github.com/swaggest/openapi-go v0.2.55/go.mod h1:sTmhR2sTvauaRX45WdaqRyJVTfRQ5FxD4OPVym49BXM=
 github.com/swaggest/refl v1.3.0 h1:PEUWIku+ZznYfsoyheF97ypSduvMApYyGkYF3nabS0I=
 github.com/swaggest/refl v1.3.0/go.mod h1:3Ujvbmh1pfSbDYjC6JGG7nMgPvpG0ehQL4iNonnLNbg=
 github.com/swaggest/swgui v1.8.2 h1:JGpRCLGLZ7EqTwHsBEOo//kx8CM7Rv3RchgvfNpB+6E=
diff --git a/go.mod b/go.mod
index 425315f..a0de625 100644
--- a/go.mod
+++ b/go.mod
@@ -7,14 +7,14 @@ require (
 	github.com/bool64/httpmock v0.1.15
 	github.com/bool64/shared v0.1.5
 	github.com/cespare/xxhash/v2 v2.3.0
-	github.com/go-chi/chi/v5 v5.2.0
+	github.com/go-chi/chi/v5 v5.2.1
 	github.com/gorilla/mux v1.8.1
 	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.73
-	github.com/swaggest/openapi-go v0.2.54
+	github.com/swaggest/openapi-go v0.2.55
 	github.com/swaggest/refl v1.3.0
 	github.com/swaggest/usecase v1.3.1
 )
diff --git a/go.sum b/go.sum
index 737ee4e..505d445 100644
--- a/go.sum
+++ b/go.sum
@@ -3,7 +3,6 @@ github.com/bool64/dev v0.2.25/go.mod h1:iJbh1y/HkunEPhgebWRNcs8wfGq7sjvJ6W5iabL8
 github.com/bool64/dev v0.2.29/go.mod h1:iJbh1y/HkunEPhgebWRNcs8wfGq7sjvJ6W5iabL8ACg=
 github.com/bool64/dev v0.2.31/go.mod h1:iJbh1y/HkunEPhgebWRNcs8wfGq7sjvJ6W5iabL8ACg=
 github.com/bool64/dev v0.2.32/go.mod h1:iJbh1y/HkunEPhgebWRNcs8wfGq7sjvJ6W5iabL8ACg=
-github.com/bool64/dev v0.2.35/go.mod h1:iJbh1y/HkunEPhgebWRNcs8wfGq7sjvJ6W5iabL8ACg=
 github.com/bool64/dev v0.2.36/go.mod h1:iJbh1y/HkunEPhgebWRNcs8wfGq7sjvJ6W5iabL8ACg=
 github.com/bool64/dev v0.2.38 h1:C5H9wkx/BhTYRfV14X90iIQKpSuhzsG+OHQvWdQ5YQ4=
 github.com/bool64/dev v0.2.38/go.mod h1:iJbh1y/HkunEPhgebWRNcs8wfGq7sjvJ6W5iabL8ACg=
@@ -19,8 +18,8 @@ github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSs
 github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
 github.com/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWoS4=
 github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ=
-github.com/go-chi/chi/v5 v5.2.0 h1:Aj1EtB0qR2Rdo2dG4O94RIU35w2lvQSj6BRA4+qwFL0=
-github.com/go-chi/chi/v5 v5.2.0/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8=
+github.com/go-chi/chi/v5 v5.2.1 h1:KOIHODQj58PmL80G2Eak4WdvUzjSJSm0vG72crDCqb8=
+github.com/go-chi/chi/v5 v5.2.1/go.mod h1:L2yAIGWB3H+phAw1NxKwWM+7eUH/lU8pOMm5hHcoops=
 github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
 github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8=
 github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA=
@@ -77,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.72/go.mod h1:OrGyEoVqpfSFJ4Am4V/FQcQ3mlEC1vVeleA+5ggbVW4=
 github.com/swaggest/jsonschema-go v0.3.73 h1:gU1pBzF3pkZ1GDD3dRMdQoCjrA0sldJ+QcM7aSSPgvc=
 github.com/swaggest/jsonschema-go v0.3.73/go.mod h1:qp+Ym2DIXHlHzch3HKz50gPf2wJhKOrAB/VYqLS2oJU=
-github.com/swaggest/openapi-go v0.2.54 h1:WnFKIHAgR2RIOiYys3qvSuYmsFd2a17MIoC9Tcvog5c=
-github.com/swaggest/openapi-go v0.2.54/go.mod h1:2Q7NpuG9NgpGeTaNOo852GSR6cCzSP4IznA9DNdUTQw=
+github.com/swaggest/openapi-go v0.2.55 h1:PI9r7E8l0iKHriqQ6QxLbTjkZyUIHA8rlpzatYZff3c=
+github.com/swaggest/openapi-go v0.2.55/go.mod h1:sTmhR2sTvauaRX45WdaqRyJVTfRQ5FxD4OPVym49BXM=
 github.com/swaggest/refl v1.3.0 h1:PEUWIku+ZznYfsoyheF97ypSduvMApYyGkYF3nabS0I=
 github.com/swaggest/refl v1.3.0/go.mod h1:3Ujvbmh1pfSbDYjC6JGG7nMgPvpG0ehQL4iNonnLNbg=
 github.com/swaggest/usecase v1.3.1 h1:JdKV30MTSsDxAXxkldLNcEn8O2uf565khyo6gr5sS+w=
diff --git a/request/decoder.go b/request/decoder.go
index 1389c05..eb1e316 100644
--- a/request/decoder.go
+++ b/request/decoder.go
@@ -1,6 +1,7 @@
 package request
 
 import (
+	"io/ioutil"
 	"net/http"
 	"net/url"
 	"strings"
@@ -189,3 +190,14 @@ func cookiesToURLValues(r *http.Request) (url.Values, error) {
 
 	return params, nil
 }
+
+func contentTypeBodyToURLValues(r *http.Request) (url.Values, error) {
+	b, err := ioutil.ReadAll(r.Body)
+	if err != nil {
+		return nil, err
+	}
+
+	return url.Values{
+		r.Header.Get("Content-Type"): []string{string(b)},
+	}, nil
+}
diff --git a/request/factory.go b/request/factory.go
index 5a4e785..4702230 100644
--- a/request/factory.go
+++ b/request/factory.go
@@ -61,6 +61,7 @@ func NewDecoderFactory() *DecoderFactory {
 	df.SetDecoderFunc(rest.ParamInHeader, headerToURLValues)
 	df.SetDecoderFunc(rest.ParamInQuery, queryToURLValues)
 	df.SetDecoderFunc("form", formToURLValues)
+	df.SetDecoderFunc("contentType", contentTypeBodyToURLValues)
 
 	defaultValDecoder := form.NewDecoder()
 	defaultValDecoder.SetNamespacePrefix("[")
diff --git a/response/encoder.go b/response/encoder.go
index fc6299d..926376d 100644
--- a/response/encoder.go
+++ b/response/encoder.go
@@ -30,13 +30,14 @@ type (
 type Encoder struct {
 	JSONWriter func(v interface{})
 
-	outputBufferType     reflect.Type
-	outputHeadersEncoder *form.Encoder
-	outputCookiesEncoder *form.Encoder
-	outputCookieBase     []http.Cookie
-	skipRendering        bool
-	outputWithWriter     bool
-	unwrapInterface      bool
+	outputBufferType             reflect.Type
+	outputHeadersEncoder         *form.Encoder
+	outputCookiesEncoder         *form.Encoder
+	outputContentTypeBodyEncoder *form.Encoder
+	outputCookieBase             []http.Cookie
+	skipRendering                bool
+	outputWithWriter             bool
+	unwrapInterface              bool
 
 	dynamicWithHeadersSetup bool
 	dynamicSetter           bool
@@ -120,12 +121,59 @@ func (h *Encoder) setupHeadersEncoder(output interface{}, ht *rest.HandlerTrait)
 	}
 }
 
+func (h *Encoder) setupContentTypeBodyEncoder(output interface{}) {
+	tagName := "contentType"
+
+	// Enable dynamic  check in interface mode.
+	if h.unwrapInterface {
+		enc := form.NewEncoder()
+		enc.SetMode(form.ModeExplicit)
+		enc.SetTagName(tagName)
+
+		h.outputContentTypeBodyEncoder = enc
+
+		return
+	}
+
+	var mapping map[string]string
+	if refl.HasTaggedFields(output, tagName) {
+		mapping = make(map[string]string)
+
+		refl.WalkTaggedFields(reflect.ValueOf(output), func(_ reflect.Value, sf reflect.StructField, _ string) {
+			// Converting name to canonical form, while keeping omitempty and any other options.
+			t := sf.Tag.Get(tagName)
+			parts := strings.Split(t, ",")
+			t = strings.Join(parts, ",")
+
+			mapping[sf.Name] = t
+		}, tagName)
+	}
+
+	if len(mapping) > 0 {
+		enc := form.NewEncoder()
+		enc.SetMode(form.ModeExplicit)
+		enc.RegisterTagNameFunc(func(field reflect.StructField) string {
+			if name, ok := mapping[field.Name]; ok {
+				return name
+			}
+
+			if field.Anonymous {
+				return ""
+			}
+
+			return "-"
+		})
+
+		h.outputContentTypeBodyEncoder = enc
+	}
+}
+
 func (h *Encoder) setupCookiesEncoder(output interface{}, ht *rest.HandlerTrait) {
-	// Enable dynamic headers check in interface mode.
+	// Enable dynamic check in interface mode.
 	if h.unwrapInterface {
 		enc := form.NewEncoder()
 		enc.SetMode(form.ModeExplicit)
-		enc.SetTagName(string(rest.ParamInCookie))
+		enc.SetTagName("contentType")
 
 		h.outputCookiesEncoder = enc
 
@@ -213,6 +261,7 @@ func (h *Encoder) SetupOutput(output interface{}, ht *rest.HandlerTrait) {
 
 	h.setupHeadersEncoder(output, ht)
 	h.setupCookiesEncoder(output, ht)
+	h.setupContentTypeBodyEncoder(output)
 
 	if h.outputBufferType.Kind() == reflect.Ptr {
 		h.outputBufferType = h.outputBufferType.Elem()
@@ -380,7 +429,7 @@ func (h *Encoder) WriteSuccessfulResponse(
 		}
 	}
 
-	if !h.whiteHeader(w, r, output, ht) {
+	if !h.writeHeader(w, r, output, ht) {
 		return
 	}
 
@@ -388,6 +437,10 @@ func (h *Encoder) WriteSuccessfulResponse(
 		return
 	}
 
+	if h.outputContentTypeBodyEncoder != nil && h.writeRawResponse(w, r, output, ht) {
+		return
+	}
+
 	skipRendering := h.skipRendering
 	if !skipRendering && h.dynamicNoContent {
 		if nc, ok := output.(noContent); ok {
@@ -423,7 +476,7 @@ func (h *Encoder) writeError(err error, w http.ResponseWriter, r *http.Request,
 	}
 }
 
-func (h *Encoder) whiteHeader(w http.ResponseWriter, r *http.Request, output interface{}, ht rest.HandlerTrait) bool {
+func (h *Encoder) writeHeader(w http.ResponseWriter, r *http.Request, output interface{}, ht rest.HandlerTrait) bool {
 	if h.dynamicWithHeadersSetup {
 		if sh, ok := output.(outputWithHeadersSetup); ok {
 			sh.SetupResponseHeader(w.Header())
@@ -463,6 +516,29 @@ func (h *Encoder) whiteHeader(w http.ResponseWriter, r *http.Request, output int
 	return true
 }
 
+func (h *Encoder) writeRawResponse(w http.ResponseWriter, r *http.Request, output interface{}, ht rest.HandlerTrait) bool {
+	values, err := h.outputContentTypeBodyEncoder.Encode(output)
+	if err != nil {
+		h.writeError(err, w, r, ht)
+
+		return true
+	}
+
+	for k, v := range values {
+		if len(v) == 1 && v[0] != "" {
+			w.Header().Set("Content-Type", k)
+
+			if _, err := w.Write([]byte(v[0])); err != nil {
+				h.writeError(err, w, r, ht)
+			}
+
+			return true
+		}
+	}
+
+	return false
+}
+
 func (h *Encoder) writeCookies(w http.ResponseWriter, r *http.Request, output interface{}, ht rest.HandlerTrait) bool {
 	if h.outputCookiesEncoder == nil {
 		return true
diff --git a/trait.go b/trait.go
index 33c28ca..27f95ab 100644
--- a/trait.go
+++ b/trait.go
@@ -76,7 +76,8 @@ func OutputHasNoContent(output interface{}) bool {
 		elemKind = rv.Elem().Kind()
 	}
 
-	hasTaggedFields := refl.HasTaggedFields(output, "json")
+	hasJSONTaggedFields := refl.HasTaggedFields(output, "json")
+	hasContentTypeTaggedFields := refl.HasTaggedFields(output, "contentType")
 	isSliceOrMap := refl.IsSliceOrMap(output)
 	hasEmbeddedSliceOrMap := refl.FindEmbeddedSliceOrMap(output) != nil
 	isJSONMarshaler := refl.As(output, new(json.Marshaler))
@@ -85,7 +86,8 @@ func OutputHasNoContent(output interface{}) bool {
 
 	if withWriter ||
 		noContent ||
-		hasTaggedFields ||
+		hasJSONTaggedFields ||
+		hasContentTypeTaggedFields ||
 		isSliceOrMap ||
 		hasEmbeddedSliceOrMap ||
 		isJSONMarshaler ||


Test patch:
diff --git a/request/factory_test.go b/request/factory_test.go
index ebad192..fdf6eb8 100644
--- a/request/factory_test.go
+++ b/request/factory_test.go
@@ -336,3 +336,34 @@ func TestNewDecoderFactory_default(t *testing.T) {
 	assert.Equal(t, enum1, string(*input.DefaultedQueryVal))
 	assert.Equal(t, enum2, string(input.DefaultedTagVal))
 }
+
+func TestNewDecoderFactory_requestBody(t *testing.T) {
+	type Req struct {
+		TextBody string `contentType:"text/plain"`
+		CSVBody  string `contentType:"text/csv"`
+	}
+
+	req, err := http.NewRequest(http.MethodPost, "/foo", bytes.NewReader([]byte(`hello world`)))
+	require.NoError(t, err)
+
+	req.Header.Set("Content-Type", "text/plain")
+
+	var input Req
+	dec := request.NewDecoderFactory().MakeDecoder(http.MethodPost, input, nil)
+
+	require.NoError(t, dec.Decode(req, &input, nil))
+
+	assert.Equal(t, "hello world", input.TextBody)
+	assert.Empty(t, input.CSVBody)
+
+	req, err = http.NewRequest(http.MethodPost, "/foo", bytes.NewReader([]byte(`hello,world`)))
+	require.NoError(t, err)
+
+	req.Header.Set("Content-Type", "text/csv")
+
+	input = Req{}
+	require.NoError(t, dec.Decode(req, &input, nil))
+
+	assert.Equal(t, "hello,world", input.CSVBody)
+	assert.Empty(t, input.TextBody)
+}
diff --git a/response/encoder_test.go b/response/encoder_test.go
index 93d530f..6b1f27f 100644
--- a/response/encoder_test.go
+++ b/response/encoder_test.go
@@ -300,3 +300,30 @@ func TestEmbeddedSetter_SetResponseWriter(t *testing.T) {
 	assert.Equal(t, `{"items":["one","two","three"]}`+"\n", w.Body.String())
 	assert.Equal(t, w, out.ResponseWriter())
 }
+
+func TestEncoder_contentTypeRaw(t *testing.T) {
+	type Resp struct {
+		TextBody string `contentType:"text/plain"`
+		CSVBody  string `contentType:"text/csv"`
+	}
+
+	ht := rest.HandlerTrait{}
+
+	e := response.Encoder{}
+	e.SetupOutput(Resp{}, &ht)
+
+	w := httptest.NewRecorder()
+
+	v := e.MakeOutput(w, ht)
+
+	re, ok := v.(*Resp)
+	assert.True(t, ok)
+
+	re.CSVBody = "hello,world"
+
+	e.WriteSuccessfulResponse(w, nil, v, ht)
+
+	assert.Equal(t, http.StatusOK, w.Code)
+	assert.Equal(t, "hello,world", w.Body.String())
+	assert.Equal(t, "text/csv", w.Header().Get("Content-Type"))
+}