Repository: swaggest/openapi-go
Pull request #65 (2023-06-27 16:20:56)

Introduce abstraction to describe operation and reflector

github-actions[bot]: ### Go API Changes

<pre>
gorelease: go: updates to go.sum needed, disabled by -mod=readonly
</pre>
<!-- Sticky Pull Request Commentgorelease -->
github-actions[bot]: ### Lines Of Code

| Language  | Files  |    Lines    |    Code     | Comments |  Blanks   | Complexity |    Bytes    |
|-----------|--------|-------------|-------------|----------|-----------|------------|-------------|
| Go        | 9 (+2) | 1666 (+344) | 1235 (+245) | 96 (+32) | 335 (+67) | 321 (+16)  | 38K (+8.1K) |
| Go (test) | 7      | 1847        | 1368        | 246      | 233       | 25         | 47.8K (-9B) |
<!-- Sticky Pull Request CommentLOC -->
github-actions[bot]: ### Unit Test Coverage
total:								(statements)				49.7%
changed lines: (statements) 66.2%, coverage is less than 90.0%, consider testing the changes more thoroughly
<details><summary>Coverage of changed lines</summary>

|          File           |         Function          | Coverage |
|-------------------------|---------------------------|----------|
| Total                   |                           | 66.2%    |
| openapi3/helper.go      |                           | 100.0%   |
| openapi3/helper.go:106  | AddOperation              | 100.0%   |
| openapi3/reflect.go     |                           | 89.0%    |
| openapi3/reflect.go:101 | toOpCtx                   | 100.0%   |
| openapi3/reflect.go:129 | fromOpCtx                 | 100.0%   |
| openapi3/reflect.go:197 | SetupRequest              | 100.0%   |
| openapi3/reflect.go:260 | parseRequestBody          | 100.0%   |
| openapi3/reflect.go:380 | parseParameters           | 100.0%   |
| openapi3/reflect.go:388 | parseParametersIn         | 100.0%   |
| openapi3/reflect.go:530 | parseResponseHeader       | 100.0%   |
| openapi3/reflect.go:636 | setupResponse             | 100.0%   |
| openapi3/reflect.go:675 | SetupResponse             | 100.0%   |
| openapi3/reflect.go:679 | ensureResponseContentType | 100.0%   |
| openapi3/reflect.go:689 | parseJSONResponse         | 100.0%   |
| openapi3/reflect.go:743 | OperationCtx              | 100.0%   |
| openapi3/reflect.go:235 | mediaType                 | 95.7%    |
| openapi3/reflect.go:169 | setupRequest              | 28.1%    |
| openapi3/reflect.go:97  | Operation                 | 0.0%     |
| openapi3/reflect.go:161 | SetupOperation            | 0.0%     |
| openapi3/reflect.go:252 | stringRequestBody         | 0.0%     |
| operation.go            |                           | 0.0%     |
| operation.go:37         | FieldMapping              | 0.0%     |
| operation.go:42         | FieldMapping              | 0.0%     |
| operation.go:80         | NewOperationContext       | 0.0%     |
| operation.go:99         | Method                    | 0.0%     |
| operation.go:104        | PathPattern               | 0.0%     |
| operation.go:109        | Request                   | 0.0%     |
| operation.go:114        | Response                  | 0.0%     |
| operation.go:119        | SetIsProcessingResponse   | 0.0%     |
| operation.go:124        | IsProcessingResponse      | 0.0%     |
| operation.go:129        | SetProcessingIn           | 0.0%     |
| operation.go:134        | ProcessingIn              | 0.0%     |
| operation.go:139        | SetMethod                 | 0.0%     |
| operation.go:144        | SetPathPattern            | 0.0%     |
| operation.go:149        | AddReqStructure           | 0.0%     |
| operation.go:161        | AddRespStructure          | 0.0%     |
| operation.go:175        | WithOperationCtx          | 0.0%     |
| operation.go:185        | OperationCtx              | 0.0%     |

</details>

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

|                        File                        |        Function         | Base Coverage | Current Coverage |
|----------------------------------------------------|-------------------------|---------------|------------------|
| Total                                              |                         | 49.8%         | 49.7% (-0.1%)    |
| github.com/swaggest/openapi-go/openapi3/reflect.go | Operation               | no function   | 0.0%             |
| github.com/swaggest/openapi-go/openapi3/reflect.go | SetupOperation          | no function   | 0.0%             |
| github.com/swaggest/openapi-go/openapi3/reflect.go | SetupResponse           | 90.0%         | 100.0% (+10.0%)  |
| github.com/swaggest/openapi-go/openapi3/reflect.go | fromOpCtx               | no function   | 100.0%           |
| github.com/swaggest/openapi-go/openapi3/reflect.go | mediaType               | no function   | 85.7%            |
| github.com/swaggest/openapi-go/openapi3/reflect.go | parseJSONResponse       | 89.5%         | 90.5% (+1.0%)    |
| github.com/swaggest/openapi-go/openapi3/reflect.go | parseParameters         | no function   | 100.0%           |
| github.com/swaggest/openapi-go/openapi3/reflect.go | parseRequestBody        | 98.0%         | 97.9% (-0.1%)    |
| github.com/swaggest/openapi-go/openapi3/reflect.go | parseResponseHeader     | 91.3%         | 92.0% (+0.7%)    |
| github.com/swaggest/openapi-go/openapi3/reflect.go | setupRequest            | no function   | 42.9%            |
| github.com/swaggest/openapi-go/openapi3/reflect.go | setupResponse           | no function   | 93.8%            |
| github.com/swaggest/openapi-go/openapi3/reflect.go | stringRequestBody       | no function   | 0.0%             |
| github.com/swaggest/openapi-go/openapi3/reflect.go | toOpCtx                 | no function   | 100.0%           |
| github.com/swaggest/openapi-go/openapi3/reflect.go | withOperation           | 100.0%        | no function      |
| github.com/swaggest/openapi-go/operation.go        | AddReqStructure         | no function   | 0.0%             |
| github.com/swaggest/openapi-go/operation.go        | AddRespStructure        | no function   | 0.0%             |
| github.com/swaggest/openapi-go/operation.go        | FieldMapping            | no function   | 0.0%             |
| github.com/swaggest/openapi-go/operation.go        | IsProcessingResponse    | no function   | 0.0%             |
| github.com/swaggest/openapi-go/operation.go        | Method                  | no function   | 0.0%             |
| github.com/swaggest/openapi-go/operation.go        | NewOperationContext     | no function   | 0.0%             |
| github.com/swaggest/openapi-go/operation.go        | OperationCtx            | no function   | 0.0%             |
| github.com/swaggest/openapi-go/operation.go        | PathPattern             | no function   | 0.0%             |
| github.com/swaggest/openapi-go/operation.go        | ProcessingIn            | no function   | 0.0%             |
| github.com/swaggest/openapi-go/operation.go        | Request                 | no function   | 0.0%             |
| github.com/swaggest/openapi-go/operation.go        | Response                | no function   | 0.0%             |
| github.com/swaggest/openapi-go/operation.go        | SetIsProcessingResponse | no function   | 0.0%             |
| github.com/swaggest/openapi-go/operation.go        | SetMethod               | no function   | 0.0%             |
| github.com/swaggest/openapi-go/operation.go        | SetPathPattern          | no function   | 0.0%             |
| github.com/swaggest/openapi-go/operation.go        | SetProcessingIn         | no function   | 0.0%             |
| github.com/swaggest/openapi-go/operation.go        | WithOperationCtx        | no function   | 0.0%             |

</details>
<!-- Sticky Pull Request Commentunit-test -->
codecov[bot]: ## [Codecov](https://app.codecov.io/gh/swaggest/openapi-go/pull/65?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=swaggest) Report
> Merging [#65](https://app.codecov.io/gh/swaggest/openapi-go/pull/65?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=swaggest) (41ece5b) into [master](https://app.codecov.io/gh/swaggest/openapi-go/commit/98ec731220f2f34f71ef653f999c6a45e55895be?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=swaggest) (98ec731) will **increase** coverage by `0.05%`.
> The diff coverage is `66.87%`.

```diff
@@            Coverage Diff             @@
##           master      #65      +/-   ##
==========================================
+ Coverage   42.17%   42.22%   +0.05%     
==========================================
  Files           5        6       +1     
  Lines        3585     3673      +88     
==========================================
+ Hits         1512     1551      +39     
- Misses       1784     1833      +49     
  Partials      289      289              
```

| Flag | Coverage Δ | |
|---|---|---|
| unittests | `42.22% <66.87%> (+0.05%)` | :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://app.codecov.io/gh/swaggest/openapi-go/pull/65?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=swaggest) | Coverage Δ | |
|---|---|---|
| [operation.go](https://app.codecov.io/gh/swaggest/openapi-go/pull/65?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=swaggest#diff-b3BlcmF0aW9uLmdv) | `0.00% <0.00%> (ø)` | |
| [openapi3/reflect.go](https://app.codecov.io/gh/swaggest/openapi-go/pull/65?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=swaggest#diff-b3BlbmFwaTMvcmVmbGVjdC5nbw==) | `80.68% <72.79%> (-6.23%)` | :arrow_down: |
| [openapi3/helper.go](https://app.codecov.io/gh/swaggest/openapi-go/pull/65?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=swaggest#diff-b3BlbmFwaTMvaGVscGVyLmdv) | `89.36% <83.33%> (-1.38%)` | :arrow_down: |

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


Patch:
diff --git a/go.mod b/go.mod
index 8a2953d..b5be700 100644
--- a/go.mod
+++ b/go.mod
@@ -6,8 +6,8 @@ require (
 	github.com/bool64/dev v0.2.29
 	github.com/stretchr/testify v1.8.2
 	github.com/swaggest/assertjson v1.9.0
-	github.com/swaggest/jsonschema-go v0.3.52
-	github.com/swaggest/refl v1.1.0
+	github.com/swaggest/jsonschema-go v0.3.54
+	github.com/swaggest/refl v1.2.0
 	gopkg.in/yaml.v2 v2.4.0
 )
 
diff --git a/go.sum b/go.sum
index e19a01c..0adb2a1 100644
--- a/go.sum
+++ b/go.sum
@@ -33,10 +33,10 @@ github.com/stretchr/testify v1.8.2 h1:+h33VjcLVPDHtOdpUCuF+7gSuG3yGIftsP1YvFihtJ
 github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
 github.com/swaggest/assertjson v1.9.0 h1:dKu0BfJkIxv/xe//mkCrK5yZbs79jL7OVf9Ija7o2xQ=
 github.com/swaggest/assertjson v1.9.0/go.mod h1:b+ZKX2VRiUjxfUIal0HDN85W0nHPAYUbYH5WkkSsFsU=
-github.com/swaggest/jsonschema-go v0.3.52 h1:cFk0Ma34MuZvA+JJ8S5WuQMedwxzUa+9+qrGwtoE39U=
-github.com/swaggest/jsonschema-go v0.3.52/go.mod h1:sRly7iaIIvbheAqsyvnKU3H7ogWbF0wtUBXyGpRXNm4=
-github.com/swaggest/refl v1.1.0 h1:a+9a75Kv6ciMozPjVbOfcVTEQe81t2R3emvaD9oGQGc=
-github.com/swaggest/refl v1.1.0/go.mod h1:g3Qa6ki0A/L2yxiuUpT+cuBURuRaltF5SDQpg1kMZSY=
+github.com/swaggest/jsonschema-go v0.3.54 h1:kRwXd7tqrub8vmtAVV5IElGTE1hiEuo/n9gHrsX7ySY=
+github.com/swaggest/jsonschema-go v0.3.54/go.mod h1:iQdEa2VW62As5W+884vA13TC2pEwnKjlQGaQe2pj+fc=
+github.com/swaggest/refl v1.2.0 h1:Qqqhfwi7REXF6/4cwJmj3gQMzl0Q0cYquxTYdD0kvi0=
+github.com/swaggest/refl v1.2.0/go.mod h1:CkC6g7h1PW33KprTuYRSw8UUOslRUt4lF3oe7tTIgNU=
 github.com/yudai/gojsondiff v1.0.0 h1:27cbfqXLVEJ1o8I6v3y9lg8Ydm53EKqHXAOMxEGlCOA=
 github.com/yudai/gojsondiff v1.0.0/go.mod h1:AY32+k2cwILAkW1fbgxQ5mUmMiZFgLIV+FBNExI05xg=
 github.com/yudai/golcs v0.0.0-20170316035057-ecda9a501e82 h1:BHyfKlQyqbsFN5p3IfnEUduWvb9is428/nNb5L3U01M=
diff --git a/internal/operation_context.go b/internal/operation_context.go
new file mode 100644
index 0000000..2a58a99
--- /dev/null
+++ b/internal/operation_context.go
@@ -0,0 +1,97 @@
+// Package internal keeps reusable internal code.
+package internal
+
+import "github.com/swaggest/openapi-go"
+
+// NewOperationContext creates OperationContext.
+func NewOperationContext(method, pathPattern string) *OperationContext {
+	return &OperationContext{
+		method:      method,
+		pathPattern: pathPattern,
+	}
+}
+
+// OperationContext implements openapi.OperationContext.
+type OperationContext struct {
+	method      string
+	pathPattern string
+	req         []openapi.ContentUnit
+	resp        []openapi.ContentUnit
+
+	isProcessingResponse bool
+	processingIn         openapi.In
+}
+
+// Method returns HTTP method of an operation.
+func (o *OperationContext) Method() string {
+	return o.method
+}
+
+// PathPattern returns operation HTTP URL path pattern.
+func (o *OperationContext) PathPattern() string {
+	return o.pathPattern
+}
+
+// Request returns list of operation request content schemas.
+func (o *OperationContext) Request() []openapi.ContentUnit {
+	return o.req
+}
+
+// Response returns list of operation response content schemas.
+func (o *OperationContext) Response() []openapi.ContentUnit {
+	return o.resp
+}
+
+// SetIsProcessingResponse sets current processing state.
+func (o *OperationContext) SetIsProcessingResponse(is bool) {
+	o.isProcessingResponse = is
+}
+
+// IsProcessingResponse indicates if response is being processed.
+func (o *OperationContext) IsProcessingResponse() bool {
+	return o.isProcessingResponse
+}
+
+// SetProcessingIn sets current content location being processed.
+func (o *OperationContext) SetProcessingIn(in openapi.In) {
+	o.processingIn = in
+}
+
+// ProcessingIn return which content location is being processed now.
+func (o *OperationContext) ProcessingIn() openapi.In {
+	return o.processingIn
+}
+
+// SetMethod sets HTTP method of an operation.
+func (o *OperationContext) SetMethod(method string) {
+	o.method = method
+}
+
+// SetPathPattern sets URL path pattern of an operation.
+func (o *OperationContext) SetPathPattern(pattern string) {
+	o.pathPattern = pattern
+}
+
+// AddReqStructure adds request content schema.
+func (o *OperationContext) AddReqStructure(s interface{}, options ...openapi.ContentOption) {
+	c := openapi.ContentUnit{}
+	c.Structure = s
+
+	for _, o := range options {
+		o(&c)
+	}
+
+	o.req = append(o.req, c)
+}
+
+// AddRespStructure adds response content schema.
+func (o *OperationContext) AddRespStructure(s interface{}, options ...openapi.ContentOption) {
+	c := openapi.ContentUnit{}
+	c.Structure = s
+
+	for _, o := range options {
+		o(&c)
+	}
+
+	o.resp = append(o.resp, c)
+}
diff --git a/marker.go b/marker.go
new file mode 100644
index 0000000..0e8df41
--- /dev/null
+++ b/marker.go
@@ -0,0 +1,16 @@
+package openapi
+
+// RequestBodyEnforcer enables request body for GET and HEAD methods.
+//
+// Should be implemented on input structure, function body can be empty.
+// Forcing request body is not recommended and should only be used for backwards compatibility.
+type RequestBodyEnforcer interface {
+	ForceRequestBody()
+}
+
+// RequestJSONBodyEnforcer enables JSON request body for structures with `formData` tags.
+//
+// Should be implemented on input structure, function body can be empty.
+type RequestJSONBodyEnforcer interface {
+	ForceJSONRequestBody()
+}
diff --git a/openapi3/helper.go b/openapi3/helper.go
index 7d7f9b7..37a37db 100644
--- a/openapi3/helper.go
+++ b/openapi3/helper.go
@@ -4,9 +4,10 @@ import (
 	"errors"
 	"fmt"
 	"net/http"
-	"regexp"
 	"strconv"
 	"strings"
+
+	"github.com/swaggest/openapi-go"
 )
 
 // ToParameterOrRef exposes Parameter in general form.
@@ -23,47 +24,12 @@ func (p *PathItem) WithOperation(method string, operation Operation) *PathItem {
 	return p.WithMapOfOperationValuesItem(strings.ToLower(method), operation)
 }
 
-var regexFindPathParameter = regexp.MustCompile(`{([^}:]+)(:[^}]+)?(?:})`)
-
-// SetupOperation creates operation if it is not present and applies setup functions.
-func (s *Spec) SetupOperation(method, path string, setup ...func(*Operation) error) error {
-	method = strings.ToLower(method)
-	pathParametersSubmatches := regexFindPathParameter.FindAllStringSubmatch(path, -1)
-
-	switch method {
-	case "get", "put", "post", "delete", "options", "head", "patch", "trace":
-		break
-	default:
-		return fmt.Errorf("unexpected http method: %s", method)
-	}
-
-	pathParams := map[string]bool{}
-
-	if len(pathParametersSubmatches) > 0 {
-		for _, submatch := range pathParametersSubmatches {
-			pathParams[submatch[1]] = true
-
-			if submatch[2] != "" { // Remove gorilla.Mux-style regexp in path
-				path = strings.Replace(path, submatch[0], "{"+submatch[1]+"}", 1)
-			}
-		}
-	}
+func (o *Operation) validatePathParams(pathParams map[string]bool) error {
+	paramIndex := make(map[string]bool, len(o.Parameters))
 
 	var errs []string
 
-	pathItem := s.Paths.MapOfPathItemValues[path]
-	operation := pathItem.MapOfOperationValues[method]
-
-	for _, f := range setup {
-		err := f(&operation)
-		if err != nil {
-			return err
-		}
-	}
-
-	paramIndex := make(map[string]bool, len(operation.Parameters))
-
-	for _, p := range operation.Parameters {
+	for _, p := range o.Parameters {
 		if p.Parameter == nil {
 			continue
 		}
@@ -93,6 +59,34 @@ func (s *Spec) SetupOperation(method, path string, setup ...func(*Operation) err
 		return errors.New(strings.Join(errs, ", "))
 	}
 
+	return nil
+}
+
+// SetupOperation creates operation if it is not present and applies setup functions.
+func (s *Spec) SetupOperation(method, path string, setup ...func(*Operation) error) error {
+	method, path, pathParams, err := openapi.SanitizeMethodPath(method, path)
+	if err != nil {
+		return err
+	}
+
+	pathItem := s.Paths.MapOfPathItemValues[path]
+	operation := pathItem.MapOfOperationValues[method]
+
+	for _, f := range setup {
+		if err := f(&operation); err != nil {
+			return err
+		}
+	}
+
+	pathParamsMap := make(map[string]bool, len(pathParams))
+	for _, p := range pathParams {
+		pathParamsMap[p] = true
+	}
+
+	if err := operation.validatePathParams(pathParamsMap); err != nil {
+		return err
+	}
+
 	pathItem.WithMapOfOperationValuesItem(method, operation)
 
 	s.Paths.WithMapOfPathItemValuesItem(path, pathItem)
@@ -108,7 +102,7 @@ func (s *Spec) AddOperation(method, path string, operation Operation) error {
 	pathItem := s.Paths.MapOfPathItemValues[path]
 
 	if _, found := pathItem.MapOfOperationValues[method]; found {
-		return errors.New("operation with method and path already exists")
+		return fmt.Errorf("operation already exists: %s %s", method, path)
 	}
 
 	// Add "No Content" response if there are no responses configured.
diff --git a/openapi3/reflect.go b/openapi3/reflect.go
index e019503..62fad67 100644
--- a/openapi3/reflect.go
+++ b/openapi3/reflect.go
@@ -2,7 +2,6 @@ package openapi3
 
 import (
 	"bytes"
-	"context"
 	"encoding/json"
 	"errors"
 	"fmt"
@@ -13,6 +12,8 @@ import (
 	"strings"
 
 	"github.com/swaggest/jsonschema-go"
+	"github.com/swaggest/openapi-go"
+	"github.com/swaggest/openapi-go/internal"
 	"github.com/swaggest/refl"
 )
 
@@ -22,10 +23,47 @@ type Reflector struct {
 	Spec *Spec
 }
 
+// NewReflector creates an instance of OpenAPI 3.0 reflector.
+func NewReflector() *Reflector {
+	r := &Reflector{}
+	r.SpecEns()
+
+	return r
+}
+
+// NewOperationContext initializes openapi.OperationContext to be prepared
+// and added later with Reflector.AddOperation.
+func (r *Reflector) NewOperationContext(method, pathPattern string) (openapi.OperationContext, error) {
+	method, pathPattern, pathParams, err := openapi.SanitizeMethodPath(method, pathPattern)
+	if err != nil {
+		return nil, err
+	}
+
+	pathItem := r.SpecEns().Paths.MapOfPathItemValues[pathPattern]
+	operation, found := pathItem.MapOfOperationValues[method]
+
+	if found {
+		return nil, fmt.Errorf("operation already exists: %s %s", method, pathPattern)
+	}
+
+	pathParamsMap := make(map[string]bool, len(pathParams))
+	for _, p := range pathParams {
+		pathParamsMap[p] = true
+	}
+
+	oc := operationContext{
+		OperationContext: internal.NewOperationContext(method, pathPattern),
+		op:               &operation,
+		pathParams:       pathParamsMap,
+	}
+
+	return oc, nil
+}
+
 // ResolveJSONSchemaRef builds JSON Schema from OpenAPI Component Schema reference.
 //
 // Can be used in jsonschema.Schema IsTrivial().
-func (r Reflector) ResolveJSONSchemaRef(ref string) (s jsonschema.SchemaOrBool, found bool) {
+func (r *Reflector) ResolveJSONSchemaRef(ref string) (s jsonschema.SchemaOrBool, found bool) {
 	if r.Spec == nil || r.Spec.Components == nil || r.Spec.Components.Schemas == nil ||
 		!strings.HasPrefix(ref, "#/components/schemas/") {
 		return s, false
@@ -67,81 +105,216 @@ func (r *Reflector) SpecEns() *Spec {
 	return r.Spec
 }
 
-// OperationContext describes operation.
-type OperationContext struct {
-	Operation  *Operation
-	Input      interface{}
-	HTTPMethod string
-
-	ReqQueryMapping    map[string]string
-	ReqPathMapping     map[string]string
-	ReqCookieMapping   map[string]string
-	ReqHeaderMapping   map[string]string
-	ReqFormDataMapping map[string]string
-
-	Output            interface{}
-	HTTPStatus        int
-	RespContentType   string
-	RespHeaderMapping map[string]string
-
-	ProcessingResponse bool
-	ProcessingIn       string
+type operationContext struct {
+	*internal.OperationContext
+
+	op *Operation
+
+	pathParams map[string]bool
+}
+
+// OperationExposer grants access to underlying *Operation.
+type OperationExposer interface {
+	Operation() *Operation
+}
+
+func (o operationContext) AddSecurity(securityName string, scopes ...string) {
+	if scopes == nil {
+		scopes = []string{}
+	}
+
+	o.op.Security = append(o.op.Security, map[string][]string{securityName: scopes})
 }
 
-// SetupRequest sets up operation parameters.
-func (r *Reflector) SetupRequest(oc OperationContext) error {
-	return joinErrors(
-		r.parseParametersIn(oc, oc.ReqQueryMapping, ParameterInQuery, tagForm),
-		r.parseParametersIn(oc, oc.ReqPathMapping, ParameterInPath),
-		r.parseParametersIn(oc, oc.ReqCookieMapping, ParameterInCookie),
-		r.parseParametersIn(oc, oc.ReqHeaderMapping, ParameterInHeader),
-		r.parseRequestBody(oc, mimeJSON, oc.HTTPMethod, nil, tagJSON),
-		r.parseRequestBody(oc, mimeFormUrlencoded, oc.HTTPMethod, oc.ReqFormDataMapping, tagFormData, tagForm),
+func (o operationContext) SetTags(tags ...string) {
+	o.op.WithTags(tags...)
+}
+
+func (o operationContext) SetIsDeprecated(isDeprecated bool) {
+	o.op.WithDeprecated(isDeprecated)
+}
+
+func (o operationContext) SetSummary(summary string) {
+	o.op.WithSummary(summary)
+}
+
+func (o operationContext) SetDescription(description string) {
+	o.op.WithDescription(description)
+}
+
+func (o operationContext) SetID(operationID string) {
+	o.op.WithID(operationID)
+}
+
+func (o operationContext) UnknownParamsAreForbidden(in openapi.In) bool {
+	return o.op.UnknownParamIsForbidden(ParameterIn(in))
+}
+
+// Operation returns OpenAPI 3 operation for customization.
+func (o operationContext) Operation() *Operation {
+	return o.op
+}
+
+func toOpCtx(c OperationContext) operationContext {
+	oc := internal.NewOperationContext(c.HTTPMethod, "")
+
+	oc.AddReqStructure(c.Input,
+		func(cu *openapi.ContentUnit) {
+			cu.SetFieldMapping(openapi.InHeader, c.ReqHeaderMapping)
+			cu.SetFieldMapping(openapi.InQuery, c.ReqQueryMapping)
+			cu.SetFieldMapping(openapi.InCookie, c.ReqCookieMapping)
+			cu.SetFieldMapping(openapi.InPath, c.ReqPathMapping)
+			cu.SetFieldMapping(openapi.InFormData, c.ReqFormDataMapping)
+		},
+	)
+
+	oc.AddRespStructure(c.Output,
+		func(cu *openapi.ContentUnit) {
+			cu.ContentType = c.RespContentType
+			cu.HTTPStatus = c.HTTPStatus
+			cu.SetFieldMapping(openapi.InHeader, c.RespHeaderMapping)
+		},
 	)
+
+	oc.SetProcessingIn(openapi.In(c.ProcessingIn))
+	oc.SetIsProcessingResponse(c.ProcessingResponse)
+
+	return operationContext{
+		OperationContext: oc,
+		op:               c.Operation,
+	}
 }
 
-// SetRequest sets up operation parameters.
-func (r *Reflector) SetRequest(o *Operation, input interface{}, httpMethod string) error {
-	return r.SetupRequest(OperationContext{
-		Operation:  o,
-		Input:      input,
-		HTTPMethod: httpMethod,
-	})
+func fromOpCtx(oc openapi.OperationContext) OperationContext {
+	c := OperationContext{}
+
+	c.HTTPMethod = oc.Method()
+
+	if req := oc.Request(); len(req) > 0 {
+		cu := req[0]
+
+		c.Input = cu.Structure
+		c.ReqQueryMapping = cu.FieldMapping(openapi.InQuery)
+		c.ReqPathMapping = cu.FieldMapping(openapi.InPath)
+		c.ReqHeaderMapping = cu.FieldMapping(openapi.InHeader)
+		c.ReqFormDataMapping = cu.FieldMapping(openapi.InFormData)
+		c.ReqCookieMapping = cu.FieldMapping(openapi.InCookie)
+	}
+
+	if resp := oc.Response(); len(resp) > 0 {
+		cu := resp[0]
+
+		c.Output = cu.Structure
+		c.RespHeaderMapping = cu.FieldMapping(openapi.InHeader)
+		c.RespContentType = cu.ContentType
+		c.HTTPStatus = cu.HTTPStatus
+	}
+
+	c.ProcessingResponse = oc.IsProcessingResponse()
+	c.ProcessingIn = string(oc.ProcessingIn())
+
+	return c
+}
+
+// AddOperation configures operation request and response schema.
+func (r *Reflector) AddOperation(oc openapi.OperationContext) error {
+	c, ok := oc.(operationContext)
+	if !ok {
+		return fmt.Errorf("wrong operation context %T received, %T expected", oc, operationContext{})
+	}
+
+	if err := r.setupRequest(c.op, oc); err != nil {
+		return fmt.Errorf("setup request %s %s: %w", oc.Method(), oc.PathPattern(), err)
+	}
+
+	if err := c.op.validatePathParams(c.pathParams); err != nil {
+		return fmt.Errorf("validate path params %s %s: %w", oc.Method(), oc.PathPattern(), err)
+	}
+
+	if err := r.setupResponse(c.op, oc); err != nil {
+		return fmt.Errorf("setup response %s %s: %w", oc.Method(), oc.PathPattern(), err)
+	}
+
+	return r.SpecEns().AddOperation(oc.Method(), oc.PathPattern(), *c.op)
+}
+
+func (r *Reflector) setupRequest(o *Operation, oc openapi.OperationContext) error {
+	for _, cu := range oc.Request() {
+		switch cu.ContentType {
+		case "":
+			return joinErrors(
+				r.parseParameters(o, oc, cu),
+				r.parseRequestBody(o, oc, cu, mimeJSON, oc.Method(), nil, tagJSON),
+				r.parseRequestBody(o, oc, cu, mimeFormUrlencoded, oc.Method(), cu.FieldMapping(openapi.InFormData), tagFormData, tagForm),
+			)
+		case mimeJSON:
+			return joinErrors(
+				r.parseParameters(o, oc, cu),
+				r.parseRequestBody(o, oc, cu, mimeJSON, oc.Method(), nil, tagJSON),
+			)
+		case mimeFormUrlencoded, mimeMultipart:
+			return joinErrors(
+				r.parseParameters(o, oc, cu),
+				r.parseRequestBody(o, oc, cu, mimeFormUrlencoded, oc.Method(), cu.FieldMapping(openapi.InFormData), tagFormData, tagForm),
+			)
+		default:
+			r.stringRequestBody(o, cu.ContentType, cu.Format)
+		}
+	}
+
+	return nil
 }
 
 const (
 	tagJSON            = "json"
 	tagFormData        = "formData"
 	tagForm            = "form"
+	tagHeader          = "header"
 	mimeJSON           = "application/json"
 	mimeFormUrlencoded = "application/x-www-form-urlencoded"
 	mimeMultipart      = "multipart/form-data"
 )
 
-// RequestBodyEnforcer enables request body for GET and HEAD methods.
-//
-// Should be implemented on input structure, function body can be empty.
-// Forcing request body is not recommended and should only be used for backwards compatibility.
-type RequestBodyEnforcer interface {
-	ForceRequestBody()
+func mediaType(format string) MediaType {
+	schema := jsonschema.String.ToSchemaOrBool()
+	if format != "" {
+		schema.TypeObject.WithFormat(format)
+	}
+
+	schemaOrRef := SchemaOrRef{}
+
+	schemaOrRef.FromJSONSchema(schema)
+
+	mt := MediaType{
+		Schema: &schemaOrRef,
+	}
+
+	return mt
 }
 
-// RequestJSONBodyEnforcer enables JSON request body for structures with `formData` tags.
-//
-// Should be implemented on input structure, function body can be empty.
-type RequestJSONBodyEnforcer interface {
-	ForceJSONRequestBody()
+func (r *Reflector) stringRequestBody(
+	o *Operation,
+	mime string,
+	format string,
+) {
+	o.RequestBodyEns().RequestBodyEns().WithContentItem(mime, mediaType(format))
 }
 
 func (r *Reflector) parseRequestBody(
-	oc OperationContext, mime string, httpMethod string, mapping map[string]string, tag string, additionalTags ...string,
+	o *Operation,
+	oc openapi.OperationContext,
+	cu openapi.ContentUnit,
+	mime string,
+	httpMethod string,
+	mapping map[string]string,
+	tag string,
+	additionalTags ...string,
 ) error {
-	o := oc.Operation
-	input := oc.Input
+	input := cu.Structure
 
 	httpMethod = strings.ToUpper(httpMethod)
-	_, forceRequestBody := input.(RequestBodyEnforcer)
-	_, forceJSONRequestBody := input.(RequestJSONBodyEnforcer)
+	_, forceRequestBody := input.(openapi.RequestBodyEnforcer)
+	_, forceJSONRequestBody := input.(openapi.RequestJSONBodyEnforcer)
 
 	// GET, HEAD, DELETE and TRACE requests should not have body.
 	switch httpMethod {
@@ -183,7 +356,7 @@ func (r *Reflector) parseRequestBody(
 	}
 
 	schema, err := r.Reflect(input,
-		r.withOperation(oc, false, "body"),
+		openapi.WithOperationCtx(oc, false, "body"),
 		jsonschema.DefinitionsPrefix("#/components/schemas/"+definitionPrefix),
 		jsonschema.RootRef,
 		jsonschema.PropertyNameMapping(mapping),
@@ -247,11 +420,23 @@ const (
 	xForbidUnknown = "x-forbid-unknown-"
 )
 
+func (r *Reflector) parseParameters(o *Operation, oc openapi.OperationContext, cu openapi.ContentUnit) error {
+	return joinErrors(r.parseParametersIn(o, oc, cu, openapi.InQuery, tagForm),
+		r.parseParametersIn(o, oc, cu, openapi.InPath),
+		r.parseParametersIn(o, oc, cu, openapi.InCookie),
+		r.parseParametersIn(o, oc, cu, openapi.InHeader),
+	)
+}
+
 func (r *Reflector) parseParametersIn(
-	oc OperationContext, propertyMapping map[string]string, in ParameterIn, additionalTags ...string,
+	o *Operation,
+	oc openapi.OperationContext,
+	c openapi.ContentUnit,
+	in openapi.In,
+	additionalTags ...string,
 ) error {
-	o := oc.Operation
-	input := oc.Input
+	input := c.Structure
+	propertyMapping := c.FieldMapping(in)
 
 	if refl.IsSliceOrMap(input) {
 		return nil
@@ -261,7 +446,7 @@ func (r *Reflector) parseParametersIn(
 	definitionsPrefix := "#/components/schemas/" + defNamePrefix
 
 	s, err := r.Reflect(input,
-		r.withOperation(oc, false, string(in)),
+		openapi.WithOperationCtx(oc, false, in),
 		jsonschema.DefinitionsPrefix(definitionsPrefix),
 		jsonschema.CollectDefinitions(r.collectDefinition(defNamePrefix)),
 		jsonschema.PropertyNameMapping(propertyMapping),
@@ -288,7 +473,7 @@ func (r *Reflector) parseParametersIn(
 
 			p := Parameter{
 				Name:        name,
-				In:          in,
+				In:          ParameterIn(in),
 				Description: propertySchema.Description,
 				Schema:      &s,
 				Content:     nil,
@@ -311,7 +496,7 @@ func (r *Reflector) parseParametersIn(
 			property := reflect.New(field.Type).Interface()
 			if refl.HasTaggedFields(property, tagJSON) && !refl.HasTaggedFields(property, string(in)) {
 				propertySchema, err := r.Reflect(property,
-					r.withOperation(oc, false, string(in)),
+					openapi.WithOperationCtx(oc, false, in),
 					jsonschema.DefinitionsPrefix(definitionsPrefix),
 					jsonschema.CollectDefinitions(r.collectDefinition(defNamePrefix)),
 					jsonschema.RootRef,
@@ -326,7 +511,7 @@ func (r *Reflector) parseParametersIn(
 				p.WithContentItem("application/json", MediaType{Schema: &openapiSchema})
 			} else {
 				ps, err := r.Reflect(reflect.New(field.Type).Interface(),
-					r.withOperation(oc, false, string(in)),
+					openapi.WithOperationCtx(oc, false, in),
 					jsonschema.InlineRefs)
 				if err != nil {
 					return err
@@ -342,7 +527,7 @@ func (r *Reflector) parseParametersIn(
 				return err
 			}
 
-			if in == ParameterInPath {
+			if in == openapi.InPath {
 				p.WithRequired(true)
 			}
 
@@ -392,19 +577,23 @@ func (r *Reflector) collectDefinition(namePrefix string) func(name string, schem
 	}
 }
 
-func (r *Reflector) parseResponseHeader(resp *Response, oc OperationContext) error {
-	output := oc.Output
-	mapping := oc.RespHeaderMapping
+func (r *Reflector) parseResponseHeader(resp *Response, oc openapi.OperationContext, cu openapi.ContentUnit) error {
+	output := cu.Structure
+	mapping := cu.FieldMapping(openapi.InHeader)
+
+	if output == nil {
+		return nil
+	}
 
 	res := make(map[string]HeaderOrRef)
 
 	schema, err := r.Reflect(output,
-		r.withOperation(oc, true, "header"),
+		openapi.WithOperationCtx(oc, true, openapi.InHeader),
 		jsonschema.InlineRefs,
 		jsonschema.PropertyNameMapping(mapping),
-		jsonschema.PropertyNameTag("header"),
+		jsonschema.PropertyNameTag(tagHeader),
 		jsonschema.InterceptProp(func(params jsonschema.InterceptPropParams) error {
-			if !params.Processed {
+			if !params.Processed || len(params.Path) > 1 { // only top-level fields (including embedded).
 				return nil
 			}
 
@@ -450,24 +639,6 @@ func (r *Reflector) parseResponseHeader(resp *Response, oc OperationContext) err
 	return nil
 }
 
-// SetStringResponse sets unstructured response.
-func (r *Reflector) SetStringResponse(o *Operation, httpStatus int, contentType string) error {
-	return r.SetupResponse(OperationContext{
-		Operation:       o,
-		HTTPStatus:      httpStatus,
-		RespContentType: contentType,
-	})
-}
-
-// SetJSONResponse sets up operation JSON response.
-func (r *Reflector) SetJSONResponse(o *Operation, output interface{}, httpStatus int) error {
-	return r.SetupResponse(OperationContext{
-		Operation:  o,
-		Output:     output,
-		HTTPStatus: httpStatus,
-	})
-}
-
 func (r *Reflector) hasJSONBody(output interface{}) (bool, error) {
 	schema, err := r.Reflect(output)
 	if err != nil {
@@ -494,64 +665,68 @@ func (r *Reflector) hasJSONBody(output interface{}) (bool, error) {
 	return false, nil
 }
 
-// SetupResponse sets up operation response.
-func (r *Reflector) SetupResponse(oc OperationContext) error {
-	if oc.HTTPStatus == 0 {
-		oc.HTTPStatus = 200
-	}
-
-	httpStatus := strconv.Itoa(oc.HTTPStatus)
-	resp := oc.Operation.Responses.MapOfResponseOrRefValues[httpStatus].Response
+func (r *Reflector) setupResponse(o *Operation, oc openapi.OperationContext) error {
+	for _, cu := range oc.Response() {
+		if cu.HTTPStatus == 0 {
+			cu.HTTPStatus = 200
+		}
 
-	if resp == nil {
-		resp = &Response{}
-	}
+		cu.ContentType = strings.Split(cu.ContentType, ";")[0]
 
-	if oc.Output != nil {
-		oc.RespContentType = strings.Split(oc.RespContentType, ";")[0]
+		httpStatus := strconv.Itoa(cu.HTTPStatus)
+		resp := o.Responses.MapOfResponseOrRefValues[httpStatus].Response
 
-		err := r.parseJSONResponse(resp, oc)
-		if err != nil {
-			return err
+		if resp == nil {
+			resp = &Response{}
 		}
 
-		err = r.parseResponseHeader(resp, oc)
-		if err != nil {
-			return err
+		if strings.ToUpper(oc.Method()) != http.MethodHead {
+			if err := joinErrors(
+				r.parseJSONResponse(resp, oc, cu),
+				r.parseResponseHeader(resp, oc, cu),
+			); err != nil {
+				return err
+			}
+
+			if cu.ContentType != "" {
+				r.ensureResponseContentType(resp, cu.ContentType, cu.Format)
+			}
+		} else {
+			// Only headers with HEAD method.
+			if err := r.parseResponseHeader(resp, oc, cu); err != nil {
+				return err
+			}
 		}
-	}
 
-	if oc.RespContentType != "" {
-		r.ensureResponseContentType(resp, oc.RespContentType)
-	}
+		if resp.Description == "" {
+			resp.Description = http.StatusText(cu.HTTPStatus)
+		}
 
-	if resp.Description == "" {
-		resp.Description = http.StatusText(oc.HTTPStatus)
+		o.Responses.WithMapOfResponseOrRefValuesItem(httpStatus, ResponseOrRef{
+			Response: resp,
+		})
 	}
 
-	oc.Operation.Responses.WithMapOfResponseOrRefValuesItem(httpStatus, ResponseOrRef{
-		Response: resp,
-	})
-
 	return nil
 }
 
-func (r *Reflector) ensureResponseContentType(resp *Response, contentType string) {
+func (r *Reflector) ensureResponseContentType(resp *Response, contentType string, format string) {
 	if _, ok := resp.Content[contentType]; !ok {
 		if resp.Content == nil {
 			resp.Content = map[string]MediaType{}
 		}
 
-		typeString := SchemaTypeString
-		resp.Content[contentType] = MediaType{
-			Schema: &SchemaOrRef{Schema: &Schema{Type: &typeString}},
-		}
+		resp.Content[contentType] = mediaType(format)
 	}
 }
 
-func (r *Reflector) parseJSONResponse(resp *Response, oc OperationContext) error {
-	output := oc.Output
-	contentType := oc.RespContentType
+func (r *Reflector) parseJSONResponse(resp *Response, oc openapi.OperationContext, cu openapi.ContentUnit) error {
+	output := cu.Structure
+	contentType := cu.ContentType
+
+	if output == nil {
+		return nil
+	}
 
 	// Check if output structure exposes meaningful schema.
 	if hasJSONBody, err := r.hasJSONBody(output); err == nil && !hasJSONBody {
@@ -559,7 +734,7 @@ func (r *Reflector) parseJSONResponse(resp *Response, oc OperationContext) error
 	}
 
 	schema, err := r.Reflect(output,
-		r.withOperation(oc, true, "body"),
+		openapi.WithOperationCtx(oc, true, openapi.InBody),
 		jsonschema.RootRef,
 		jsonschema.DefinitionsPrefix("#/components/schemas/"),
 		jsonschema.CollectDefinitions(r.collectDefinition("")),
@@ -597,23 +772,3 @@ func (r *Reflector) parseJSONResponse(resp *Response, oc OperationContext) error
 
 	return nil
 }
-
-type ocCtxKey struct{}
-
-func (r *Reflector) withOperation(oc OperationContext, processingResponse bool, in string) func(rc *jsonschema.ReflectContext) {
-	return func(rc *jsonschema.ReflectContext) {
-		oc.ProcessingResponse = processingResponse
-		oc.ProcessingIn = in
-
-		rc.Context = context.WithValue(rc.Context, ocCtxKey{}, oc)
-	}
-}
-
-// OperationCtx retrieves operation context from reflect context.
-func OperationCtx(rc *jsonschema.ReflectContext) (OperationContext, bool) {
-	if oc, ok := rc.Value(ocCtxKey{}).(OperationContext); ok {
-		return oc, true
-	}
-
-	return OperationContext{}, false
-}
diff --git a/openapi3/reflect_deprecated.go b/openapi3/reflect_deprecated.go
new file mode 100644
index 0000000..01c8658
--- /dev/null
+++ b/openapi3/reflect_deprecated.go
@@ -0,0 +1,106 @@
+package openapi3
+
+import (
+	"github.com/swaggest/jsonschema-go"
+	"github.com/swaggest/openapi-go"
+)
+
+// SetupRequest sets up operation parameters.
+//
+// Deprecated: instrument openapi.OperationContext and use AddOperation.
+func (r *Reflector) SetupRequest(c OperationContext) error {
+	return r.setupRequest(c.Operation, toOpCtx(c))
+}
+
+// SetRequest sets up operation parameters.
+//
+// Deprecated: instrument openapi.OperationContext and use AddOperation.
+func (r *Reflector) SetRequest(o *Operation, input interface{}, httpMethod string) error {
+	return r.SetupRequest(OperationContext{
+		Operation:  o,
+		Input:      input,
+		HTTPMethod: httpMethod,
+	})
+}
+
+// RequestBodyEnforcer enables request body for GET and HEAD methods.
+//
+// Should be implemented on input structure, function body can be empty.
+// Forcing request body is not recommended and should only be used for backwards compatibility.
+//
+// Deprecated: use openapi.RequestBodyEnforcer.
+type RequestBodyEnforcer interface {
+	ForceRequestBody()
+}
+
+// RequestJSONBodyEnforcer enables JSON request body for structures with `formData` tags.
+//
+// Should be implemented on input structure, function body can be empty.
+//
+// Deprecated: use openapi.RequestJSONBodyEnforcer.
+type RequestJSONBodyEnforcer interface {
+	ForceJSONRequestBody()
+}
+
+// SetStringResponse sets unstructured response.
+//
+// Deprecated: use AddOperation with openapi.OperationContext AddRespStructure.
+func (r *Reflector) SetStringResponse(o *Operation, httpStatus int, contentType string) error {
+	return r.SetupResponse(OperationContext{
+		Operation:       o,
+		HTTPStatus:      httpStatus,
+		RespContentType: contentType,
+	})
+}
+
+// SetJSONResponse sets up operation JSON response.
+//
+// Deprecated: use AddOperation with openapi.OperationContext AddRespStructure.
+func (r *Reflector) SetJSONResponse(o *Operation, output interface{}, httpStatus int) error {
+	return r.SetupResponse(OperationContext{
+		Operation:  o,
+		Output:     output,
+		HTTPStatus: httpStatus,
+	})
+}
+
+// SetupResponse sets up operation response.
+//
+// Deprecated: use AddOperation with openapi.OperationContext AddRespStructure.
+func (r *Reflector) SetupResponse(oc OperationContext) error {
+	return r.setupResponse(oc.Operation, toOpCtx(oc))
+}
+
+// OperationCtx retrieves operation context from reflect context.
+//
+// Deprecated: use openapi.OperationCtx.
+func OperationCtx(rc *jsonschema.ReflectContext) (OperationContext, bool) {
+	if oc, ok := openapi.OperationCtx(rc); ok {
+		return fromOpCtx(oc), true
+	}
+
+	return OperationContext{}, false
+}
+
+// OperationContext describes operation.
+//
+// Deprecated: use Reflector.NewOperationContext, exported access might be revoked in the future.
+type OperationContext struct {
+	Operation  *Operation
+	Input      interface{}
+	HTTPMethod string
+
+	ReqQueryMapping    map[string]string
+	ReqPathMapping     map[string]string
+	ReqCookieMapping   map[string]string
+	ReqHeaderMapping   map[string]string
+	ReqFormDataMapping map[string]string
+
+	Output            interface{}
+	HTTPStatus        int
+	RespContentType   string
+	RespHeaderMapping map[string]string
+
+	ProcessingResponse bool
+	ProcessingIn       string
+}
diff --git a/openapi3/walk_schema.go b/openapi3/walk_schema.go
new file mode 100644
index 0000000..877a252
--- /dev/null
+++ b/openapi3/walk_schema.go
@@ -0,0 +1,209 @@
+package openapi3
+
+import (
+	"fmt"
+	"net/http"
+
+	"github.com/swaggest/jsonschema-go"
+	"github.com/swaggest/openapi-go"
+	"github.com/swaggest/openapi-go/internal"
+)
+
+// WalkResponseJSONSchemas provides JSON schemas for response structure.
+func (r *Reflector) WalkResponseJSONSchemas(cu openapi.ContentUnit, cb openapi.JSONSchemaCallback, done func(oc openapi.OperationContext)) error {
+	oc := operationContext{
+		OperationContext: internal.NewOperationContext(http.MethodGet, "/"),
+		op:               &Operation{},
+	}
+
+	oc.AddRespStructure(nil, func(c *openapi.ContentUnit) {
+		*c = cu
+	})
+
+	defer func() {
+		if done != nil {
+			done(oc)
+		}
+	}()
+
+	op := oc.op
+
+	if err := r.setupResponse(op, oc); err != nil {
+		return err
+	}
+
+	var resp *Response
+
+	for _, r := range op.Responses.MapOfResponseOrRefValues {
+		resp = r.Response
+	}
+
+	if err := r.provideHeaderSchemas(resp, cb); err != nil {
+		return err
+	}
+
+	for _, cont := range resp.Content {
+		if cont.Schema == nil {
+			continue
+		}
+
+		schema := cont.Schema.ToJSONSchema(r.Spec)
+
+		if err := cb(openapi.InBody, "body", &schema, false); err != nil {
+			return fmt.Errorf("response body schema: %w", err)
+		}
+	}
+
+	return nil
+}
+
+func (r *Reflector) provideHeaderSchemas(resp *Response, cb openapi.JSONSchemaCallback) error {
+	for name, h := range resp.Headers {
+		if h.Header.Schema == nil {
+			continue
+		}
+
+		hh := h.Header
+		schema := hh.Schema.ToJSONSchema(r.Spec)
+
+		required := false
+		if hh.Required != nil && *hh.Required {
+			required = true
+		}
+
+		name = http.CanonicalHeaderKey(name)
+
+		if err := cb(openapi.InHeader, name, &schema, required); err != nil {
+			return fmt.Errorf("response header schema (%s): %w", name, err)
+		}
+	}
+
+	return nil
+}
+
+// WalkRequestJSONSchemas iterates over request parameters of a ContentUnit and call user function for param schemas.
+func (r *Reflector) WalkRequestJSONSchemas(
+	method string,
+	cu openapi.ContentUnit,
+	cb openapi.JSONSchemaCallback,
+	done func(oc openapi.OperationContext),
+) error {
+	oc := operationContext{
+		OperationContext: internal.NewOperationContext(method, "/"),
+		op:               &Operation{},
+	}
+
+	oc.AddReqStructure(nil, func(c *openapi.ContentUnit) {
+		*c = cu
+	})
+
+	defer func() {
+		if done != nil {
+			done(oc)
+		}
+	}()
+
+	op := oc.op
+
+	if err := r.setupRequest(op, oc); err != nil {
+		return err
+	}
+
+	err := r.provideParametersJSONSchemas(op, cb)
+	if err != nil {
+		return err
+	}
+
+	if op.RequestBody == nil || op.RequestBody.RequestBody == nil {
+		return nil
+	}
+
+	for ct, content := range op.RequestBody.RequestBody.Content {
+		schema := content.Schema.ToJSONSchema(r.Spec)
+
+		if ct == mimeJSON {
+			err = cb(openapi.InBody, "body", &schema, false)
+			if err != nil {
+				return fmt.Errorf("request body schema: %w", err)
+			}
+		}
+
+		if ct == mimeFormUrlencoded {
+			if err = provideFormDataSchemas(schema, cb); err != nil {
+				return err
+			}
+		}
+	}
+
+	return nil
+}
+
+func provideFormDataSchemas(schema jsonschema.SchemaOrBool, cb openapi.JSONSchemaCallback) error {
+	for name, propertySchema := range schema.TypeObject.Properties {
+		propertySchema := propertySchema
+
+		if propertySchema.TypeObject != nil && len(schema.TypeObject.ExtraProperties) > 0 {
+			cp := *propertySchema.TypeObject
+			propertySchema.TypeObject = &cp
+			propertySchema.TypeObject.ExtraProperties = schema.TypeObject.ExtraProperties
+		}
+
+		isRequired := false
+
+		for _, req := range schema.TypeObject.Required {
+			if req == name {
+				isRequired = true
+
+				break
+			}
+		}
+
+		err := cb(openapi.InFormData, name, &propertySchema, isRequired)
+		if err != nil {
+			return fmt.Errorf("request body schema: %w", err)
+		}
+	}
+
+	return nil
+}
+
+func (r *Reflector) provideParametersJSONSchemas(op *Operation, cb openapi.JSONSchemaCallback) error {
+	for _, p := range op.Parameters {
+		pp := p.Parameter
+
+		required := false
+		if pp.Required != nil && *pp.Required {
+			required = true
+		}
+
+		sc := paramSchema(pp)
+
+		if sc == nil {
+			if err := cb(openapi.In(pp.In), pp.Name, nil, required); err != nil {
+				return fmt.Errorf("schema for parameter (%s, %s): %w", pp.In, pp.Name, err)
+			}
+
+			continue
+		}
+
+		schema := sc.ToJSONSchema(r.Spec)
+
+		if err := cb(openapi.In(pp.In), pp.Name, &schema, required); err != nil {
+			return fmt.Errorf("schema for parameter (%s, %s): %w", pp.In, pp.Name, err)
+		}
+	}
+
+	return nil
+}
+
+func paramSchema(p *Parameter) *SchemaOrRef {
+	sc := p.Schema
+
+	if sc == nil {
+		if jsc, ok := p.Content[mimeJSON]; ok {
+			sc = jsc.Schema
+		}
+	}
+
+	return sc
+}
diff --git a/operation.go b/operation.go
new file mode 100644
index 0000000..a2378c3
--- /dev/null
+++ b/operation.go
@@ -0,0 +1,139 @@
+package openapi
+
+import (
+	"context"
+	"fmt"
+	"regexp"
+	"strings"
+
+	"github.com/swaggest/jsonschema-go"
+)
+
+// In defines value location in HTTP content.
+type In string
+
+// In values enumeration.
+const (
+	InPath     = In("path")
+	InQuery    = In("query")
+	InHeader   = In("header")
+	InCookie   = In("cookie")
+	InFormData = In("formData")
+	InBody     = In("body")
+)
+
+// ContentOption configures ContentUnit.
+type ContentOption func(cu *ContentUnit)
+
+// ContentUnit defines HTTP content.
+type ContentUnit struct {
+	Structure    interface{}
+	ContentType  string
+	Format       string
+	HTTPStatus   int
+	Description  string
+	fieldMapping map[In]map[string]string
+}
+
+// SetFieldMapping sets custom field mapping.
+func (c *ContentUnit) SetFieldMapping(in In, fieldToParamName map[string]string) {
+	if len(fieldToParamName) == 0 {
+		return
+	}
+
+	if c.fieldMapping == nil {
+		c.fieldMapping = make(map[In]map[string]string)
+	}
+
+	c.fieldMapping[in] = fieldToParamName
+}
+
+// FieldMapping returns custom field mapping.
+func (c ContentUnit) FieldMapping(in In) map[string]string {
+	return c.fieldMapping[in]
+}
+
+// OperationContext defines operation and processing state.
+type OperationContext interface {
+	OperationInfo
+	OperationState
+
+	Method() string
+	PathPattern() string
+
+	Request() []ContentUnit
+	Response() []ContentUnit
+
+	AddReqStructure(i interface{}, options ...ContentOption)
+	AddRespStructure(o interface{}, options ...ContentOption)
+
+	UnknownParamsAreForbidden(in In) bool
+}
+
+// OperationInfo extends OperationContext with general information.
+type OperationInfo interface {
+	SetTags(tags ...string)
+	SetIsDeprecated(isDeprecated bool)
+	SetSummary(summary string)
+	SetDescription(description string)
+	SetID(operationID string)
+
+	AddSecurity(securityName string, scopes ...string)
+}
+
+// OperationState extends OperationContext with processing state information.
+type OperationState interface {
+	IsProcessingResponse() bool
+	ProcessingIn() In
+
+	SetIsProcessingResponse(bool)
+	SetProcessingIn(in In)
+}
+
+type ocCtxKey struct{}
+
+// WithOperationCtx is a jsonschema.ReflectContext option.
+func WithOperationCtx(oc OperationContext, isProcessingResponse bool, in In) func(rc *jsonschema.ReflectContext) {
+	return func(rc *jsonschema.ReflectContext) {
+		oc.SetIsProcessingResponse(isProcessingResponse)
+		oc.SetProcessingIn(in)
+
+		rc.Context = context.WithValue(rc.Context, ocCtxKey{}, oc)
+	}
+}
+
+// OperationCtx retrieves operation context from reflect context.
+func OperationCtx(rc *jsonschema.ReflectContext) (OperationContext, bool) {
+	if oc, ok := rc.Value(ocCtxKey{}).(OperationContext); ok {
+		return oc, true
+	}
+
+	return nil, false
+}
+
+var regexFindPathParameter = regexp.MustCompile(`{([^}:]+)(:[^}]+)?(?:})`)
+
+// SanitizeMethodPath validates method and parses path element names.
+func SanitizeMethodPath(method, pathPattern string) (cleanMethod string, cleanPath string, pathParams []string, err error) {
+	method = strings.ToLower(method)
+	pathParametersSubmatches := regexFindPathParameter.FindAllStringSubmatch(pathPattern, -1)
+
+	switch method {
+	case "get", "put", "post", "delete", "options", "head", "patch", "trace":
+		break
+	default:
+		return "", "", nil, fmt.Errorf("unexpected http method: %s", method)
+	}
+
+	if len(pathParametersSubmatches) > 0 {
+		for _, submatch := range pathParametersSubmatches {
+			pathParams = append(pathParams, submatch[1])
+
+			if submatch[2] != "" { // Remove gorilla.Mux-style regexp in path.
+				pathPattern = strings.Replace(pathPattern, submatch[0], "{"+submatch[1]+"}", 1)
+			}
+		}
+	}
+
+	return method, pathPattern, pathParams, nil
+}
diff --git a/reflector.go b/reflector.go
new file mode 100644
index 0000000..fa1790f
--- /dev/null
+++ b/reflector.go
@@ -0,0 +1,21 @@
+package openapi
+
+import "github.com/swaggest/jsonschema-go"
+
+// Reflector defines OpenAPI reflector behavior.
+type Reflector interface {
+	JSONSchemaWalker
+
+	NewOperationContext(method, pathPattern string) (OperationContext, error)
+	AddOperation(oc OperationContext) error
+}
+
+// JSONSchemaCallback is a user function called by JSONSchemaWalker.
+type JSONSchemaCallback func(in In, paramName string, schema *jsonschema.SchemaOrBool, required bool) error
+
+// JSONSchemaWalker can extract JSON schemas (for example, for validation purposes) from a ContentUnit.
+type JSONSchemaWalker interface {
+	ResolveJSONSchemaRef(ref string) (s jsonschema.SchemaOrBool, found bool)
+	WalkRequestJSONSchemas(method string, cu ContentUnit, cb JSONSchemaCallback, done func(oc OperationContext)) error
+	WalkResponseJSONSchemas(cu ContentUnit, cb JSONSchemaCallback, done func(oc OperationContext)) error
+}


Test patch:
diff --git a/openapi3/_testdata/openapi.json b/openapi3/_testdata/openapi.json
index 5140cba..21a959b 100644
--- a/openapi3/_testdata/openapi.json
+++ b/openapi3/_testdata/openapi.json
@@ -81,12 +81,6 @@
      },
      "409":{
       "description":"Conflict",
-      "headers":{
-       "X-Header-Field":{
-        "style":"simple","description":"Sample header response.",
-        "schema":{"type":"string","description":"Sample header response."}
-       }
-      },
       "content":{
        "application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/Openapi3TestResp"}}},
        "text/html":{"schema":{"type":"string"}}
diff --git a/openapi3/_testdata/swgui/go.mod b/openapi3/_testdata/swgui/go.mod
new file mode 100644
index 0000000..e1e6160
--- /dev/null
+++ b/openapi3/_testdata/swgui/go.mod
@@ -0,0 +1,11 @@
+module swgui
+
+go 1.19
+
+require github.com/swaggest/swgui v1.6.4
+
+require (
+	github.com/shurcooL/httpgzip v0.0.0-20190720172056-320755c1c1b0 // indirect
+	golang.org/x/net v0.8.0 // indirect
+	golang.org/x/text v0.8.0 // indirect
+)
diff --git a/openapi3/_testdata/swgui/go.sum b/openapi3/_testdata/swgui/go.sum
new file mode 100644
index 0000000..54f062e
--- /dev/null
+++ b/openapi3/_testdata/swgui/go.sum
@@ -0,0 +1,10 @@
+github.com/bool64/dev v0.2.29 h1:x+syGyh+0eWtOzQ1ItvLzOGIWyNWnyjXpHIcpF2HvL4=
+github.com/shurcooL/httpgzip v0.0.0-20190720172056-320755c1c1b0 h1:mj/nMDAwTBiaCqMEs4cYCqF7pO6Np7vhy1D1wcQGz+E=
+github.com/shurcooL/httpgzip v0.0.0-20190720172056-320755c1c1b0/go.mod h1:919LwcH0M7/W4fcZ0/jy0qGght1GIhqyS/EgWGH2j5Q=
+github.com/swaggest/swgui v1.6.4 h1:9G7HTUMOAu/Y9YF2wDvoq9GXFlV4BH5y8BSOl4MWfl8=
+github.com/swaggest/swgui v1.6.4/go.mod h1:xsfGb4NtnBspBXKXtlPdVrvoqzCIZ338Aj3tHNikz2Q=
+golang.org/x/net v0.8.0 h1:Zrh2ngAOFYneWTAIAPethzeaQLuHwhuBkuV6ZiRnUaQ=
+golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc=
+golang.org/x/text v0.8.0 h1:57P1ETyNKtuIjB4SRd15iJxuhj8Gc416Y78H3qgMh68=
+golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
+golang.org/x/tools v0.6.0 h1:BOw41kyTf3PuCW1pVQf8+Cyg8pMlkYB1oo9iJ6D/lKM=
diff --git a/openapi3/_testdata/swgui/swgui.go b/openapi3/_testdata/swgui/swgui.go
index f3c352f..a5d9193 100644
--- a/openapi3/_testdata/swgui/swgui.go
+++ b/openapi3/_testdata/swgui/swgui.go
@@ -1,18 +1,18 @@
 package main
 
 import (
-	"io/ioutil"
 	"log"
 	"net/http"
+	"os"
 
-	v3 "github.com/swaggest/swgui/v3"
+	swgui "github.com/swaggest/swgui/v5"
 )
 
 func main() {
-	h := v3.NewHandler("Foo", "/openapi.json", "/")
+	h := swgui.NewHandler("Foo", "/openapi.json", "/")
 	hh := http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
 		if r.URL.Path == "/openapi.json" {
-			o, err := ioutil.ReadFile("openapi3/_testdata/openapi.json")
+			o, err := os.ReadFile("../openapi.json")
 			if err != nil {
 				http.Error(rw, err.Error(), 500)
 				return
@@ -25,5 +25,5 @@ func main() {
 		h.ServeHTTP(rw, r)
 	})
 	log.Println("Starting Swagger UI server at http://localhost:8082/")
-	http.ListenAndServe(":8082", hh)
+	_ = http.ListenAndServe("localhost:8082", hh)
 }
diff --git a/openapi3/example_misc_test.go b/openapi3/example_misc_test.go
index a1ded54..3337482 100644
--- a/openapi3/example_misc_test.go
+++ b/openapi3/example_misc_test.go
@@ -26,13 +26,10 @@ func ExampleReflector_options() {
 		Foo []int `json:"foo"`
 	}
 
-	oc := openapi3.OperationContext{
-		Operation: &openapi3.Operation{},
-		Input:     new(req),
-	}
+	oc, _ := r.NewOperationContext(http.MethodPost, "/foo")
+	oc.AddReqStructure(new(req))
 
-	_ = r.SetupRequest(oc)
-	_ = r.SpecEns().AddOperation(http.MethodGet, "/foo", *oc.Operation)
+	_ = r.AddOperation(oc)
 
 	j, _ := assertjson.MarshalIndentCompact(r.Spec, "", " ", 120)
 
@@ -43,7 +40,7 @@ func ExampleReflector_options() {
 	//  "openapi":"3.0.3","info":{"title":"","version":""},
 	//  "paths":{
 	//   "/foo":{
-	//    "get":{
+	//    "post":{
 	//     "requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Openapi3TestReq"}}}},
 	//     "responses":{"204":{"description":"No Content"}}
 	//    }
diff --git a/openapi3/example_security_test.go b/openapi3/example_security_test.go
index db937ef..1129de6 100644
--- a/openapi3/example_security_test.go
+++ b/openapi3/example_security_test.go
@@ -5,10 +5,11 @@ import (
 	"log"
 	"net/http"
 
+	"github.com/swaggest/openapi-go"
 	"github.com/swaggest/openapi-go/openapi3"
 )
 
-func ExampleReflector_SetJSONResponse_http_basic_auth() {
+func ExampleReflector_AddOperation_http_basic_auth() {
 	reflector := openapi3.Reflector{}
 	securityName := "admin"
 
@@ -22,21 +23,23 @@ func ExampleReflector_SetJSONResponse_http_basic_auth() {
 		},
 	)
 
-	op := openapi3.Operation{}
-	_ = reflector.SetJSONResponse(&op, struct {
+	oc, _ := reflector.NewOperationContext(http.MethodGet, "/secure")
+	oc.AddRespStructure(struct {
 		Secret string `json:"secret"`
-	}{}, http.StatusOK)
+	}{})
 
 	// Add security requirement to operation.
-	op.Security = append(op.Security, map[string][]string{securityName: {}})
+	oc.AddSecurity(securityName)
 
 	// Describe unauthorized response.
-	_ = reflector.SetJSONResponse(&op, struct {
+	oc.AddRespStructure(struct {
 		Error string `json:"error"`
-	}{}, http.StatusUnauthorized)
+	}{}, func(cu *openapi.ContentUnit) {
+		cu.HTTPStatus = http.StatusUnauthorized
+	})
 
 	// Add operation to schema.
-	_ = reflector.SpecEns().AddOperation(http.MethodGet, "/secure", op)
+	_ = reflector.AddOperation(oc)
 
 	schema, err := reflector.Spec.MarshalYAML()
 	if err != nil {
@@ -82,7 +85,7 @@ func ExampleReflector_SetJSONResponse_http_basic_auth() {
 	//       type: http
 }
 
-func ExampleReflector_SetJSONResponse_api_key_auth() {
+func ExampleReflector_AddOperation_api_key_auth() {
 	reflector := openapi3.Reflector{}
 	securityName := "api_key"
 
@@ -99,21 +102,23 @@ func ExampleReflector_SetJSONResponse_api_key_auth() {
 		},
 	)
 
-	op := openapi3.Operation{}
-	_ = reflector.SetJSONResponse(&op, struct {
+	oc, _ := reflector.NewOperationContext(http.MethodGet, "/secure")
+	oc.AddRespStructure(struct {
 		Secret string `json:"secret"`
-	}{}, http.StatusOK)
+	}{})
 
 	// Add security requirement to operation.
-	op.Security = append(op.Security, map[string][]string{securityName: {}})
+	oc.AddSecurity(securityName)
 
 	// Describe unauthorized response.
-	_ = reflector.SetJSONResponse(&op, struct {
+	oc.AddRespStructure(struct {
 		Error string `json:"error"`
-	}{}, http.StatusUnauthorized)
+	}{}, func(cu *openapi.ContentUnit) {
+		cu.HTTPStatus = http.StatusUnauthorized
+	})
 
 	// Add operation to schema.
-	_ = reflector.SpecEns().AddOperation(http.MethodGet, "/secure", op)
+	_ = reflector.AddOperation(oc)
 
 	schema, err := reflector.Spec.MarshalYAML()
 	if err != nil {
@@ -160,7 +165,7 @@ func ExampleReflector_SetJSONResponse_api_key_auth() {
 	//       type: apiKey
 }
 
-func ExampleReflector_SetJSONResponse_http_bearer_token_auth() {
+func ExampleReflector_AddOperation_http_bearer_token_auth() {
 	reflector := openapi3.Reflector{}
 	securityName := "bearer_token"
 
@@ -177,21 +182,23 @@ func ExampleReflector_SetJSONResponse_http_bearer_token_auth() {
 		},
 	)
 
-	op := openapi3.Operation{}
-	_ = reflector.SetJSONResponse(&op, struct {
+	oc, _ := reflector.NewOperationContext(http.MethodGet, "/secure")
+	oc.AddRespStructure(struct {
 		Secret string `json:"secret"`
-	}{}, http.StatusOK)
+	}{})
 
 	// Add security requirement to operation.
-	op.Security = append(op.Security, map[string][]string{securityName: {}})
+	oc.AddSecurity(securityName)
 
 	// Describe unauthorized response.
-	_ = reflector.SetJSONResponse(&op, struct {
+	oc.AddRespStructure(struct {
 		Error string `json:"error"`
-	}{}, http.StatusUnauthorized)
+	}{}, func(cu *openapi.ContentUnit) {
+		cu.HTTPStatus = http.StatusUnauthorized
+	})
 
 	// Add operation to schema.
-	_ = reflector.SpecEns().AddOperation(http.MethodGet, "/secure", op)
+	_ = reflector.AddOperation(oc)
 
 	schema, err := reflector.Spec.MarshalYAML()
 	if err != nil {
diff --git a/openapi3/helper_test.go b/openapi3/helper_test.go
index 04630f3..d1cf611 100644
--- a/openapi3/helper_test.go
+++ b/openapi3/helper_test.go
@@ -29,7 +29,7 @@ func TestSpec_SetOperation(t *testing.T) {
 	assert.NoError(t, s.AddOperation(http.MethodGet, "/{foo}", op))
 
 	assert.EqualError(t, s.AddOperation(http.MethodGet, "/{foo}", op),
-		"operation with method and path already exists")
+		"operation already exists: get /{foo}")
 
 	op.WithParameters(
 		openapi3.Parameter{In: openapi3.ParameterInPath, Name: "foo"}.ToParameterOrRef(),
diff --git a/openapi3/reflect_deprecated_test.go b/openapi3/reflect_deprecated_test.go
new file mode 100644
index 0000000..af67140
--- /dev/null
+++ b/openapi3/reflect_deprecated_test.go
@@ -0,0 +1,864 @@
+package openapi3_test
+
+import (
+	"mime/multipart"
+	"net/http"
+	"os"
+	"strconv"
+	"testing"
+
+	"github.com/stretchr/testify/assert"
+	"github.com/stretchr/testify/require"
+	"github.com/swaggest/assertjson"
+	"github.com/swaggest/jsonschema-go"
+	"github.com/swaggest/openapi-go/openapi3"
+)
+
+func TestReflector_SetRequest_array(t *testing.T) {
+	reflector := openapi3.Reflector{}
+
+	s := reflector.SpecEns()
+	s.Info.Title = apiName
+	s.Info.Version = apiVersion
+
+	op := openapi3.Operation{}
+
+	err := reflector.SetRequest(&op, new([]GetReq), http.MethodPost)
+	assert.NoError(t, err)
+
+	require.NoError(t, s.AddOperation(http.MethodPost, "/somewhere", op))
+
+	b, err := assertjson.MarshalIndentCompact(s, "", " ", 120)
+	assert.NoError(t, err)
+
+	require.NoError(t, os.WriteFile("_testdata/openapi_req_array_last_run.json", b, 0o600))
+
+	expected, err := os.ReadFile("_testdata/openapi_req_array.json")
+	require.NoError(t, err)
+
+	assertjson.Equal(t, expected, b)
+}
+
+func TestReflector_SetRequest_uploadInterface(t *testing.T) {
+	type req struct {
+		File1 multipart.File `formData:"upload1"`
+	}
+
+	reflector := openapi3.Reflector{}
+	s := reflector.SpecEns()
+	op := openapi3.Operation{}
+
+	err := reflector.SetRequest(&op, new(req), http.MethodPost)
+	assert.NoError(t, err)
+
+	require.NoError(t, s.AddOperation(http.MethodPost, "/somewhere", op))
+
+	assertjson.EqMarshal(t, `{
+	  "openapi":"3.0.3","info":{"title":"","version":""},
+	  "paths":{
+		"/somewhere":{
+		  "post":{
+			"requestBody":{
+			  "content":{
+				"multipart/form-data":{"schema":{"$ref":"#/components/schemas/FormDataOpenapi3TestReq"}}
+			  }
+			},
+			"responses":{"204":{"description":"No Content"}}
+		  }
+		}
+	  },
+	  "components":{
+		"schemas":{
+		  "FormDataMultipartFile":{"type":"string","format":"binary","nullable":true},
+		  "FormDataOpenapi3TestReq":{
+			"type":"object",
+			"properties":{"upload1":{"$ref":"#/components/schemas/FormDataMultipartFile"}}
+		  }
+		}
+	  }
+	}`, s)
+}
+
+func TestReflector_SetRequest(t *testing.T) {
+	reflector := openapi3.Reflector{}
+
+	s := reflector.SpecEns()
+	s.Info.Title = apiName
+	s.Info.Version = apiVersion
+
+	op := openapi3.Operation{}
+
+	err := reflector.SetRequest(&op, new(GetReq), http.MethodGet)
+	assert.NoError(t, err)
+
+	require.NoError(t, s.AddOperation(http.MethodGet, "/somewhere/{in_path}", op))
+
+	b, err := assertjson.MarshalIndentCompact(s, "", " ", 120)
+	assert.NoError(t, err)
+
+	require.NoError(t, os.WriteFile("_testdata/openapi_req_last_run.json", b, 0o600))
+
+	expected, err := os.ReadFile("_testdata/openapi_req.json")
+	require.NoError(t, err)
+
+	assertjson.Equal(t, expected, b)
+}
+
+func TestReflector_SetJSONResponse(t *testing.T) {
+	reflector := openapi3.Reflector{}
+
+	s := reflector.SpecEns()
+	s.Info.Title = apiName
+	s.Info.Version = apiVersion
+
+	reflector.AddTypeMapping(new(WeirdResp), new(Resp))
+
+	op := openapi3.Operation{}
+
+	assert.NoError(t, reflector.SetRequest(&op, new(Req), http.MethodPost))
+	assert.NoError(t, reflector.SetJSONResponse(&op, new(WeirdResp), http.StatusOK))
+	assert.NoError(t, reflector.SetJSONResponse(&op, new([]WeirdResp), http.StatusConflict))
+	assert.NoError(t, reflector.SetStringResponse(&op, http.StatusConflict, "text/html"))
+
+	pathItem := openapi3.PathItem{}
+	pathItem.
+		WithSummary("Path Summary").
+		WithDescription("Path Description")
+	s.Paths.WithMapOfPathItemValuesItem("/somewhere/{in_path}", pathItem)
+
+	js := op.RequestBody.RequestBody.Content["multipart/form-data"].Schema.ToJSONSchema(s)
+	expected, err := os.ReadFile("_testdata/req_schema.json")
+	require.NoError(t, err)
+	assertjson.EqualMarshal(t, expected, js)
+
+	assert.NoError(t, s.AddOperation(http.MethodPost, "/somewhere/{in_path}", op))
+
+	op = openapi3.Operation{}
+
+	assert.NoError(t, reflector.SetRequest(&op, new(GetReq), http.MethodGet))
+	assert.NoError(t, reflector.SetJSONResponse(&op, new(Resp), http.StatusOK))
+	assert.NoError(t, s.AddOperation(http.MethodGet, "/somewhere/{in_path}", op))
+
+	js = op.Responses.MapOfResponseOrRefValues[strconv.Itoa(http.StatusOK)].Response.Content["application/json"].
+		Schema.ToJSONSchema(s)
+	jsb, err := assertjson.MarshalIndentCompact(js, "", " ", 120)
+	require.NoError(t, err)
+
+	require.NoError(t, os.WriteFile("_testdata/resp_schema_last_run.json", jsb, 0o600))
+
+	expected, err = os.ReadFile("_testdata/resp_schema.json")
+	require.NoError(t, err)
+	assertjson.EqualMarshal(t, expected, js)
+
+	js = op.Responses.MapOfResponseOrRefValues[strconv.Itoa(http.StatusOK)].Response.Headers["X-Header-Field"].Header.
+		Schema.ToJSONSchema(s)
+	assertjson.EqMarshal(t, `{"type": "string", "description": "Sample header response."}`, js)
+
+	require.NoError(t, err)
+	assertjson.EqMarshal(t, `{"type": "integer", "description": "Query parameter."}`,
+		op.Parameters[0].Parameter.Schema.ToJSONSchema(s))
+
+	b, err := assertjson.MarshalIndentCompact(s, "", " ", 120)
+	assert.NoError(t, err)
+
+	require.NoError(t, os.WriteFile("_testdata/openapi_last_run.json", b, 0o600))
+
+	expected, err = os.ReadFile("_testdata/openapi.json")
+	require.NoError(t, err)
+
+	assertjson.EqualMarshal(t, expected, s)
+}
+
+func TestReflector_SetRequest_pathParamAndBody(t *testing.T) {
+	reflector := openapi3.Reflector{}
+	op := openapi3.Operation{}
+
+	err := reflector.SetRequest(&op, new(PathParamAndBody), http.MethodPost)
+	assert.NoError(t, err)
+
+	s := reflector.SpecEns()
+	s.Info.Title = apiName
+	s.Info.Version = apiVersion
+	assert.NoError(t, s.AddOperation(http.MethodPost, "/somewhere/{id}", op))
+
+	assertjson.EqMarshal(t, `{
+	 "openapi":"3.0.3","info":{"title":"SampleAPI","version":"1.2.3"},
+	 "paths":{
+	  "/somewhere/{id}":{
+	   "post":{
+		"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],
+		"requestBody":{
+		 "content":{"application/json":{"schema":{"$ref":"#/components/schemas/Openapi3TestPathParamAndBody"}}}
+		},
+		"responses":{"204":{"description":"No Content"}}
+	   }
+	  }
+	 },
+	 "components":{
+	  "schemas":{"Openapi3TestPathParamAndBody":{"type":"array","items":{"type":"string"},"nullable":true}}
+	 }
+	}`, s)
+}
+
+func TestRequestBodyEnforcer(t *testing.T) {
+	reflector := openapi3.Reflector{}
+	op := openapi3.Operation{}
+
+	err := reflector.SetRequest(&op, new(WithReqBody), http.MethodGet)
+	assert.NoError(t, err)
+
+	s := reflector.SpecEns()
+	s.Info.Title = apiName
+	s.Info.Version = apiVersion
+	assert.NoError(t, s.AddOperation(http.MethodGet, "/somewhere/{id}", op))
+
+	assertjson.EqMarshal(t, `{
+	 "openapi":"3.0.3","info":{"title":"SampleAPI","version":"1.2.3"},
+	 "paths":{
+	  "/somewhere/{id}":{
+	   "get":{
+		"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],
+		"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Openapi3TestWithReqBody"}}}},
+		"responses":{"204":{"description":"No Content"}}
+	   }
+	  }
+	 },
+	 "components":{"schemas":{"Openapi3TestWithReqBody":{"type":"array","items":{"type":"string"},"nullable":true}}}
+	}`, s)
+}
+
+func TestReflector_SetupResponse(t *testing.T) {
+	reflector := openapi3.Reflector{}
+	op := openapi3.Operation{}
+
+	s := reflector.SpecEns()
+	s.Info.Title = apiName
+	s.Info.Version = apiVersion
+
+	require.NoError(t, reflector.SetupResponse(openapi3.OperationContext{
+		Operation:       &op,
+		RespContentType: "text/csv; charset=utf-8",
+		HTTPStatus:      http.StatusNoContent,
+		Output: new(struct {
+			Val1 int
+			Val2 string
+		}),
+		RespHeaderMapping: map[string]string{
+			"Val1": "X-Value-1",
+			"Val2": "X-Value-2",
+		},
+	}))
+	require.NoError(t, s.AddOperation(http.MethodGet, "/somewhere", op))
+
+	assertjson.EqMarshal(t, `{
+	 "openapi":"3.0.3","info":{"title":"SampleAPI","version":"1.2.3"},
+	 "paths":{
+	  "/somewhere":{
+	   "get":{
+		"responses":{
+		 "204":{
+		  "description":"No Content",
+		  "headers":{
+		   "X-Value-1":{"style":"simple","schema":{"type":"integer"}},
+		   "X-Value-2":{"style":"simple","schema":{"type":"string"}}
+		  },
+		  "content":{"text/csv":{"schema":{"type":"string"}}}
+		 }
+		}
+	   }
+	  }
+	 }
+	}`, s)
+}
+
+func TestReflector_SetupRequest(t *testing.T) {
+	reflector := openapi3.Reflector{}
+	op := openapi3.Operation{}
+
+	s := reflector.SpecEns()
+	s.Info.Title = apiName
+	s.Info.Version = apiVersion
+
+	require.NoError(t, reflector.SetupRequest(openapi3.OperationContext{
+		Operation:  &op,
+		HTTPMethod: http.MethodPost,
+		Input: new(struct {
+			Val1 int
+			Val2 string
+			Val3 float64
+			Val4 bool
+			Val5 string
+			Val6 multipart.File
+		}),
+		ReqHeaderMapping: map[string]string{
+			"Val1": "X-Value-1",
+		},
+		ReqQueryMapping: map[string]string{
+			"Val2": "value_2",
+		},
+		ReqFormDataMapping: map[string]string{
+			"Val3": "value3",
+			"Val6": "upload6",
+		},
+		ReqPathMapping: map[string]string{
+			"Val4": "value-4",
+		},
+		ReqCookieMapping: map[string]string{
+			"Val5": "value_5",
+		},
+	}))
+	require.NoError(t, s.AddOperation(http.MethodPost, "/somewhere/{value-4}", op))
+
+	assertjson.EqMarshal(t, `{
+	 "openapi":"3.0.3","info":{"title":"SampleAPI","version":"1.2.3"},
+	 "paths":{
+	  "/somewhere/{value-4}":{
+	   "post":{
+		"parameters":[
+		 {"name":"value_2","in":"query","schema":{"type":"string"}},
+		 {"name":"value-4","in":"path","required":true,"schema":{"type":"boolean"}},
+		 {"name":"value_5","in":"cookie","schema":{"type":"string"}},
+		 {"name":"X-Value-1","in":"header","schema":{"type":"integer"}}
+		],
+		"requestBody":{
+		 "content":{
+		  "multipart/form-data":{
+		   "schema":{
+			"type":"object",
+			"properties":{"upload6":{"$ref":"#/components/schemas/FormDataMultipartFile"},"value3":{"type":"number"}}
+		   }
+		  }
+		 }
+		},
+		"responses":{"204":{"description":"No Content"}}
+	   }
+	  }
+	 },
+	 "components":{"schemas":{"FormDataMultipartFile":{"type":"string","format":"binary","nullable":true}}}
+	}`, s)
+}
+
+func TestReflector_SetupRequest_queryObject(t *testing.T) {
+	reflector := openapi3.Reflector{}
+	op := openapi3.Operation{}
+
+	s := reflector.SpecEns()
+	s.Info.Title = apiName
+	s.Info.Version = apiVersion
+
+	require.NoError(t, reflector.SetupRequest(openapi3.OperationContext{
+		Operation:  &op,
+		HTTPMethod: http.MethodGet,
+		Input: new(struct {
+			InQuery map[int]float64 `query:"in_query"`
+		}),
+	}))
+	require.NoError(t, s.AddOperation(http.MethodPost, "/somewhere", op))
+
+	assertjson.EqMarshal(t, `{
+	 "openapi":"3.0.3","info":{"title":"SampleAPI","version":"1.2.3"},
+	 "paths":{
+	  "/somewhere":{
+	   "post":{
+		"parameters":[
+		 {
+		  "name":"in_query","in":"query","style":"deepObject","explode":true,
+		  "schema":{"type":"object","additionalProperties":{"type":"number"}}
+		 }
+		],
+		"responses":{"204":{"description":"No Content"}}
+	   }
+	  }
+	 }
+	}`, s)
+}
+
+func TestReflector_SetupRequest_queryNamedObject(t *testing.T) {
+	reflector := openapi3.Reflector{}
+	op := openapi3.Operation{}
+
+	s := reflector.SpecEns()
+	s.Info.Title = apiName
+	s.Info.Version = apiVersion
+
+	require.NoError(t, reflector.SetupRequest(openapi3.OperationContext{
+		Operation:  &op,
+		HTTPMethod: http.MethodGet,
+		Input:      new(namedType),
+	}))
+	require.NoError(t, s.AddOperation(http.MethodPost, "/somewhere", op))
+
+	assertjson.EqMarshal(t, `{
+	  "openapi":"3.0.3","info":{"title":"SampleAPI","version":"1.2.3"},
+	  "paths":{
+		"/somewhere":{
+		  "post":{
+			"parameters":[
+			  {
+				"name":"in_query","in":"query","style":"deepObject","explode":true,
+				"schema":{"$ref":"#/components/schemas/QueryOpenapi3TestLabels"}
+			  }
+			],
+			"responses":{"204":{"description":"No Content"}}
+		  }
+		}
+	  },
+	  "components":{
+		"schemas":{
+		  "QueryOpenapi3TestLabels":{"type":"object","additionalProperties":{"type":"number"}}
+		}
+	  }
+	}`, s)
+
+	js, found := reflector.ResolveJSONSchemaRef("#/components/schemas/QueryOpenapi3TestLabels")
+	assert.True(t, found)
+	assertjson.EqMarshal(t, `{"type":"object","additionalProperties":{"type":"number"}}`, js)
+}
+
+func TestReflector_SetupRequest_jsonQuery(t *testing.T) {
+	type filter struct {
+		Labels []string `json:"labels,omitempty"`
+		Type   string   `json:"type"`
+	}
+
+	type req struct {
+		One   filter         `query:"one"`
+		Two   filter         `query:"two"`
+		Three map[string]int `query:"three"`
+	}
+
+	r := openapi3.Reflector{}
+	oc := openapi3.OperationContext{
+		Operation: &openapi3.Operation{},
+		Input:     new(req),
+	}
+
+	require.NoError(t, r.SetupRequest(oc))
+	require.NoError(t, r.SpecEns().AddOperation(http.MethodGet, "/", *oc.Operation))
+
+	assertjson.EqMarshal(t, `{
+	  "openapi":"3.0.3","info":{"title":"","version":""},
+	  "paths":{
+		"/":{
+		  "get":{
+			"parameters":[
+			  {
+				"name":"one","in":"query",
+				"content":{
+				  "application/json":{
+					"schema":{"$ref":"#/components/schemas/QueryOpenapi3TestFilter"}
+				  }
+				}
+			  },
+			  {
+				"name":"two","in":"query",
+				"content":{
+				  "application/json":{
+					"schema":{"$ref":"#/components/schemas/QueryOpenapi3TestFilter"}
+				  }
+				}
+			  },
+			  {
+				"name":"three","in":"query","style":"deepObject","explode":true,
+				"schema":{"type":"object","additionalProperties":{"type":"integer"}}
+			  }
+			],
+			"responses":{"204":{"description":"No Content"}}
+		  }
+		}
+	  },
+	  "components":{
+		"schemas":{
+		  "QueryOpenapi3TestFilter":{
+			"type":"object",
+			"properties":{
+			  "labels":{"type":"array","items":{"type":"string"}},
+			  "type":{"type":"string"}
+			}
+		  }
+		}
+	  }
+	}`, r.SpecEns())
+}
+
+func TestReflector_SetupRequest_forbidParams(t *testing.T) {
+	type req struct {
+		Query  string `query:"query"`
+		Header string `header:"header"`
+		Cookie string `cookie:"cookie"`
+		Path   string `path:"path"`
+
+		_ struct{} `query:"_" cookie:"_" additionalProperties:"false"`
+	}
+
+	r := openapi3.Reflector{}
+	oc := openapi3.OperationContext{
+		Operation: &openapi3.Operation{},
+		Input:     new(req),
+	}
+
+	require.NoError(t, r.SetupRequest(oc))
+	require.NoError(t, r.SpecEns().AddOperation(http.MethodGet, "/{path}", *oc.Operation))
+
+	assertjson.EqMarshal(t, `{
+	  "openapi":"3.0.3","info":{"title":"","version":""},
+	  "paths":{
+		"/{path}":{
+		  "get":{
+			"parameters":[
+			  {"name":"query","in":"query","schema":{"type":"string"}},
+			  {
+				"name":"path","in":"path","required":true,
+				"schema":{"type":"string"}
+			  },
+			  {"name":"cookie","in":"cookie","schema":{"type":"string"}},
+			  {"name":"header","in":"header","schema":{"type":"string"}}
+			],
+			"responses":{"204":{"description":"No Content"}},
+			"x-forbid-unknown-cookie":true,"x-forbid-unknown-query":true
+		  }
+		}
+	  }
+	}`, r.SpecEns())
+
+	assert.True(t, oc.Operation.UnknownParamIsForbidden(openapi3.ParameterInCookie))
+	assert.False(t, oc.Operation.UnknownParamIsForbidden(openapi3.ParameterInHeader))
+	assert.True(t, oc.Operation.UnknownParamIsForbidden(openapi3.ParameterInQuery))
+	assert.False(t, oc.Operation.UnknownParamIsForbidden(openapi3.ParameterInPath))
+}
+
+func TestReflector_SetupRequest_noBody(t *testing.T) {
+	type req struct {
+		ID int `json:"id" path:"id"`
+	}
+
+	r := openapi3.Reflector{}
+
+	for _, method := range []string{http.MethodHead, http.MethodGet, http.MethodDelete, http.MethodTrace} {
+		oc := openapi3.OperationContext{
+			Operation:  &openapi3.Operation{},
+			HTTPMethod: method,
+			Input:      new(req),
+		}
+
+		require.NoError(t, r.SetupRequest(oc))
+		assertjson.EqMarshal(t, `{
+		  "parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"integer"}}],
+		  "responses":{}
+		}`, oc.Operation)
+	}
+
+	for _, method := range []string{http.MethodPost, http.MethodPatch, http.MethodPut} {
+		oc := openapi3.OperationContext{
+			Operation:  &openapi3.Operation{},
+			HTTPMethod: method,
+			Input:      new(req),
+		}
+
+		require.NoError(t, r.SetupRequest(oc))
+		assertjson.EqMarshal(t, `{
+		  "parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"integer"}}],
+		  "requestBody":{
+			"content":{
+			  "application/json":{"schema":{"$ref":"#/components/schemas/Openapi3TestReq"}}
+			}
+		  },
+		  "responses":{}
+		}`, oc.Operation)
+	}
+}
+
+func TestOperationCtx(t *testing.T) {
+	type req struct {
+		Query  string `query:"query"`
+		Header string `header:"header"`
+		Cookie string `cookie:"cookie"`
+		Path   string `path:"path"`
+		Body   string `json:"body"`
+	}
+
+	type resp struct {
+		Header string `header:"header"`
+		Body   string `json:"body"`
+	}
+
+	r := openapi3.Reflector{}
+	oc := openapi3.OperationContext{
+		Operation: &openapi3.Operation{},
+		Input:     new(req),
+		Output:    new(resp),
+	}
+
+	visited := map[string]bool{}
+
+	var currentRC *jsonschema.ReflectContext
+
+	r.DefaultOptions = append(r.DefaultOptions,
+		func(rc *jsonschema.ReflectContext) {
+			currentRC = rc
+		},
+		jsonschema.InterceptSchema(func(params jsonschema.InterceptSchemaParams) (stop bool, err error) {
+			if occ, ok := openapi3.OperationCtx(currentRC); ok {
+				if occ.ProcessingResponse {
+					visited["resp:"+occ.ProcessingIn] = true
+				} else {
+					visited["req:"+occ.ProcessingIn] = true
+				}
+			}
+
+			return false, nil
+		}),
+	)
+
+	require.NoError(t, r.SetupRequest(oc))
+	require.NoError(t, r.SetupResponse(oc))
+
+	assert.Equal(t, map[string]bool{
+		"req:body":    true,
+		"req:cookie":  true,
+		"req:header":  true,
+		"req:path":    true,
+		"req:query":   true,
+		"resp:body":   true,
+		"resp:header": true,
+	}, visited)
+}
+
+func TestReflector_SetRequest_formData_with_json(t *testing.T) {
+	// In presence of `formData` tags, `json` tags would be ignored as request body.
+	type req struct {
+		Foo int `formData:"foo" json:"foo"`
+	}
+
+	r := openapi3.Reflector{}
+	oc := openapi3.OperationContext{
+		Operation: &openapi3.Operation{},
+		Input:     new(req),
+	}
+
+	require.NoError(t, r.SetupRequest(oc))
+	require.NoError(t, r.SpecEns().AddOperation(http.MethodGet, "/foo", *oc.Operation))
+
+	assertjson.EqMarshal(t, `{
+	  "openapi":"3.0.3","info":{"title":"","version":""},
+	  "paths":{
+		"/foo":{
+		  "get":{
+			"requestBody":{
+			  "content":{
+				"application/x-www-form-urlencoded":{"schema":{"$ref":"#/components/schemas/FormDataOpenapi3TestReq"}}
+			  }
+			},
+			"responses":{"204":{"description":"No Content"}}
+		  }
+		}
+	  },
+	  "components":{
+		"schemas":{
+		  "FormDataOpenapi3TestReq":{"type":"object","properties":{"foo":{"type":"integer"}}}
+		}
+	  }
+	}`, r.SpecEns())
+}
+
+func TestReflector_SetupRequest_form(t *testing.T) {
+	// Fields with `form` will be present as both `query` parameter and `formData` property.
+	type req struct {
+		Foo  int    `formData:"foo"`
+		Bar  int    `form:"bar"`
+		Baz  string `query:"baz"`
+		Quux string `query:"quux" form:"ignored"`
+	}
+
+	r := openapi3.Reflector{}
+	oc := openapi3.OperationContext{
+		Operation: &openapi3.Operation{},
+		Input:     req{},
+	}
+
+	require.NoError(t, r.SetupRequest(oc))
+	require.NoError(t, r.SpecEns().AddOperation(http.MethodPost, "/foo", *oc.Operation))
+
+	assertjson.EqMarshal(t, `{
+	  "openapi":"3.0.3","info":{"title":"","version":""},
+	  "paths":{
+		"/foo":{
+		  "post":{
+			"parameters":[
+			  {"name":"bar","in":"query","schema":{"type":"integer"}},
+			  {"name":"baz","in":"query","schema":{"type":"string"}},
+			  {"name":"quux","in":"query","schema":{"type":"string"}}
+			],
+			"requestBody":{
+			  "content":{
+				"application/x-www-form-urlencoded":{"schema":{"$ref":"#/components/schemas/FormDataOpenapi3TestReq"}}
+			  }
+			},
+			"responses":{"204":{"description":"No Content"}}
+		  }
+		}
+	  },
+	  "components":{
+		"schemas":{
+		  "FormDataOpenapi3TestReq":{
+			"type":"object",
+			"properties":{
+			  "bar":{"type":"integer"},"foo":{"type":"integer"},
+			  "ignored":{"type":"string"}
+			}
+		  }
+		}
+	  }
+	}`, r.SpecEns())
+}
+
+func TestReflector_SetupRequest_form_only(t *testing.T) {
+	// Fields with `form` will be present as both `query` parameter and `formData` property.
+	type req struct {
+		Bar  int    `form:"bar"`
+		Quux string `form:"quux"`
+	}
+
+	r := openapi3.Reflector{}
+	oc := openapi3.OperationContext{
+		Operation: &openapi3.Operation{},
+		Input:     req{},
+	}
+
+	require.NoError(t, r.SetupRequest(oc))
+	require.NoError(t, r.SpecEns().AddOperation(http.MethodPost, "/foo", *oc.Operation))
+
+	assertjson.EqMarshal(t, `{
+	  "openapi":"3.0.3","info":{"title":"","version":""},
+	  "paths":{
+		"/foo":{
+		  "post":{
+			"parameters":[
+			  {"name":"bar","in":"query","schema":{"type":"integer"}},
+			  {"name":"quux","in":"query","schema":{"type":"string"}}
+			],
+			"requestBody":{
+			  "content":{
+				"application/x-www-form-urlencoded":{"schema":{"$ref":"#/components/schemas/FormDataOpenapi3TestReq"}}
+			  }
+			},
+			"responses":{"204":{"description":"No Content"}}
+		  }
+		}
+	  },
+	  "components":{
+		"schemas":{
+		  "FormDataOpenapi3TestReq":{
+			"type":"object",
+			"properties":{"bar":{"type":"integer"},"quux":{"type":"string"}}
+		  }
+		}
+	  }
+	}`, r.SpecEns())
+}
+
+func TestReflector_SetRequest_queryObject(t *testing.T) {
+	reflector := openapi3.Reflector{}
+
+	// JSON object is only enabled when at least one `json` tag is available on top-level property,
+	// and there are no `in` tags, e.g. `query`.
+	type jsonFilter struct {
+		Foo    string `json:"foo"`
+		Bar    int    `json:"bar"`
+		Deeper struct {
+			Val string `json:"val"`
+		} `json:"deeper"`
+	}
+
+	// Deep object structure may have `json` tags, they are ignored in presence of
+	// at least one top-level field with a matching `in` tag, e.g. `query`.
+	type deepObjectFilter struct {
+		Baz    bool    `json:"baz" query:"baz"`
+		Quux   float64 `json:"quux" query:"quux"`
+		Deeper struct {
+			Val string `query:"val"`
+		} `query:"deeper"`
+	}
+
+	type EmbeddedParams struct {
+		Embedded string `query:"embedded"`
+	}
+
+	type DeeplyEmbedded struct {
+		EmbeddedParams
+	}
+
+	type req struct {
+		// Simple scalar parameters.
+		ID     string `path:"id" example:"XXX-XXXXX"`
+		Locale string `query:"locale" pattern:"^[a-z]{2}-[A-Z]{2}$"`
+		// Embedded types are expanded into top-level scope.
+		DeeplyEmbedded
+		// Object values can be serialized in JSON (with json field tags in the value struct).
+		JSONFilter jsonFilter `query:"json_filter"`
+		// Or as deepObject (with same field tag as parent, .e.g query).
+		DeepObjectFilter deepObjectFilter `query:"deep_object_filter"`
+		// JSON body tags are ignored for GET request by default.
+		Amount uint `json:"amount"`
+	}
+
+	getOp := openapi3.Operation{}
+
+	handleError(reflector.SetRequest(&getOp, new(req), http.MethodGet))
+	handleError(reflector.Spec.AddOperation(http.MethodGet, "/things/{id}", getOp))
+
+	assertjson.EqMarshal(t, `{
+	  "openapi":"3.0.3","info":{"title":"","version":""},
+	  "paths":{
+		"/things/{id}":{
+		  "get":{
+			"parameters":[
+			  {
+				"name":"locale","in":"query",
+				"schema":{"pattern":"^[a-z]{2}-[A-Z]{2}$","type":"string"}
+			  },
+			  {"name":"embedded","in":"query","schema":{"type":"string"}},
+			  {
+				"name":"json_filter","in":"query",
+				"content":{
+				  "application/json":{
+					"schema":{"$ref":"#/components/schemas/QueryOpenapi3TestJsonFilter"}
+				  }
+				}
+			  },
+			  {
+				"name":"deep_object_filter","in":"query","style":"deepObject",
+				"explode":true,
+				"schema":{"$ref":"#/components/schemas/QueryOpenapi3TestDeepObjectFilter"}
+			  },
+			  {
+				"name":"id","in":"path","required":true,
+				"schema":{"type":"string","example":"XXX-XXXXX"}
+			  }
+			],
+			"responses":{"204":{"description":"No Content"}}
+		  }
+		}
+	  },
+	  "components":{
+		"schemas":{
+		  "QueryOpenapi3TestDeepObjectFilter":{
+			"type":"object",
+			"properties":{
+			  "baz":{"type":"boolean"},
+			  "deeper":{"type":"object","properties":{"val":{"type":"string"}}},
+			  "quux":{"type":"number"}
+			}
+		  },
+		  "QueryOpenapi3TestJsonFilter":{
+			"type":"object",
+			"properties":{
+			  "bar":{"type":"integer"},
+			  "deeper":{"type":"object","properties":{"val":{"type":"string"}}},
+			  "foo":{"type":"string"}
+			}
+		  }
+		}
+	  }
+	}`, reflector.Spec)
+}
diff --git a/openapi3/reflect_test.go b/openapi3/reflect_test.go
index e44aeda..93f0f19 100644
--- a/openapi3/reflect_test.go
+++ b/openapi3/reflect_test.go
@@ -11,6 +11,7 @@ import (
 	"github.com/stretchr/testify/require"
 	"github.com/swaggest/assertjson"
 	"github.com/swaggest/jsonschema-go"
+	"github.com/swaggest/openapi-go"
 	"github.com/swaggest/openapi-go/openapi3"
 )
 
@@ -20,11 +21,15 @@ type WeirdResp interface {
 
 type UUID [16]byte
 
-type Resp struct {
+type EmbeddedHeader struct {
 	HeaderField string `header:"X-Header-Field" description:"Sample header response."`
-	Field1      int    `json:"field1"`
-	Field2      string `json:"field2"`
-	Info        struct {
+}
+
+type Resp struct {
+	EmbeddedHeader
+	Field1 int    `json:"field1"`
+	Field2 string `json:"field2"`
+	Info   struct {
 		Foo string  `json:"foo" default:"baz" required:"true" pattern:"\\d+"`
 		Bar float64 `json:"bar" description:"This is Bar."`
 	} `json:"info"`
@@ -90,19 +95,19 @@ const (
 	apiVersion = "1.2.3"
 )
 
-func TestReflector_SetRequest_array(t *testing.T) {
+func TestReflector_AddOperation_request_array(t *testing.T) {
 	reflector := openapi3.Reflector{}
 
 	s := reflector.SpecEns()
 	s.Info.Title = apiName
 	s.Info.Version = apiVersion
 
-	op := openapi3.Operation{}
+	oc, err := reflector.NewOperationContext(http.MethodPost, "/somewhere")
+	require.NoError(t, err)
 
-	err := reflector.SetRequest(&op, new([]GetReq), http.MethodPost)
-	assert.NoError(t, err)
+	oc.AddReqStructure(new([]GetReq))
 
-	require.NoError(t, s.AddOperation(http.MethodPost, "/somewhere", op))
+	require.NoError(t, reflector.AddOperation(oc))
 
 	b, err := assertjson.MarshalIndentCompact(s, "", " ", 120)
 	assert.NoError(t, err)
@@ -115,19 +120,18 @@ func TestReflector_SetRequest_array(t *testing.T) {
 	assertjson.Equal(t, expected, b)
 }
 
-func TestReflector_SetRequest_uploadInterface(t *testing.T) {
+func TestReflector_AddOperation_uploadInterface(t *testing.T) {
 	type req struct {
 		File1 multipart.File `formData:"upload1"`
 	}
 
 	reflector := openapi3.Reflector{}
-	s := reflector.SpecEns()
-	op := openapi3.Operation{}
+	oc, err := reflector.NewOperationContext(http.MethodPost, "/somewhere")
+	require.NoError(t, err)
 
-	err := reflector.SetRequest(&op, new(req), http.MethodPost)
-	assert.NoError(t, err)
+	oc.AddReqStructure(new(req))
 
-	require.NoError(t, s.AddOperation(http.MethodPost, "/somewhere", op))
+	require.NoError(t, reflector.AddOperation(oc))
 
 	assertjson.EqMarshal(t, `{
 	  "openapi":"3.0.3","info":{"title":"","version":""},
@@ -152,22 +156,21 @@ func TestReflector_SetRequest_uploadInterface(t *testing.T) {
 		  }
 		}
 	  }
-	}`, s)
+	}`, reflector.Spec)
 }
 
-func TestReflector_SetRequest(t *testing.T) {
+func TestReflector_AddOperation_request(t *testing.T) {
 	reflector := openapi3.Reflector{}
 
 	s := reflector.SpecEns()
 	s.Info.Title = apiName
 	s.Info.Version = apiVersion
 
-	op := openapi3.Operation{}
-
-	err := reflector.SetRequest(&op, new(GetReq), http.MethodGet)
-	assert.NoError(t, err)
+	oc, err := reflector.NewOperationContext(http.MethodGet, "/somewhere/{in_path}")
+	require.NoError(t, err)
+	oc.AddReqStructure(new(GetReq))
 
-	require.NoError(t, s.AddOperation(http.MethodGet, "/somewhere/{in_path}", op))
+	require.NoError(t, reflector.AddOperation(oc))
 
 	b, err := assertjson.MarshalIndentCompact(s, "", " ", 120)
 	assert.NoError(t, err)
@@ -180,7 +183,7 @@ func TestReflector_SetRequest(t *testing.T) {
 	assertjson.Equal(t, expected, b)
 }
 
-func TestReflector_SetJSONResponse(t *testing.T) {
+func TestReflector_AddOperation_JSON_response(t *testing.T) {
 	reflector := openapi3.Reflector{}
 
 	s := reflector.SpecEns()
@@ -189,12 +192,20 @@ func TestReflector_SetJSONResponse(t *testing.T) {
 
 	reflector.AddTypeMapping(new(WeirdResp), new(Resp))
 
-	op := openapi3.Operation{}
+	oc, err := reflector.NewOperationContext(http.MethodPost, "/somewhere/{in_path}")
+	require.NoError(t, err)
 
-	assert.NoError(t, reflector.SetRequest(&op, new(Req), http.MethodPost))
-	assert.NoError(t, reflector.SetJSONResponse(&op, new(WeirdResp), http.StatusOK))
-	assert.NoError(t, reflector.SetJSONResponse(&op, new([]WeirdResp), http.StatusConflict))
-	assert.NoError(t, reflector.SetStringResponse(&op, http.StatusConflict, "text/html"))
+	oc.AddReqStructure(new(Req))
+	oc.AddRespStructure(new(WeirdResp), func(cu *openapi.ContentUnit) {
+		cu.HTTPStatus = http.StatusOK
+	})
+	oc.AddRespStructure(new([]WeirdResp), func(cu *openapi.ContentUnit) {
+		cu.HTTPStatus = http.StatusConflict
+	})
+	oc.AddRespStructure("", func(cu *openapi.ContentUnit) {
+		cu.HTTPStatus = http.StatusConflict
+		cu.ContentType = "text/html"
+	})
 
 	pathItem := openapi3.PathItem{}
 	pathItem.
@@ -202,44 +213,54 @@ func TestReflector_SetJSONResponse(t *testing.T) {
 		WithDescription("Path Description")
 	s.Paths.WithMapOfPathItemValuesItem("/somewhere/{in_path}", pathItem)
 
-	js := op.RequestBody.RequestBody.Content["multipart/form-data"].Schema.ToJSONSchema(s)
-	expected, err := os.ReadFile("_testdata/req_schema.json")
+	require.NoError(t, reflector.AddOperation(oc))
+
+	require.NoError(t, s.SetupOperation(http.MethodPost, "/somewhere/{in_path}", func(op *openapi3.Operation) error {
+		js := op.RequestBody.RequestBody.Content["multipart/form-data"].Schema.ToJSONSchema(s)
+		expected, err := os.ReadFile("_testdata/req_schema.json")
+		require.NoError(t, err)
+		assertjson.EqualMarshal(t, expected, js)
+
+		return nil
+	}))
+
+	oc, err = reflector.NewOperationContext(http.MethodGet, "/somewhere/{in_path}")
 	require.NoError(t, err)
-	assertjson.EqualMarshal(t, expected, js)
 
-	assert.NoError(t, s.AddOperation(http.MethodPost, "/somewhere/{in_path}", op))
+	oc.AddReqStructure(new(GetReq))
+	oc.AddRespStructure(new(Resp))
 
-	op = openapi3.Operation{}
+	assert.NoError(t, reflector.AddOperation(oc))
 
-	assert.NoError(t, reflector.SetRequest(&op, new(GetReq), http.MethodGet))
-	assert.NoError(t, reflector.SetJSONResponse(&op, new(Resp), http.StatusOK))
-	assert.NoError(t, s.AddOperation(http.MethodGet, "/somewhere/{in_path}", op))
+	require.NoError(t, s.SetupOperation(http.MethodGet, "/somewhere/{in_path}", func(op *openapi3.Operation) error {
+		js := op.Responses.MapOfResponseOrRefValues[strconv.Itoa(http.StatusOK)].Response.Content["application/json"].
+			Schema.ToJSONSchema(s)
+		jsb, err := assertjson.MarshalIndentCompact(js, "", " ", 120)
+		require.NoError(t, err)
 
-	js = op.Responses.MapOfResponseOrRefValues[strconv.Itoa(http.StatusOK)].Response.Content["application/json"].
-		Schema.ToJSONSchema(s)
-	jsb, err := assertjson.MarshalIndentCompact(js, "", " ", 120)
-	require.NoError(t, err)
+		require.NoError(t, os.WriteFile("_testdata/resp_schema_last_run.json", jsb, 0o600))
 
-	require.NoError(t, os.WriteFile("_testdata/resp_schema_last_run.json", jsb, 0o600))
+		expected, err := os.ReadFile("_testdata/resp_schema.json")
+		require.NoError(t, err)
+		assertjson.EqualMarshal(t, expected, js)
 
-	expected, err = os.ReadFile("_testdata/resp_schema.json")
-	require.NoError(t, err)
-	assertjson.EqualMarshal(t, expected, js)
+		js = op.Responses.MapOfResponseOrRefValues[strconv.Itoa(http.StatusOK)].Response.Headers["X-Header-Field"].Header.
+			Schema.ToJSONSchema(s)
+		assertjson.EqMarshal(t, `{"type": "string", "description": "Sample header response."}`, js)
 
-	js = op.Responses.MapOfResponseOrRefValues[strconv.Itoa(http.StatusOK)].Response.Headers["X-Header-Field"].Header.
-		Schema.ToJSONSchema(s)
-	assertjson.EqMarshal(t, `{"type": "string", "description": "Sample header response."}`, js)
+		require.NoError(t, err)
+		assertjson.EqMarshal(t, `{"type": "integer", "description": "Query parameter."}`,
+			op.Parameters[0].Parameter.Schema.ToJSONSchema(s))
 
-	require.NoError(t, err)
-	assertjson.EqMarshal(t, `{"type": "integer", "description": "Query parameter."}`,
-		op.Parameters[0].Parameter.Schema.ToJSONSchema(s))
+		return nil
+	}))
 
 	b, err := assertjson.MarshalIndentCompact(s, "", " ", 120)
 	assert.NoError(t, err)
 
 	require.NoError(t, os.WriteFile("_testdata/openapi_last_run.json", b, 0o600))
 
-	expected, err = os.ReadFile("_testdata/openapi.json")
+	expected, err := os.ReadFile("_testdata/openapi.json")
 	require.NoError(t, err)
 
 	assertjson.EqualMarshal(t, expected, s)
@@ -256,17 +277,19 @@ type PathParamAndBody struct {
 	Data
 }
 
-func TestReflector_SetRequest_pathParamAndBody(t *testing.T) {
+func TestReflector_AddOperation_pathParamAndBody(t *testing.T) {
 	reflector := openapi3.Reflector{}
-	op := openapi3.Operation{}
 
-	err := reflector.SetRequest(&op, new(PathParamAndBody), http.MethodPost)
-	assert.NoError(t, err)
+	oc, err := reflector.NewOperationContext(http.MethodPost, "/somewhere/{id}")
+	require.NoError(t, err)
+
+	oc.AddReqStructure(new(PathParamAndBody))
+
+	assert.NoError(t, reflector.AddOperation(oc))
 
 	s := reflector.SpecEns()
 	s.Info.Title = apiName
 	s.Info.Version = apiVersion
-	assert.NoError(t, s.AddOperation(http.MethodPost, "/somewhere/{id}", op))
 
 	assertjson.EqMarshal(t, `{
 	 "openapi":"3.0.3","info":{"title":"SampleAPI","version":"1.2.3"},
@@ -291,17 +314,18 @@ type WithReqBody PathParamAndBody
 
 func (*WithReqBody) ForceRequestBody() {}
 
-func TestRequestBodyEnforcer(t *testing.T) {
+func TestReflector_AddOperation_RequestBodyEnforcer(t *testing.T) {
 	reflector := openapi3.Reflector{}
-	op := openapi3.Operation{}
+	oc, err := reflector.NewOperationContext(http.MethodGet, "/somewhere/{id}")
+	require.NoError(t, err)
 
-	err := reflector.SetRequest(&op, new(WithReqBody), http.MethodGet)
-	assert.NoError(t, err)
+	oc.AddReqStructure(new(WithReqBody))
 
 	s := reflector.SpecEns()
 	s.Info.Title = apiName
 	s.Info.Version = apiVersion
-	assert.NoError(t, s.AddOperation(http.MethodGet, "/somewhere/{id}", op))
+
+	assert.NoError(t, reflector.AddOperation(oc))
 
 	assertjson.EqMarshal(t, `{
 	 "openapi":"3.0.3","info":{"title":"SampleAPI","version":"1.2.3"},
@@ -318,28 +342,28 @@ func TestRequestBodyEnforcer(t *testing.T) {
 	}`, s)
 }
 
-func TestReflector_SetupResponse(t *testing.T) {
+func TestReflector_AddOperation_response(t *testing.T) {
 	reflector := openapi3.Reflector{}
-	op := openapi3.Operation{}
+	oc, err := reflector.NewOperationContext(http.MethodGet, "/somewhere")
+	require.NoError(t, err)
 
 	s := reflector.SpecEns()
 	s.Info.Title = apiName
 	s.Info.Version = apiVersion
 
-	require.NoError(t, reflector.SetupResponse(openapi3.OperationContext{
-		Operation:       &op,
-		RespContentType: "text/csv; charset=utf-8",
-		HTTPStatus:      http.StatusNoContent,
-		Output: new(struct {
-			Val1 int
-			Val2 string
-		}),
-		RespHeaderMapping: map[string]string{
+	oc.AddRespStructure(new(struct {
+		Val1 int
+		Val2 string
+	}), func(cu *openapi.ContentUnit) {
+		cu.ContentType = "text/csv; charset=utf-8"
+		cu.HTTPStatus = http.StatusNoContent
+		cu.SetFieldMapping(openapi.InHeader, map[string]string{
 			"Val1": "X-Value-1",
 			"Val2": "X-Value-2",
-		},
-	}))
-	require.NoError(t, s.AddOperation(http.MethodGet, "/somewhere", op))
+		})
+	})
+
+	require.NoError(t, reflector.AddOperation(oc))
 
 	assertjson.EqMarshal(t, `{
 	 "openapi":"3.0.3","info":{"title":"SampleAPI","version":"1.2.3"},
@@ -362,43 +386,42 @@ func TestReflector_SetupResponse(t *testing.T) {
 	}`, s)
 }
 
-func TestReflector_SetupRequest(t *testing.T) {
+func TestReflector_AddOperation_setup_request(t *testing.T) {
 	reflector := openapi3.Reflector{}
-	op := openapi3.Operation{}
+	oc, err := reflector.NewOperationContext(http.MethodPost, "/somewhere/{value-4}")
+	require.NoError(t, err)
 
 	s := reflector.SpecEns()
 	s.Info.Title = apiName
 	s.Info.Version = apiVersion
 
-	require.NoError(t, reflector.SetupRequest(openapi3.OperationContext{
-		Operation:  &op,
-		HTTPMethod: http.MethodPost,
-		Input: new(struct {
-			Val1 int
-			Val2 string
-			Val3 float64
-			Val4 bool
-			Val5 string
-			Val6 multipart.File
-		}),
-		ReqHeaderMapping: map[string]string{
+	oc.AddReqStructure(new(struct {
+		Val1 int
+		Val2 string
+		Val3 float64
+		Val4 bool
+		Val5 string
+		Val6 multipart.File
+	}), func(cu *openapi.ContentUnit) {
+		cu.SetFieldMapping(openapi.InHeader, map[string]string{
 			"Val1": "X-Value-1",
-		},
-		ReqQueryMapping: map[string]string{
+		})
+		cu.SetFieldMapping(openapi.InQuery, map[string]string{
 			"Val2": "value_2",
-		},
-		ReqFormDataMapping: map[string]string{
+		})
+		cu.SetFieldMapping(openapi.InFormData, map[string]string{
 			"Val3": "value3",
 			"Val6": "upload6",
-		},
-		ReqPathMapping: map[string]string{
+		})
+		cu.SetFieldMapping(openapi.InPath, map[string]string{
 			"Val4": "value-4",
-		},
-		ReqCookieMapping: map[string]string{
+		})
+		cu.SetFieldMapping(openapi.InCookie, map[string]string{
 			"Val5": "value_5",
-		},
-	}))
-	require.NoError(t, s.AddOperation(http.MethodPost, "/somewhere/{value-4}", op))
+		})
+	})
+
+	require.NoError(t, reflector.AddOperation(oc))
 
 	assertjson.EqMarshal(t, `{
 	 "openapi":"3.0.3","info":{"title":"SampleAPI","version":"1.2.3"},
@@ -429,22 +452,21 @@ func TestReflector_SetupRequest(t *testing.T) {
 	}`, s)
 }
 
-func TestReflector_SetupRequest_queryObject(t *testing.T) {
+func TestReflector_AddOperation_request_queryObject(t *testing.T) {
 	reflector := openapi3.Reflector{}
-	op := openapi3.Operation{}
+
+	oc, err := reflector.NewOperationContext(http.MethodPost, "/somewhere")
+	require.NoError(t, err)
 
 	s := reflector.SpecEns()
 	s.Info.Title = apiName
 	s.Info.Version = apiVersion
 
-	require.NoError(t, reflector.SetupRequest(openapi3.OperationContext{
-		Operation:  &op,
-		HTTPMethod: http.MethodGet,
-		Input: new(struct {
-			InQuery map[int]float64 `query:"in_query"`
-		}),
+	oc.AddReqStructure(new(struct {
+		InQuery map[int]float64 `query:"in_query"`
 	}))
-	require.NoError(t, s.AddOperation(http.MethodPost, "/somewhere", op))
+
+	require.NoError(t, reflector.AddOperation(oc))
 
 	assertjson.EqMarshal(t, `{
 	 "openapi":"3.0.3","info":{"title":"SampleAPI","version":"1.2.3"},
@@ -470,20 +492,17 @@ type namedType struct {
 
 type labels map[int]float64
 
-func TestReflector_SetupRequest_queryNamedObject(t *testing.T) {
+func TestReflector_AddOperation_request_queryNamedObject(t *testing.T) {
 	reflector := openapi3.Reflector{}
-	op := openapi3.Operation{}
+	oc, err := reflector.NewOperationContext(http.MethodPost, "/somewhere")
+	require.NoError(t, err)
 
 	s := reflector.SpecEns()
 	s.Info.Title = apiName
 	s.Info.Version = apiVersion
 
-	require.NoError(t, reflector.SetupRequest(openapi3.OperationContext{
-		Operation:  &op,
-		HTTPMethod: http.MethodGet,
-		Input:      new(namedType),
-	}))
-	require.NoError(t, s.AddOperation(http.MethodPost, "/somewhere", op))
+	oc.AddReqStructure(new(namedType))
+	require.NoError(t, reflector.AddOperation(oc))
 
 	assertjson.EqMarshal(t, `{
 	  "openapi":"3.0.3","info":{"title":"SampleAPI","version":"1.2.3"},
@@ -512,7 +531,7 @@ func TestReflector_SetupRequest_queryNamedObject(t *testing.T) {
 	assertjson.EqMarshal(t, `{"type":"object","additionalProperties":{"type":"number"}}`, js)
 }
 
-func TestReflector_SetupRequest_jsonQuery(t *testing.T) {
+func TestReflector_AddOperation_request_jsonQuery(t *testing.T) {
 	type filter struct {
 		Labels []string `json:"labels,omitempty"`
 		Type   string   `json:"type"`
@@ -525,13 +544,12 @@ func TestReflector_SetupRequest_jsonQuery(t *testing.T) {
 	}
 
 	r := openapi3.Reflector{}
-	oc := openapi3.OperationContext{
-		Operation: &openapi3.Operation{},
-		Input:     new(req),
-	}
+	oc, err := r.NewOperationContext(http.MethodGet, "/")
+	require.NoError(t, err)
+
+	oc.AddReqStructure(new(req))
 
-	require.NoError(t, r.SetupRequest(oc))
-	require.NoError(t, r.SpecEns().AddOperation(http.MethodGet, "/", *oc.Operation))
+	require.NoError(t, r.AddOperation(oc))
 
 	assertjson.EqMarshal(t, `{
 	  "openapi":"3.0.3","info":{"title":"","version":""},
@@ -578,7 +596,7 @@ func TestReflector_SetupRequest_jsonQuery(t *testing.T) {
 	}`, r.SpecEns())
 }
 
-func TestReflector_SetupRequest_forbidParams(t *testing.T) {
+func TestReflector_AddOperation_request_forbidParams(t *testing.T) {
 	type req struct {
 		Query  string `query:"query"`
 		Header string `header:"header"`
@@ -589,13 +607,12 @@ func TestReflector_SetupRequest_forbidParams(t *testing.T) {
 	}
 
 	r := openapi3.Reflector{}
-	oc := openapi3.OperationContext{
-		Operation: &openapi3.Operation{},
-		Input:     new(req),
-	}
+	oc, err := r.NewOperationContext(http.MethodGet, "/{path}")
+	require.NoError(t, err)
 
-	require.NoError(t, r.SetupRequest(oc))
-	require.NoError(t, r.SpecEns().AddOperation(http.MethodGet, "/{path}", *oc.Operation))
+	oc.AddReqStructure(new(req))
+
+	require.NoError(t, r.AddOperation(oc))
 
 	assertjson.EqMarshal(t, `{
 	  "openapi":"3.0.3","info":{"title":"","version":""},
@@ -618,13 +635,16 @@ func TestReflector_SetupRequest_forbidParams(t *testing.T) {
 	  }
 	}`, r.SpecEns())
 
-	assert.True(t, oc.Operation.UnknownParamIsForbidden(openapi3.ParameterInCookie))
-	assert.False(t, oc.Operation.UnknownParamIsForbidden(openapi3.ParameterInHeader))
-	assert.True(t, oc.Operation.UnknownParamIsForbidden(openapi3.ParameterInQuery))
-	assert.False(t, oc.Operation.UnknownParamIsForbidden(openapi3.ParameterInPath))
+	o3, ok := oc.(openapi3.OperationExposer)
+	require.True(t, ok)
+
+	assert.True(t, o3.Operation().UnknownParamIsForbidden(openapi3.ParameterInCookie))
+	assert.False(t, o3.Operation().UnknownParamIsForbidden(openapi3.ParameterInHeader))
+	assert.True(t, o3.Operation().UnknownParamIsForbidden(openapi3.ParameterInQuery))
+	assert.False(t, o3.Operation().UnknownParamIsForbidden(openapi3.ParameterInPath))
 }
 
-func TestReflector_SetupRequest_noBody(t *testing.T) {
+func TestReflector_AddOperation_request_noBody(t *testing.T) {
 	type req struct {
 		ID int `json:"id" path:"id"`
 	}
@@ -632,40 +652,48 @@ func TestReflector_SetupRequest_noBody(t *testing.T) {
 	r := openapi3.Reflector{}
 
 	for _, method := range []string{http.MethodHead, http.MethodGet, http.MethodDelete, http.MethodTrace} {
-		oc := openapi3.OperationContext{
-			Operation:  &openapi3.Operation{},
-			HTTPMethod: method,
-			Input:      new(req),
-		}
+		oc, err := r.NewOperationContext(method, "/{id}")
+		require.NoError(t, err)
 
-		require.NoError(t, r.SetupRequest(oc))
-		assertjson.EqMarshal(t, `{
-		  "parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"integer"}}],
-		  "responses":{}
-		}`, oc.Operation)
+		oc.AddReqStructure(new(req))
+
+		require.NoError(t, r.AddOperation(oc))
+
+		require.NoError(t, r.SpecEns().SetupOperation(method, "/{id}", func(op *openapi3.Operation) error {
+			assertjson.EqMarshal(t, `{
+			  "parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"integer"}}],
+			  "responses":{"204":{"description":"No Content"}}
+			}`, op)
+
+			return nil
+		}))
 	}
 
 	for _, method := range []string{http.MethodPost, http.MethodPatch, http.MethodPut} {
-		oc := openapi3.OperationContext{
-			Operation:  &openapi3.Operation{},
-			HTTPMethod: method,
-			Input:      new(req),
-		}
+		oc, err := r.NewOperationContext(method, "/{id}")
+		require.NoError(t, err)
 
-		require.NoError(t, r.SetupRequest(oc))
-		assertjson.EqMarshal(t, `{
-		  "parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"integer"}}],
-		  "requestBody":{
-			"content":{
-			  "application/json":{"schema":{"$ref":"#/components/schemas/Openapi3TestReq"}}
-			}
-		  },
-		  "responses":{}
-		}`, oc.Operation)
+		oc.AddReqStructure(new(req))
+
+		require.NoError(t, r.AddOperation(oc))
+
+		require.NoError(t, r.SpecEns().SetupOperation(method, "/{id}", func(op *openapi3.Operation) error {
+			assertjson.EqMarshal(t, `{
+			  "parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"integer"}}],
+			  "requestBody":{
+				"content":{
+				  "application/json":{"schema":{"$ref":"#/components/schemas/Openapi3TestReq"}}
+				}
+			  },
+			  "responses":{"204":{"description":"No Content"}}
+			}`, op)
+
+			return nil
+		}))
 	}
 }
 
-func TestOperationCtx(t *testing.T) {
+func TestReflector_AddOperation_OperationCtx(t *testing.T) {
 	type req struct {
 		Query  string `query:"query"`
 		Header string `header:"header"`
@@ -680,11 +708,11 @@ func TestOperationCtx(t *testing.T) {
 	}
 
 	r := openapi3.Reflector{}
-	oc := openapi3.OperationContext{
-		Operation: &openapi3.Operation{},
-		Input:     new(req),
-		Output:    new(resp),
-	}
+	oc, err := r.NewOperationContext(http.MethodPost, "/{path}")
+	require.NoError(t, err)
+
+	oc.AddReqStructure(new(req))
+	oc.AddRespStructure(new(resp))
 
 	visited := map[string]bool{}
 
@@ -695,11 +723,11 @@ func TestOperationCtx(t *testing.T) {
 			currentRC = rc
 		},
 		jsonschema.InterceptSchema(func(params jsonschema.InterceptSchemaParams) (stop bool, err error) {
-			if occ, ok := openapi3.OperationCtx(currentRC); ok {
-				if occ.ProcessingResponse {
-					visited["resp:"+occ.ProcessingIn] = true
+			if occ, ok := openapi.OperationCtx(currentRC); ok {
+				if occ.IsProcessingResponse() {
+					visited["resp:"+string(occ.ProcessingIn())] = true
 				} else {
-					visited["req:"+occ.ProcessingIn] = true
+					visited["req:"+string(occ.ProcessingIn())] = true
 				}
 			}
 
@@ -707,8 +735,7 @@ func TestOperationCtx(t *testing.T) {
 		}),
 	)
 
-	require.NoError(t, r.SetupRequest(oc))
-	require.NoError(t, r.SetupResponse(oc))
+	require.NoError(t, r.AddOperation(oc))
 
 	assert.Equal(t, map[string]bool{
 		"req:body":    true,
@@ -721,47 +748,24 @@ func TestOperationCtx(t *testing.T) {
 	}, visited)
 }
 
-func TestReflector_SetStringResponse(t *testing.T) {
-	reflector := openapi3.Reflector{}
-
-	s := reflector.SpecEns()
-	s.Info.Title = apiName
-	s.Info.Version = apiVersion
-
-	reflector.AddTypeMapping(new(WeirdResp), new(Resp))
-
-	op := openapi3.Operation{}
-
-	err := reflector.SetRequest(&op, new(Req), http.MethodPost)
-	assert.NoError(t, err)
-
-	err = reflector.SetJSONResponse(&op, new(WeirdResp), http.StatusOK)
-	assert.NoError(t, err)
-
-	err = reflector.SetJSONResponse(&op, new([]WeirdResp), http.StatusConflict)
-	assert.NoError(t, err)
-}
-
-func TestReflector_SetRequest_formData_with_json(t *testing.T) {
+func TestReflector_AddOperation_request_formData_with_json(t *testing.T) {
 	// In presence of `formData` tags, `json` tags would be ignored as request body.
 	type req struct {
 		Foo int `formData:"foo" json:"foo"`
 	}
 
 	r := openapi3.Reflector{}
-	oc := openapi3.OperationContext{
-		Operation: &openapi3.Operation{},
-		Input:     new(req),
-	}
+	oc, err := r.NewOperationContext(http.MethodPost, "/foo")
+	require.NoError(t, err)
 
-	require.NoError(t, r.SetupRequest(oc))
-	require.NoError(t, r.SpecEns().AddOperation(http.MethodGet, "/foo", *oc.Operation))
+	oc.AddReqStructure(new(req))
+	require.NoError(t, r.AddOperation(oc))
 
 	assertjson.EqMarshal(t, `{
 	  "openapi":"3.0.3","info":{"title":"","version":""},
 	  "paths":{
 		"/foo":{
-		  "get":{
+		  "post":{
 			"requestBody":{
 			  "content":{
 				"application/x-www-form-urlencoded":{"schema":{"$ref":"#/components/schemas/FormDataOpenapi3TestReq"}}
@@ -779,7 +783,7 @@ func TestReflector_SetRequest_formData_with_json(t *testing.T) {
 	}`, r.SpecEns())
 }
 
-func TestReflector_SetupRequest_form(t *testing.T) {
+func TestReflector_AddOperation_request_form(t *testing.T) {
 	// Fields with `form` will be present as both `query` parameter and `formData` property.
 	type req struct {
 		Foo  int    `formData:"foo"`
@@ -789,13 +793,12 @@ func TestReflector_SetupRequest_form(t *testing.T) {
 	}
 
 	r := openapi3.Reflector{}
-	oc := openapi3.OperationContext{
-		Operation: &openapi3.Operation{},
-		Input:     req{},
-	}
+	oc, err := r.NewOperationContext(http.MethodPost, "/foo")
+	require.NoError(t, err)
 
-	require.NoError(t, r.SetupRequest(oc))
-	require.NoError(t, r.SpecEns().AddOperation(http.MethodPost, "/foo", *oc.Operation))
+	oc.AddReqStructure(req{})
+
+	require.NoError(t, r.AddOperation(oc))
 
 	assertjson.EqMarshal(t, `{
 	  "openapi":"3.0.3","info":{"title":"","version":""},
@@ -830,7 +833,7 @@ func TestReflector_SetupRequest_form(t *testing.T) {
 	}`, r.SpecEns())
 }
 
-func TestReflector_SetupRequest_form_only(t *testing.T) {
+func TestReflector_AddOperation_request_form_only(t *testing.T) {
 	// Fields with `form` will be present as both `query` parameter and `formData` property.
 	type req struct {
 		Bar  int    `form:"bar"`
@@ -838,13 +841,12 @@ func TestReflector_SetupRequest_form_only(t *testing.T) {
 	}
 
 	r := openapi3.Reflector{}
-	oc := openapi3.OperationContext{
-		Operation: &openapi3.Operation{},
-		Input:     req{},
-	}
+	oc, err := r.NewOperationContext(http.MethodPost, "/foo")
+	require.NoError(t, err)
 
-	require.NoError(t, r.SetupRequest(oc))
-	require.NoError(t, r.SpecEns().AddOperation(http.MethodPost, "/foo", *oc.Operation))
+	oc.AddReqStructure(req{})
+
+	require.NoError(t, r.AddOperation(oc))
 
 	assertjson.EqMarshal(t, `{
 	  "openapi":"3.0.3","info":{"title":"","version":""},
@@ -875,7 +877,7 @@ func TestReflector_SetupRequest_form_only(t *testing.T) {
 	}`, r.SpecEns())
 }
 
-func TestReflector_SetRequest_queryObject(t *testing.T) {
+func TestReflector_AddOperation_request_queryObject_deepObject(t *testing.T) {
 	reflector := openapi3.Reflector{}
 
 	// JSON object is only enabled when at least one `json` tag is available on top-level property,
@@ -920,10 +922,12 @@ func TestReflector_SetRequest_queryObject(t *testing.T) {
 		Amount uint `json:"amount"`
 	}
 
-	getOp := openapi3.Operation{}
+	oc, err := reflector.NewOperationContext(http.MethodGet, "/things/{id}")
+	require.NoError(t, err)
+
+	oc.AddReqStructure(new(req))
 
-	handleError(reflector.SetRequest(&getOp, new(req), http.MethodGet))
-	handleError(reflector.Spec.AddOperation(http.MethodGet, "/things/{id}", getOp))
+	require.NoError(t, reflector.AddOperation(oc))
 
 	assertjson.EqMarshal(t, `{
 	  "openapi":"3.0.3","info":{"title":"","version":""},
diff --git a/openapi3/walk_schema_test.go b/openapi3/walk_schema_test.go
new file mode 100644
index 0000000..3264935
--- /dev/null
+++ b/openapi3/walk_schema_test.go
@@ -0,0 +1,275 @@
+package openapi3_test
+
+import (
+	"net/http"
+	"strconv"
+	"testing"
+
+	"github.com/stretchr/testify/assert"
+	"github.com/swaggest/assertjson"
+	jsonschema "github.com/swaggest/jsonschema-go"
+	"github.com/swaggest/openapi-go"
+	"github.com/swaggest/openapi-go/openapi3"
+)
+
+func TestReflector_WalkRequestJSONSchemas(t *testing.T) {
+	r := openapi3.NewReflector()
+
+	type Embed struct {
+		Query1 int    `query:"query1" minimum:"3"`
+		Query2 string `query:"query2" minLength:"2"`
+		Query3 bool   `query:"query3" description:"Trivial schema."`
+	}
+
+	type DeeplyEmbedded struct {
+		Embed
+	}
+
+	type req struct {
+		*DeeplyEmbedded
+
+		Path1 int    `path:"path1" minimum:"3"`
+		Path2 string `path:"path2" minLength:"2"`
+		Path3 bool   `path:"path3" description:"Trivial schema."`
+
+		Header1 int    `header:"header1" minimum:"3"`
+		Header2 string `header:"header2" minLength:"2"`
+		Header3 bool   `header:"header3" description:"Trivial schema."`
+
+		Cookie1 int    `cookie:"cookie1" minimum:"3"`
+		Cookie2 string `cookie:"cookie2" minLength:"2"`
+		Cookie3 bool   `cookie:"cookie3" description:"Trivial schema."`
+
+		Form1 int    `form:"form1" minimum:"3"`
+		Form2 string `form:"form2" minLength:"2"`
+		Form3 bool   `form:"form3" description:"Trivial schema."`
+	}
+
+	cu := openapi.ContentUnit{}
+	cu.Structure = req{}
+
+	schemas := map[string]*jsonschema.SchemaOrBool{}
+	doneCalled := 0
+
+	assert.NoError(t, r.WalkRequestJSONSchemas(http.MethodPost, cu,
+		func(in openapi.In, paramName string, schema *jsonschema.SchemaOrBool, required bool) error {
+			schemas[string(in)+"-"+paramName+"-"+strconv.FormatBool(required)+"-"+
+				strconv.FormatBool(schema.IsTrivial(r.ResolveJSONSchemaRef))] = schema
+
+			return nil
+		},
+		func(oc openapi.OperationContext) {
+			doneCalled++
+		},
+	))
+
+	assert.Equal(t, 1, doneCalled)
+	assertjson.EqMarshal(t, `{
+	  "cookie-cookie1-false-false":{"minimum":3,"type":"integer"},
+	  "cookie-cookie2-false-false":{"minLength":2,"type":"string"},
+	  "cookie-cookie3-false-true":{"description":"Trivial schema.","type":"boolean"},
+	  "formData-form1-false-false":{"minimum":3,"type":"integer"},
+	  "formData-form2-false-false":{"minLength":2,"type":"string"},
+	  "formData-form3-false-true":{"description":"Trivial schema.","type":"boolean"},
+	  "header-header1-false-false":{"minimum":3,"type":"integer"},
+	  "header-header2-false-false":{"minLength":2,"type":"string"},
+	  "header-header3-false-true":{"description":"Trivial schema.","type":"boolean"},
+	  "path-path1-true-false":{"minimum":3,"type":"integer"},
+	  "path-path2-true-false":{"minLength":2,"type":"string"},
+	  "path-path3-true-true":{"description":"Trivial schema.","type":"boolean"},
+	  "query-form1-false-false":{"minimum":3,"type":"integer"},
+	  "query-form2-false-false":{"minLength":2,"type":"string"},
+	  "query-form3-false-true":{"description":"Trivial schema.","type":"boolean"},
+	  "query-query1-false-false":{"minimum":3,"type":"integer"},
+	  "query-query2-false-false":{"minLength":2,"type":"string"},
+	  "query-query3-false-true":{"description":"Trivial schema.","type":"boolean"}
+	}`, schemas)
+
+	assertjson.EqMarshal(t, `{
+	  "openapi":"3.0.3","info":{"title":"","version":""},"paths":{},
+	  "components":{
+		"schemas":{
+		  "FormDataOpenapi3TestReq":{
+			"type":"object",
+			"properties":{
+			  "form1":{"minimum":3,"type":"integer"},
+			  "form2":{"minLength":2,"type":"string"},
+			  "form3":{"type":"boolean","description":"Trivial schema."}
+			}
+		  }
+		}
+	  }
+	}`, r.Spec)
+}
+
+func TestReflector_WalkRequestJSONSchemas_jsonBody(t *testing.T) {
+	r := openapi3.NewReflector()
+
+	type Embed struct {
+		Query1 int    `query:"query1" minimum:"3"`
+		Query2 string `query:"query2" minLength:"2"`
+		Query3 bool   `query:"query3" description:"Trivial schema."`
+
+		Foo int `json:"foo" minimum:"6"`
+	}
+
+	type DeeplyEmbedded struct {
+		Embed
+	}
+
+	type req struct {
+		*DeeplyEmbedded
+
+		Path1 int    `path:"path1" minimum:"3"`
+		Path2 string `path:"path2" minLength:"2"`
+		Path3 bool   `path:"path3" description:"Trivial schema."`
+
+		Header1 int    `header:"header1" minimum:"3"`
+		Header2 string `header:"header2" minLength:"2"`
+		Header3 bool   `header:"header3" description:"Trivial schema."`
+
+		Cookie1 int    `cookie:"cookie1" minimum:"3"`
+		Cookie2 string `cookie:"cookie2" minLength:"2"`
+		Cookie3 bool   `cookie:"cookie3" description:"Trivial schema."`
+
+		Bar []string `json:"bar" minItems:"15"`
+	}
+
+	cu := openapi.ContentUnit{}
+	cu.Structure = req{}
+
+	schemas := map[string]*jsonschema.SchemaOrBool{}
+	doneCalled := 0
+
+	assert.NoError(t, r.WalkRequestJSONSchemas(http.MethodPost, cu,
+		func(in openapi.In, paramName string, schema *jsonschema.SchemaOrBool, required bool) error {
+			schemas[string(in)+"-"+paramName+"-"+strconv.FormatBool(required)+"-"+
+				strconv.FormatBool(schema.IsTrivial(r.ResolveJSONSchemaRef))] = schema
+
+			return nil
+		},
+		func(oc openapi.OperationContext) {
+			doneCalled++
+		},
+	))
+
+	assert.Equal(t, 1, doneCalled)
+	assertjson.EqMarshal(t, `{
+	  "body-body-false-false":{
+		"properties":{
+		  "bar":{"items":{"type":"string"},"minItems":15,"type":["array","null"]},
+		  "foo":{"minimum":6,"type":"integer"}
+		},
+		"type":"object"
+	  },
+	  "cookie-cookie1-false-false":{"minimum":3,"type":"integer"},
+	  "cookie-cookie2-false-false":{"minLength":2,"type":"string"},
+	  "cookie-cookie3-false-true":{"description":"Trivial schema.","type":"boolean"},
+	  "header-header1-false-false":{"minimum":3,"type":"integer"},
+	  "header-header2-false-false":{"minLength":2,"type":"string"},
+	  "header-header3-false-true":{"description":"Trivial schema.","type":"boolean"},
+	  "path-path1-true-false":{"minimum":3,"type":"integer"},
+	  "path-path2-true-false":{"minLength":2,"type":"string"},
+	  "path-path3-true-true":{"description":"Trivial schema.","type":"boolean"},
+	  "query-query1-false-false":{"minimum":3,"type":"integer"},
+	  "query-query2-false-false":{"minLength":2,"type":"string"},
+	  "query-query3-false-true":{"description":"Trivial schema.","type":"boolean"}
+	}`, schemas)
+
+	assertjson.EqMarshal(t, `{
+	  "openapi":"3.0.3","info":{"title":"","version":""},"paths":{},
+	  "components":{
+		"schemas":{
+		  "Openapi3TestReq":{
+			"type":"object",
+			"properties":{
+			  "bar":{
+				"minItems":15,"type":"array","items":{"type":"string"},
+				"nullable":true
+			  },
+			  "foo":{"minimum":6,"type":"integer"}
+			}
+		  }
+		}
+	  }
+	}`, r.Spec)
+}
+
+func TestReflector_WalkResponseJSONSchemas(t *testing.T) {
+	r := openapi3.NewReflector()
+
+	type Embed struct {
+		Header1 int    `header:"header1" minimum:"3"`
+		Header2 string `header:"header2" minLength:"2"`
+		Header3 bool   `header:"header3" description:"Trivial schema."`
+
+		Foo int `json:"foo" minimum:"6"`
+	}
+
+	type DeeplyEmbedded struct {
+		Embed
+	}
+
+	type req struct {
+		*DeeplyEmbedded
+
+		Header4 int    `header:"header4" minimum:"3"`
+		Header5 string `header:"header5" minLength:"2"`
+		Header6 bool   `header:"header6" description:"Trivial schema."`
+
+		Bar []string `json:"bar" minItems:"15"`
+	}
+
+	cu := openapi.ContentUnit{}
+	cu.Structure = req{}
+
+	schemas := map[string]*jsonschema.SchemaOrBool{}
+	doneCalled := 0
+
+	assert.NoError(t, r.WalkResponseJSONSchemas(cu,
+		func(in openapi.In, paramName string, schema *jsonschema.SchemaOrBool, required bool) error {
+			schemas[string(in)+"-"+paramName+"-"+strconv.FormatBool(required)+"-"+
+				strconv.FormatBool(schema.IsTrivial(r.ResolveJSONSchemaRef))] = schema
+
+			return nil
+		},
+		func(oc openapi.OperationContext) {
+			doneCalled++
+		},
+	))
+
+	assert.Equal(t, 1, doneCalled)
+	assertjson.EqMarshal(t, `{
+	  "body-body-false-false":{
+		"properties":{
+		  "bar":{"items":{"type":"string"},"minItems":15,"type":["array","null"]},
+		  "foo":{"minimum":6,"type":"integer"}
+		},
+		"type":"object"
+	  },
+	  "header-Header1-false-false":{"minimum":3,"type":"integer"},
+	  "header-Header2-false-false":{"minLength":2,"type":"string"},
+	  "header-Header3-false-true":{"description":"Trivial schema.","type":"boolean"},
+	  "header-Header4-false-false":{"minimum":3,"type":"integer"},
+	  "header-Header5-false-false":{"minLength":2,"type":"string"},
+	  "header-Header6-false-true":{"description":"Trivial schema.","type":"boolean"}
+	}`, schemas)
+
+	assertjson.EqMarshal(t, `{
+	  "openapi":"3.0.3","info":{"title":"","version":""},"paths":{},
+	  "components":{
+		"schemas":{
+		  "Openapi3TestReq":{
+			"type":"object",
+			"properties":{
+			  "bar":{
+				"minItems":15,"type":"array","items":{"type":"string"},
+				"nullable":true
+			  },
+			  "foo":{"minimum":6,"type":"integer"}
+			}
+		  }
+		}
+	  }
+	}`, r.Spec)
+}