Repository: swaggest/openapi-go
Pull request #84 (2023-10-03 23:04:30)

Avoid redundant prefixes for schema definitions

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

| Language  | Files |    Lines    |    Code     | Comments |  Blanks   | Complexity |     Bytes      |
|-----------|-------|-------------|-------------|----------|-----------|------------|----------------|
| Go        | 20    | 3864 (+38)  | 2871 (+32)  | 214      | 779 (+6)  | 799 (+8)   | 89.4K (+842B)  |
| Go (test) | 19    | 5681 (+174) | 4264 (+158) | 660      | 757 (+16) | 55         | 148.5K (+4.2K) |
| JSON      | 16    | 5554 (-2)   | 5554 (-2)   | 0        | 0         | 0          | 133.5K (-140B) |
<!-- Sticky Pull Request CommentLOC -->
github-actions[bot]: ### Go API Changes

<pre>
# summary
Inferred base version: v0.2.40
Suggested version: v0.3.0
</pre>
<!-- Sticky Pull Request Commentgorelease -->
github-actions[bot]: ### Unit Test Coverage
total:									(statements)				43.2%
changed lines: (statements) 93.3%
<details><summary>Coverage of changed lines</summary>

|           File           |     Function     | Coverage |
|--------------------------|------------------|----------|
| Total                    |                  | 93.3%    |
| openapi3/reflect.go      |                  | 93.8%    |
| openapi3/reflect.go:316  | parseRequestBody | 100.0%   |
| openapi3/reflect.go:242  | setupRequest     | 50.0%    |
| openapi31/reflect.go     |                  | 92.9%    |
| openapi31/reflect.go:264 | parseRequestBody | 100.0%   |
| openapi31/reflect.go:189 | setupRequest     | 50.0%    |

</details>

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

|         File         |     Function     | Base Coverage | Current Coverage |
|----------------------|------------------|---------------|------------------|
| Total                |                  | 43.1%         | 43.2% (+0.1%)    |
| openapi3/reflect.go  | parseRequestBody | 97.9%         | 98.2% (+0.3%)    |
| openapi31/reflect.go | parseRequestBody | 94.2%         | 95.0% (+0.8%)    |

</details>
<!-- Sticky Pull Request Commentunit-test -->
codecov[bot]: ## [Codecov](https://app.codecov.io/gh/swaggest/openapi-go/pull/84?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=swaggest) Report
> Merging [#84](https://app.codecov.io/gh/swaggest/openapi-go/pull/84?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=swaggest) (2e6dca8) into [master](https://app.codecov.io/gh/swaggest/openapi-go/commit/1c5cea92bf8f09bcacca127e2586afda73a928f1?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=swaggest) (1c5cea9) will **increase** coverage by `0.11%`.
> The diff coverage is `93.33%`.

```diff
@@            Coverage Diff             @@
##           master      #84      +/-   ##
==========================================
+ Coverage   37.92%   38.03%   +0.11%     
==========================================
  Files          14       14              
  Lines        6555     6567      +12     
==========================================
+ Hits         2486     2498      +12     
  Misses       3637     3637              
  Partials      432      432              
```

| [Flag](https://app.codecov.io/gh/swaggest/openapi-go/pull/84/flags?src=pr&el=flags&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=swaggest) | Coverage Δ | |
|---|---|---|
| [unittests](https://app.codecov.io/gh/swaggest/openapi-go/pull/84/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=swaggest) | `38.03% <93.33%> (+0.11%)` | :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.

| [Files](https://app.codecov.io/gh/swaggest/openapi-go/pull/84?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=swaggest) | Coverage Δ | |
|---|---|---|
| [openapi3/reflect.go](https://app.codecov.io/gh/swaggest/openapi-go/pull/84?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=swaggest#diff-b3BlbmFwaTMvcmVmbGVjdC5nbw==) | `81.25% <93.33%> (+0.27%)` | :arrow_up: |
| [openapi31/reflect.go](https://app.codecov.io/gh/swaggest/openapi-go/pull/84?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=swaggest#diff-b3BlbmFwaTMxL3JlZmxlY3QuZ28=) | `74.80% <93.33%> (+0.39%)` | :arrow_up: |

: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 036b9de..7acb9cc 100644
--- a/go.mod
+++ b/go.mod
@@ -6,8 +6,8 @@ require (
 	github.com/bool64/dev v0.2.31
 	github.com/stretchr/testify v1.8.2
 	github.com/swaggest/assertjson v1.9.0
-	github.com/swaggest/jsonschema-go v0.3.61
-	github.com/swaggest/refl v1.2.1
+	github.com/swaggest/jsonschema-go v0.3.62
+	github.com/swaggest/refl v1.3.0
 	gopkg.in/yaml.v2 v2.4.0
 )
 
diff --git a/go.sum b/go.sum
index 1d04bf2..1ba37b6 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.61 h1:46Jk7nifEDrGsPEAlWCVprqSaczETWO1LhtAKtxMM5I=
-github.com/swaggest/jsonschema-go v0.3.61/go.mod h1:FAsTxvFjOp+aS4DqXrY7MTYC2r7rrmxEzxEgbQVyR/c=
-github.com/swaggest/refl v1.2.1 h1:1meX9NaXjM5lmb4kk4RP3OZsXFRke9B1EHAP/pCEKO0=
-github.com/swaggest/refl v1.2.1/go.mod h1:CkC6g7h1PW33KprTuYRSw8UUOslRUt4lF3oe7tTIgNU=
+github.com/swaggest/jsonschema-go v0.3.62 h1:eIE0aRklWa2eLJg2L/zqyWpKvgUPbq2oKOtrJGJkPH0=
+github.com/swaggest/jsonschema-go v0.3.62/go.mod h1:DYuKqdpms/edvywsX6p1zHXCZkdwB28wRaBdFCe3Duw=
+github.com/swaggest/refl v1.3.0 h1:PEUWIku+ZznYfsoyheF97ypSduvMApYyGkYF3nabS0I=
+github.com/swaggest/refl v1.3.0/go.mod h1:3Ujvbmh1pfSbDYjC6JGG7nMgPvpG0ehQL4iNonnLNbg=
 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/openapi3/reflect.go b/openapi3/reflect.go
index b5759eb..5e42778 100644
--- a/openapi3/reflect.go
+++ b/openapi3/reflect.go
@@ -244,9 +244,9 @@ func (r *Reflector) setupRequest(o *Operation, oc openapi.OperationContext) erro
 		switch cu.ContentType {
 		case "":
 			if err := joinErrors(
+				r.parseRequestBody(o, oc, cu, mimeFormUrlencoded, oc.Method(), cu.FieldMapping(openapi.InFormData), tagFormData, tagForm),
 				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),
 			); err != nil {
 				return err
 			}
@@ -259,8 +259,8 @@ func (r *Reflector) setupRequest(o *Operation, oc openapi.OperationContext) erro
 			}
 		case mimeFormUrlencoded, mimeMultipart:
 			if err := joinErrors(
-				r.parseParameters(o, oc, cu),
 				r.parseRequestBody(o, oc, cu, mimeFormUrlencoded, oc.Method(), cu.FieldMapping(openapi.InFormData), tagFormData, tagForm),
+				r.parseParameters(o, oc, cu),
 			); err != nil {
 				return err
 			}
@@ -352,7 +352,9 @@ func (r *Reflector) parseRequestBody(
 	}
 
 	// If `formData` is defined on a request body `json` is ignored.
-	if tag == tagJSON && refl.HasTaggedFields(input, tagFormData) && !forceJSONRequestBody {
+	if tag == tagJSON &&
+		(refl.HasTaggedFields(input, tagFormData) || refl.HasTaggedFields(input, tagForm)) &&
+		!forceJSONRequestBody {
 		return nil
 	}
 
@@ -370,7 +372,24 @@ func (r *Reflector) parseRequestBody(
 
 	schema, err := r.Reflect(input,
 		openapi.WithOperationCtx(oc, false, "body"),
-		jsonschema.DefinitionsPrefix(componentsSchemas+definitionPrefix),
+		jsonschema.DefinitionsPrefix(componentsSchemas),
+		jsonschema.InterceptDefName(func(t reflect.Type, defaultDefName string) string {
+			if tag != tagJSON {
+				v := reflect.New(t).Interface()
+
+				if refl.HasTaggedFields(v, tag) {
+					return definitionPrefix + defaultDefName
+				}
+
+				for _, at := range additionalTags {
+					if refl.HasTaggedFields(v, at) {
+						return definitionPrefix + defaultDefName
+					}
+				}
+			}
+
+			return defaultDefName
+		}),
 		jsonschema.RootRef,
 		jsonschema.PropertyNameMapping(mapping),
 		jsonschema.PropertyNameTag(tag, additionalTags...),
@@ -416,7 +435,7 @@ func (r *Reflector) parseRequestBody(
 
 		s.FromJSONSchema(def)
 
-		r.SpecEns().ComponentsEns().SchemasEns().WithMapOfSchemaOrRefValuesItem(definitionPrefix+name, s)
+		r.SpecEns().ComponentsEns().SchemasEns().WithMapOfSchemaOrRefValuesItem(name, s)
 	}
 
 	if mime == mimeFormUrlencoded && hasFileUpload {
diff --git a/openapi31/reflect.go b/openapi31/reflect.go
index e1b15ba..4b1bc87 100644
--- a/openapi31/reflect.go
+++ b/openapi31/reflect.go
@@ -191,9 +191,9 @@ func (r *Reflector) setupRequest(o *Operation, oc openapi.OperationContext) erro
 		switch cu.ContentType {
 		case "":
 			if err := joinErrors(
+				r.parseRequestBody(o, oc, cu, mimeFormUrlencoded, oc.Method(), cu.FieldMapping(openapi.InFormData), tagFormData, tagForm),
 				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),
 			); err != nil {
 				return err
 			}
@@ -206,8 +206,8 @@ func (r *Reflector) setupRequest(o *Operation, oc openapi.OperationContext) erro
 			}
 		case mimeFormUrlencoded, mimeMultipart:
 			if err := joinErrors(
-				r.parseParameters(o, oc, cu),
 				r.parseRequestBody(o, oc, cu, mimeFormUrlencoded, oc.Method(), cu.FieldMapping(openapi.InFormData), tagFormData, tagForm),
+				r.parseParameters(o, oc, cu),
 			); err != nil {
 				return err
 			}
@@ -300,7 +300,9 @@ func (r *Reflector) parseRequestBody(
 	}
 
 	// If `formData` is defined on a request body `json` is ignored.
-	if tag == tagJSON && refl.HasTaggedFields(input, tagFormData) && !forceJSONRequestBody {
+	if tag == tagJSON &&
+		(refl.HasTaggedFields(input, tagFormData) || refl.HasTaggedFields(input, tagForm)) &&
+		!forceJSONRequestBody {
 		return nil
 	}
 
@@ -318,7 +320,24 @@ func (r *Reflector) parseRequestBody(
 
 	schema, err := r.Reflect(input,
 		openapi.WithOperationCtx(oc, false, "body"),
-		jsonschema.DefinitionsPrefix(componentsSchemas+definitionPrefix),
+		jsonschema.DefinitionsPrefix(componentsSchemas),
+		jsonschema.InterceptDefName(func(t reflect.Type, defaultDefName string) string {
+			if tag != tagJSON {
+				v := reflect.New(t).Interface()
+
+				if refl.HasTaggedFields(v, tag) {
+					return definitionPrefix + defaultDefName
+				}
+
+				for _, at := range additionalTags {
+					if refl.HasTaggedFields(v, at) {
+						return definitionPrefix + defaultDefName
+					}
+				}
+			}
+
+			return defaultDefName
+		}),
 		jsonschema.RootRef,
 		jsonschema.PropertyNameMapping(mapping),
 		jsonschema.PropertyNameTag(tag, additionalTags...),
@@ -369,7 +388,7 @@ func (r *Reflector) parseRequestBody(
 			return err
 		}
 
-		r.SpecEns().ComponentsEns().WithSchemasItem(definitionPrefix+name, sm)
+		r.SpecEns().ComponentsEns().WithSchemasItem(name, sm)
 	}
 
 	if mime == mimeFormUrlencoded && hasFileUpload {


Test patch:
diff --git a/openapi3/_testdata/req_schema.json b/openapi3/_testdata/req_schema.json
deleted file mode 100644
index 8aaa4b8..0000000
--- a/openapi3/_testdata/req_schema.json
+++ /dev/null
@@ -1,14 +0,0 @@
-{
-  "properties":{
-    "in_form1":{"type":"string"},"in_form2":{"type":"string"},
-    "upload1":{"$ref":"#/components/schemas/FormDataMultipartFile"},
-    "upload2":{"$ref":"#/components/schemas/FormDataMultipartFileHeader"}
-  },
-  "type":"object",
-  "components":{
-    "schemas":{
-      "FormDataMultipartFile":{"type":["string","null"],"format":"binary"},
-      "FormDataMultipartFileHeader":{"type":["string","null"],"format":"binary"}
-    }
-  }
-}
diff --git a/openapi3/reflect_deprecated_test.go b/openapi3/reflect_deprecated_test.go
index 82218c7..9bda437 100644
--- a/openapi3/reflect_deprecated_test.go
+++ b/openapi3/reflect_deprecated_test.go
@@ -31,9 +31,9 @@ func TestReflector_SetRequest_array(t *testing.T) {
 	b, err := assertjson.MarshalIndentCompact(s, "", " ", 120)
 	assert.NoError(t, err)
 
-	require.NoError(t, os.WriteFile("_testdata/openapi_req_array_last_run.json", b, 0o600))
+	require.NoError(t, os.WriteFile("testdata/openapi_req_array_last_run.json", b, 0o600))
 
-	expected, err := os.ReadFile("_testdata/openapi_req_array.json")
+	expected, err := os.ReadFile("testdata/openapi_req_array.json")
 	require.NoError(t, err)
 
 	assertjson.Equal(t, expected, b)
@@ -69,11 +69,11 @@ func TestReflector_SetRequest_uploadInterface(t *testing.T) {
 	  },
 	  "components":{
 		"schemas":{
-		  "FormDataMultipartFile":{"type":"string","format":"binary","nullable":true},
 		  "FormDataOpenapi3TestReq":{
 			"type":"object",
-			"properties":{"upload1":{"$ref":"#/components/schemas/FormDataMultipartFile"}}
-		  }
+			"properties":{"upload1":{"$ref":"#/components/schemas/MultipartFile"}}
+		  },
+		  "MultipartFile":{"type":"string","format":"binary","nullable":true}
 		}
 	  }
 	}`, s)
@@ -97,9 +97,9 @@ func TestReflector_SetRequest(t *testing.T) {
 	b, err := assertjson.MarshalIndentCompact(s, "", " ", 120)
 	assert.NoError(t, err)
 
-	require.NoError(t, os.WriteFile("_testdata/openapi_req2_last_run.json", b, 0o600))
+	require.NoError(t, os.WriteFile("testdata/openapi_req2_last_run.json", b, 0o600))
 
-	expected, err := os.ReadFile("_testdata/openapi_req2.json")
+	expected, err := os.ReadFile("testdata/openapi_req2.json")
 	require.NoError(t, err)
 
 	assertjson.Equal(t, expected, b)
@@ -128,7 +128,7 @@ func TestReflector_SetJSONResponse(t *testing.T) {
 	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")
+	expected, err := os.ReadFile("testdata/req_schema.json")
 	require.NoError(t, err)
 	assertjson.EqualMarshal(t, expected, js)
 
@@ -145,9 +145,9 @@ func TestReflector_SetJSONResponse(t *testing.T) {
 	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")
+	expected, err = os.ReadFile("testdata/resp_schema.json")
 	require.NoError(t, err)
 	assertjson.EqualMarshal(t, expected, js)
 
@@ -162,9 +162,9 @@ func TestReflector_SetJSONResponse(t *testing.T) {
 	b, err := assertjson.MarshalIndentCompact(s, "", " ", 120)
 	assert.NoError(t, err)
 
-	require.NoError(t, os.WriteFile("_testdata/openapi_last_run.json", b, 0o600))
+	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)
@@ -311,31 +311,39 @@ func TestReflector_SetupRequest(t *testing.T) {
 	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"}}
-		   }
+	  "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/MultipartFile"},
+					  "value3":{"type":"number"}
+					}
+				  }
+				}
+			  }
+			},
+			"responses":{"204":{"description":"No Content"}}
 		  }
-		 }
-		},
-		"responses":{"204":{"description":"No Content"}}
-	   }
+		}
+	  },
+	  "components":{
+		"schemas":{"MultipartFile":{"type":"string","format":"binary","nullable":true}}
 	  }
-	 },
-	 "components":{"schemas":{"FormDataMultipartFile":{"type":"string","format":"binary","nullable":true}}}
 	}`, s)
 }
 
diff --git a/openapi3/reflect_test.go b/openapi3/reflect_test.go
index 24eba68..eafba5e 100644
--- a/openapi3/reflect_test.go
+++ b/openapi3/reflect_test.go
@@ -112,9 +112,9 @@ func TestReflector_AddOperation_request_array(t *testing.T) {
 	b, err := assertjson.MarshalIndentCompact(s, "", " ", 120)
 	assert.NoError(t, err)
 
-	require.NoError(t, os.WriteFile("_testdata/openapi_req_array_last_run.json", b, 0o600))
+	require.NoError(t, os.WriteFile("testdata/openapi_req_array_last_run.json", b, 0o600))
 
-	expected, err := os.ReadFile("_testdata/openapi_req_array.json")
+	expected, err := os.ReadFile("testdata/openapi_req_array.json")
 	require.NoError(t, err)
 
 	assertjson.Equal(t, expected, b)
@@ -149,11 +149,11 @@ func TestReflector_AddOperation_uploadInterface(t *testing.T) {
 	  },
 	  "components":{
 		"schemas":{
-		  "FormDataMultipartFile":{"type":"string","format":"binary","nullable":true},
 		  "FormDataOpenapi3TestReq":{
 			"type":"object",
-			"properties":{"upload1":{"$ref":"#/components/schemas/FormDataMultipartFile"}}
-		  }
+			"properties":{"upload1":{"$ref":"#/components/schemas/MultipartFile"}}
+		  },
+		  "MultipartFile":{"type":"string","format":"binary","nullable":true}
 		}
 	  }
 	}`, reflector.Spec)
@@ -180,9 +180,9 @@ func TestReflector_AddOperation_request(t *testing.T) {
 	b, err := assertjson.MarshalIndentCompact(s, "", " ", 120)
 	assert.NoError(t, err)
 
-	require.NoError(t, os.WriteFile("_testdata/openapi_req_last_run.json", b, 0o600))
+	require.NoError(t, os.WriteFile("testdata/openapi_req_last_run.json", b, 0o600))
 
-	expected, err := os.ReadFile("_testdata/openapi_req.json")
+	expected, err := os.ReadFile("testdata/openapi_req.json")
 	require.NoError(t, err)
 
 	assertjson.Equal(t, expected, b)
@@ -222,7 +222,7 @@ func TestReflector_AddOperation_JSON_response(t *testing.T) {
 
 	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")
+		expected, err := os.ReadFile("testdata/req_schema.json")
 		require.NoError(t, err)
 		assertjson.EqualMarshal(t, expected, js)
 
@@ -243,9 +243,9 @@ func TestReflector_AddOperation_JSON_response(t *testing.T) {
 		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")
+		expected, err := os.ReadFile("testdata/resp_schema.json")
 		require.NoError(t, err)
 		assertjson.EqualMarshal(t, expected, js)
 
@@ -263,9 +263,9 @@ func TestReflector_AddOperation_JSON_response(t *testing.T) {
 	b, err := assertjson.MarshalIndentCompact(s, "", " ", 120)
 	assert.NoError(t, err)
 
-	require.NoError(t, os.WriteFile("_testdata/openapi_last_run.json", b, 0o600))
+	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)
@@ -429,31 +429,39 @@ func TestReflector_AddOperation_setup_request(t *testing.T) {
 	require.NoError(t, reflector.AddOperation(oc))
 
 	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"}}
-		   }
+	  "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/MultipartFile"},
+					  "value3":{"type":"number"}
+					}
+				  }
+				}
+			  }
+			},
+			"responses":{"204":{"description":"No Content"}}
 		  }
-		 }
-		},
-		"responses":{"204":{"description":"No Content"}}
-	   }
+		}
+	  },
+	  "components":{
+		"schemas":{"MultipartFile":{"type":"string","format":"binary","nullable":true}}
 	  }
-	 },
-	 "components":{"schemas":{"FormDataMultipartFile":{"type":"string","format":"binary","nullable":true}}}
 	}`, s)
 }
 
@@ -1024,3 +1032,82 @@ func TestReflector_AddOperation_contentUnitPreparer(t *testing.T) {
 	  }
 	}`, r.SpecSchema())
 }
+
+func TestReflector_AddOperation_defName(t *testing.T) {
+	r := openapi3.NewReflector()
+	oc, err := r.NewOperationContext(http.MethodPost, "/foo")
+	require.NoError(t, err)
+
+	type (
+		simpleString  string
+		specialString string
+	)
+
+	specialStringDef := jsonschema.Schema{}
+	specialStringDef.AddType(jsonschema.String)
+	specialStringDef.WithExamples("xy5abcd4sq9s")
+	specialStringDef.MinLength = 12
+	specialStringDef.WithMaxLength(12)
+	specialStringDef.WithDescription("Very special.")
+	r.AddTypeMapping(specialString(""), specialStringDef)
+
+	type reqForm struct {
+		Simple  simpleString  `formData:"simple"`
+		Special specialString `formData:"special"`
+		Foo     int           `formData:"foo"`
+		Bar     float64       `header:"bar"`
+	}
+
+	type reqJSON struct {
+		Simple  simpleString  `json:"simple"`
+		Special specialString `json:"special"`
+		Foo     int           `json:"foo"`
+	}
+
+	oc.AddReqStructure(reqForm{})
+	oc.AddReqStructure(reqJSON{})
+
+	require.NoError(t, r.AddOperation(oc))
+
+	assertjson.EqMarshal(t, `{
+	  "openapi":"3.0.3","info":{"title":"","version":""},
+	  "paths":{
+		"/foo":{
+		  "post":{
+			"parameters":[{"name":"bar","in":"header","schema":{"type":"number"}}],
+			"requestBody":{
+			  "content":{
+				"application/json":{"schema":{"$ref":"#/components/schemas/Openapi3TestReqJSON"}},
+				"application/x-www-form-urlencoded":{
+				  "schema":{"$ref":"#/components/schemas/FormDataOpenapi3TestReqForm"}
+				}
+			  }
+			},
+			"responses":{"204":{"description":"No Content"}}
+		  }
+		}
+	  },
+	  "components":{
+		"schemas":{
+		  "FormDataOpenapi3TestReqForm":{
+			"type":"object",
+			"properties":{
+			  "foo":{"type":"integer"},"simple":{"type":"string"},
+			  "special":{"$ref":"#/components/schemas/Openapi3TestSpecialString"}
+			}
+		  },
+		  "Openapi3TestReqJSON":{
+			"type":"object",
+			"properties":{
+			  "foo":{"type":"integer"},"simple":{"type":"string"},
+			  "special":{"$ref":"#/components/schemas/Openapi3TestSpecialString"}
+			}
+		  },
+		  "Openapi3TestSpecialString":{
+			"maxLength":12,"minLength":12,"type":"string",
+			"description":"Very special.","example":"xy5abcd4sq9s"
+		  }
+		}
+	  }
+	}`, r.Spec)
+}
diff --git a/openapi3/_testdata/openapi.json b/openapi3/testdata/openapi.json
similarity index 92%
rename from openapi3/_testdata/openapi.json
rename to openapi3/testdata/openapi.json
index 79ac852..da3429d 100644
--- a/openapi3/_testdata/openapi.json
+++ b/openapi3/testdata/openapi.json
@@ -92,16 +92,15 @@
  },
  "components":{
   "schemas":{
-   "FormDataMultipartFile":{"type":"string","format":"binary","nullable":true},
-   "FormDataMultipartFileHeader":{"type":"string","format":"binary","nullable":true},
    "FormDataOpenapi3TestReq":{
     "type":"object",
     "properties":{
-     "in_form1":{"type":"string"},"in_form2":{"type":"string"},
-     "upload1":{"$ref":"#/components/schemas/FormDataMultipartFile"},
-     "upload2":{"$ref":"#/components/schemas/FormDataMultipartFileHeader"}
+     "in_form1":{"type":"string"},"in_form2":{"type":"string"},"upload1":{"$ref":"#/components/schemas/MultipartFile"},
+     "upload2":{"$ref":"#/components/schemas/MultipartFileHeader"}
     }
    },
+   "MultipartFile":{"type":"string","format":"binary","nullable":true},
+   "MultipartFileHeader":{"type":"string","format":"binary","nullable":true},
    "Openapi3TestReq":{"type":"object","properties":{"in_body1":{"type":"integer"},"in_body2":{"type":"string"}}},
    "Openapi3TestResp":{
     "title":"Sample Response","type":"object",
diff --git a/openapi3/_testdata/openapi_req.json b/openapi3/testdata/openapi_req.json
similarity index 100%
rename from openapi3/_testdata/openapi_req.json
rename to openapi3/testdata/openapi_req.json
diff --git a/openapi3/_testdata/openapi_req2.json b/openapi3/testdata/openapi_req2.json
similarity index 100%
rename from openapi3/_testdata/openapi_req2.json
rename to openapi3/testdata/openapi_req2.json
diff --git a/openapi3/_testdata/openapi_req_array.json b/openapi3/testdata/openapi_req_array.json
similarity index 100%
rename from openapi3/_testdata/openapi_req_array.json
rename to openapi3/testdata/openapi_req_array.json
diff --git a/openapi3/testdata/req_schema.json b/openapi3/testdata/req_schema.json
new file mode 100644
index 0000000..bfc5d19
--- /dev/null
+++ b/openapi3/testdata/req_schema.json
@@ -0,0 +1,14 @@
+{
+  "properties":{
+    "in_form1":{"type":"string"},"in_form2":{"type":"string"},
+    "upload1":{"$ref":"#/components/schemas/MultipartFile"},
+    "upload2":{"$ref":"#/components/schemas/MultipartFileHeader"}
+  },
+  "type":"object",
+  "components":{
+    "schemas":{
+      "MultipartFile":{"type":["string","null"],"format":"binary"},
+      "MultipartFileHeader":{"type":["string","null"],"format":"binary"}
+    }
+  }
+}
diff --git a/openapi3/_testdata/resp_schema.json b/openapi3/testdata/resp_schema.json
similarity index 100%
rename from openapi3/_testdata/resp_schema.json
rename to openapi3/testdata/resp_schema.json
diff --git a/openapi3/_testdata/swgui/go.mod b/openapi3/testdata/swgui/go.mod
similarity index 100%
rename from openapi3/_testdata/swgui/go.mod
rename to openapi3/testdata/swgui/go.mod
diff --git a/openapi3/_testdata/swgui/go.sum b/openapi3/testdata/swgui/go.sum
similarity index 100%
rename from openapi3/_testdata/swgui/go.sum
rename to openapi3/testdata/swgui/go.sum
diff --git a/openapi3/_testdata/swgui/swgui.go b/openapi3/testdata/swgui/swgui.go
similarity index 100%
rename from openapi3/_testdata/swgui/swgui.go
rename to openapi3/testdata/swgui/swgui.go
diff --git a/openapi31/_testdata/req_schema.json b/openapi31/_testdata/req_schema.json
deleted file mode 100644
index 7fbe50b..0000000
--- a/openapi31/_testdata/req_schema.json
+++ /dev/null
@@ -1,14 +0,0 @@
-{
-  "properties":{
-    "in_form1":{"type":"string"},"in_form2":{"type":"string"},
-    "upload1":{"$ref":"#/components/schemas/FormDataMultipartFile"},
-    "upload2":{"$ref":"#/components/schemas/FormDataMultipartFileHeader"}
-  },
-  "type":"object",
-  "components":{
-    "schemas":{
-      "FormDataMultipartFile":{"type":["null","string"],"format":"binary"},
-      "FormDataMultipartFileHeader":{"type":["null","string"],"format":"binary"}
-    }
-  }
-}
diff --git a/openapi31/entities_test.go b/openapi31/entities_test.go
index 586887f..759a621 100644
--- a/openapi31/entities_test.go
+++ b/openapi31/entities_test.go
@@ -9,7 +9,7 @@ import (
 )
 
 func TestSpec_UnmarshalYAML(t *testing.T) {
-	bytes, err := os.ReadFile("_testdata/albums_api.yaml")
+	bytes, err := os.ReadFile("testdata/albums_api.yaml")
 	require.NoError(t, err)
 
 	refl := openapi31.NewReflector()
diff --git a/openapi31/reflect_test.go b/openapi31/reflect_test.go
index 1fd82fd..fd6f29f 100644
--- a/openapi31/reflect_test.go
+++ b/openapi31/reflect_test.go
@@ -112,9 +112,9 @@ func TestReflector_AddOperation_request_array(t *testing.T) {
 	b, err := assertjson.MarshalIndentCompact(s, "", " ", 120)
 	assert.NoError(t, err)
 
-	require.NoError(t, os.WriteFile("_testdata/openapi_req_array_last_run.json", b, 0o600))
+	require.NoError(t, os.WriteFile("testdata/openapi_req_array_last_run.json", b, 0o600))
 
-	expected, err := os.ReadFile("_testdata/openapi_req_array.json")
+	expected, err := os.ReadFile("testdata/openapi_req_array.json")
 	require.NoError(t, err)
 
 	assertjson.Equal(t, expected, b)
@@ -140,7 +140,9 @@ func TestReflector_AddOperation_uploadInterface(t *testing.T) {
 		  "post":{
 			"requestBody":{
 			  "content":{
-				"multipart/form-data":{"schema":{"$ref":"#/components/schemas/FormDataOpenapi31TestReq"}}
+				"multipart/form-data":{
+				  "schema":{"$ref":"#/components/schemas/FormDataOpenapi31TestReq"}
+				}
 			  }
 			},
 			"responses":{"204":{"description":"No Content"}}
@@ -149,11 +151,11 @@ func TestReflector_AddOperation_uploadInterface(t *testing.T) {
 	  },
 	  "components":{
 		"schemas":{
-		  "FormDataMultipartFile":{"type":["null","string"],"format":"binary"},
 		  "FormDataOpenapi31TestReq":{
-			"type":"object",
-			"properties":{"upload1":{"$ref":"#/components/schemas/FormDataMultipartFile"}}
-		  }
+			"properties":{"upload1":{"$ref":"#/components/schemas/MultipartFile"}},
+			"type":"object"
+		  },
+		  "MultipartFile":{"format":"binary","type":["null","string"]}
 		}
 	  }
 	}`, reflector.Spec)
@@ -180,9 +182,9 @@ func TestReflector_AddOperation_request(t *testing.T) {
 	b, err := assertjson.MarshalIndentCompact(s, "", " ", 120)
 	assert.NoError(t, err)
 
-	require.NoError(t, os.WriteFile("_testdata/openapi_req_last_run.json", b, 0o600))
+	require.NoError(t, os.WriteFile("testdata/openapi_req_last_run.json", b, 0o600))
 
-	expected, err := os.ReadFile("_testdata/openapi_req.json")
+	expected, err := os.ReadFile("testdata/openapi_req.json")
 	require.NoError(t, err)
 
 	assertjson.Equal(t, expected, b)
@@ -222,7 +224,7 @@ func TestReflector_AddOperation_JSON_response(t *testing.T) {
 
 	require.NoError(t, s.SetupOperation(http.MethodPost, "/somewhere/{in_path}", func(op *openapi31.Operation) error {
 		js := openapi31.ToJSONSchema(op.RequestBody.RequestBody.Content["multipart/form-data"].Schema, s)
-		expected, err := os.ReadFile("_testdata/req_schema.json")
+		expected, err := os.ReadFile("testdata/req_schema.json")
 		require.NoError(t, err)
 		assertjson.EqualMarshal(t, expected, js)
 
@@ -243,9 +245,9 @@ func TestReflector_AddOperation_JSON_response(t *testing.T) {
 		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")
+		expected, err := os.ReadFile("testdata/resp_schema.json")
 		require.NoError(t, err)
 		assertjson.EqualMarshal(t, expected, js)
 
@@ -263,9 +265,9 @@ func TestReflector_AddOperation_JSON_response(t *testing.T) {
 	b, err := assertjson.MarshalIndentCompact(s, "", " ", 120)
 	assert.NoError(t, err)
 
-	require.NoError(t, os.WriteFile("_testdata/openapi_last_run.json", b, 0o600))
+	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)
@@ -463,7 +465,7 @@ func TestReflector_AddOperation_setup_request(t *testing.T) {
 				"multipart/form-data":{
 				  "schema":{
 					"properties":{
-					  "upload6":{"$ref":"#/components/schemas/FormDataMultipartFile"},
+					  "upload6":{"$ref":"#/components/schemas/MultipartFile"},
 					  "value3":{"type":"number"}
 					},
 					"type":"object"
@@ -475,9 +477,7 @@ func TestReflector_AddOperation_setup_request(t *testing.T) {
 		  }
 		}
 	  },
-	  "components":{
-		"schemas":{"FormDataMultipartFile":{"format":"binary","type":["null","string"]}}
-	  }
+	  "components":{"schemas":{"MultipartFile":{"format":"binary","type":["null","string"]}}}
 	}`, s)
 }
 
@@ -1056,3 +1056,82 @@ func TestReflector_AddOperation_contentUnitPreparer(t *testing.T) {
 	  }
 	}`, r.SpecSchema())
 }
+
+func TestReflector_AddOperation_defName(t *testing.T) {
+	r := openapi31.NewReflector()
+	oc, err := r.NewOperationContext(http.MethodPost, "/foo")
+	require.NoError(t, err)
+
+	type (
+		simpleString  string
+		specialString string
+	)
+
+	specialStringDef := jsonschema.Schema{}
+	specialStringDef.AddType(jsonschema.String)
+	specialStringDef.WithExamples("xy5abcd4sq9s")
+	specialStringDef.MinLength = 12
+	specialStringDef.WithMaxLength(12)
+	specialStringDef.WithDescription("Very special.")
+	r.AddTypeMapping(specialString(""), specialStringDef)
+
+	type reqForm struct {
+		Simple  simpleString  `formData:"simple"`
+		Special specialString `formData:"special"`
+		Foo     int           `formData:"foo"`
+		Bar     float64       `header:"bar"`
+	}
+
+	type reqJSON struct {
+		Simple  simpleString  `json:"simple"`
+		Special specialString `json:"special"`
+		Foo     int           `json:"foo"`
+	}
+
+	oc.AddReqStructure(reqForm{})
+	oc.AddReqStructure(reqJSON{})
+
+	require.NoError(t, r.AddOperation(oc))
+
+	assertjson.EqMarshal(t, `{
+	  "openapi":"3.1.0","info":{"title":"","version":""},
+	  "paths":{
+		"/foo":{
+		  "post":{
+			"parameters":[{"name":"bar","in":"header","schema":{"type":"number"}}],
+			"requestBody":{
+			  "content":{
+				"application/json":{"schema":{"$ref":"#/components/schemas/Openapi31TestReqJSON"}},
+				"application/x-www-form-urlencoded":{
+				  "schema":{"$ref":"#/components/schemas/FormDataOpenapi31TestReqForm"}
+				}
+			  }
+			},
+			"responses":{"204":{"description":"No Content"}}
+		  }
+		}
+	  },
+	  "components":{
+		"schemas":{
+		  "FormDataOpenapi31TestReqForm":{
+			"properties":{
+			  "foo":{"type":"integer"},"simple":{"type":"string"},
+			  "special":{"$ref":"#/components/schemas/Openapi31TestSpecialString"}
+			},
+			"type":"object"
+		  },
+		  "Openapi31TestReqJSON":{
+			"properties":{
+			  "foo":{"type":"integer"},"simple":{"type":"string"},
+			  "special":{"$ref":"#/components/schemas/Openapi31TestSpecialString"}
+			},
+			"type":"object"
+		  },
+		  "Openapi31TestSpecialString":{
+			"description":"Very special.","examples":["xy5abcd4sq9s"],
+			"maxLength":12,"minLength":12,"type":"string"
+		  }
+		}
+	  }
+	}`, r.Spec)
+}
diff --git a/openapi31/_testdata/albums_api.yaml b/openapi31/testdata/albums_api.yaml
similarity index 100%
rename from openapi31/_testdata/albums_api.yaml
rename to openapi31/testdata/albums_api.yaml
diff --git a/openapi31/_testdata/openapi.json b/openapi31/testdata/openapi.json
similarity index 93%
rename from openapi31/_testdata/openapi.json
rename to openapi31/testdata/openapi.json
index 34c2276..f4e7745 100644
--- a/openapi31/_testdata/openapi.json
+++ b/openapi31/testdata/openapi.json
@@ -92,16 +92,15 @@
  },
  "components":{
   "schemas":{
-   "FormDataMultipartFile":{"format":"binary","type":["null","string"]},
-   "FormDataMultipartFileHeader":{"format":"binary","type":["null","string"]},
    "FormDataOpenapi31TestReq":{
     "properties":{
-     "in_form1":{"type":"string"},"in_form2":{"type":"string"},
-     "upload1":{"$ref":"#/components/schemas/FormDataMultipartFile"},
-     "upload2":{"$ref":"#/components/schemas/FormDataMultipartFileHeader"}
+     "in_form1":{"type":"string"},"in_form2":{"type":"string"},"upload1":{"$ref":"#/components/schemas/MultipartFile"},
+     "upload2":{"$ref":"#/components/schemas/MultipartFileHeader"}
     },
     "type":"object"
    },
+   "MultipartFile":{"format":"binary","type":["null","string"]},
+   "MultipartFileHeader":{"format":"binary","type":["null","string"]},
    "Openapi31TestReq":{"properties":{"in_body1":{"type":"integer"},"in_body2":{"type":"string"}},"type":"object"},
    "Openapi31TestResp":{
     "description":"This is a sample response.",
diff --git a/openapi31/_testdata/openapi_req.json b/openapi31/testdata/openapi_req.json
similarity index 100%
rename from openapi31/_testdata/openapi_req.json
rename to openapi31/testdata/openapi_req.json
diff --git a/openapi31/_testdata/openapi_req_array.json b/openapi31/testdata/openapi_req_array.json
similarity index 100%
rename from openapi31/_testdata/openapi_req_array.json
rename to openapi31/testdata/openapi_req_array.json
diff --git a/openapi31/testdata/req_schema.json b/openapi31/testdata/req_schema.json
new file mode 100644
index 0000000..2f36cc1
--- /dev/null
+++ b/openapi31/testdata/req_schema.json
@@ -0,0 +1,14 @@
+{
+  "properties":{
+    "in_form1":{"type":"string"},"in_form2":{"type":"string"},
+    "upload1":{"$ref":"#/components/schemas/MultipartFile"},
+    "upload2":{"$ref":"#/components/schemas/MultipartFileHeader"}
+  },
+  "type":"object",
+  "components":{
+    "schemas":{
+      "MultipartFile":{"type":["null","string"],"format":"binary"},
+      "MultipartFileHeader":{"type":["null","string"],"format":"binary"}
+    }
+  }
+}