Repository: getkin/kin-openapi
Pull request #394 (2021-08-04 09:39:59)

feature: Add more discriminator error messages and return specific er…
…ror when possible

fenollp: LGTM.

Do please add a test that fits your needs (if you're looking for specific pieces of information out of a schema validation failure, make sure that your test looks for a set of specific strings). This way it's easier to port code and error messages over when this lib switches to a different schema handling lib (for openapi >= 3.1 anyway).
rtfpessoa: I think regarding changes it should be fine now. I will now work on the tests.
rtfpessoa: @fenollp I think the tests cover pretty much what I wanted to see. Let me know if you have suggestions on how to clean anything or reorganize.

Patch:
diff --git a/.github/workflows/go.yml b/.github/workflows/go.yml
index be19f94..4a5b87c 100644
--- a/.github/workflows/go.yml
+++ b/.github/workflows/go.yml
@@ -68,15 +68,13 @@ jobs:
     - run: go test -v -run TestRaceyPatternSchema -race ./...
       env:
         CGO_ENABLED: '1'
-    - run: |
-        cd openapi3/testdata
-        go get -u -v github.com/getkin/kin-openapi
-        go test -tags with_embed ./...
-        git --no-pager diff && git checkout -- .
-        cd -
-      if: matrix.go != '1.14'
     - if: runner.os == 'Linux'
       run: git --no-pager diff && [[ $(git --no-pager diff --name-only | wc -l) = 0 ]]
+    - run: |
+        cp openapi3/testdata/load_with_go_embed_test.go openapi3/
+        cat go.mod | sed 's%go 1.14%go 1.16%' >gomod && mv gomod go.mod
+        go test ./...
+      if: matrix.go != '1.14'
 
 
     - if: runner.os == 'Linux'
diff --git a/.gitignore b/.gitignore
index 31ab03f..caf9547 100644
--- a/.gitignore
+++ b/.gitignore
@@ -18,3 +18,5 @@
 
  # IntelliJ / GoLand
 .idea
+
+/openapi3/load_with_go_embed_test.go
diff --git a/jsoninfo/field_info.go b/jsoninfo/field_info.go
index d2ad505..2382b73 100644
--- a/jsoninfo/field_info.go
+++ b/jsoninfo/field_info.go
@@ -21,6 +21,9 @@ type FieldInfo struct {
 }
 
 func AppendFields(fields []FieldInfo, parentIndex []int, t reflect.Type) []FieldInfo {
+	if t.Kind() == reflect.Ptr {
+		t = t.Elem()
+	}
 	// For each field
 	numField := t.NumField()
 iteration:
diff --git a/openapi3/loader.go b/openapi3/loader.go
index da34797..c42d8ba 100644
--- a/openapi3/loader.go
+++ b/openapi3/loader.go
@@ -35,6 +35,8 @@ type Loader struct {
 
 	Context context.Context
 
+	rootDir string
+
 	visitedPathItemRefs map[string]struct{}
 
 	visitedDocuments map[string]*T
@@ -66,6 +68,7 @@ func (loader *Loader) LoadFromURI(location *url.URL) (*T, error) {
 
 // LoadFromFile loads a spec from a local file path
 func (loader *Loader) LoadFromFile(location string) (*T, error) {
+	loader.rootDir = path.Dir(location)
 	return loader.LoadFromURI(&url.URL{Path: filepath.ToSlash(location)})
 }
 
@@ -415,6 +418,14 @@ func drillIntoField(cursor interface{}, fieldName string) (interface{}, error) {
 	}
 }
 
+func (loader *Loader) documentPathForRecursiveRef(current *url.URL, resolvedRef string) *url.URL {
+	if loader.rootDir == "" {
+		return current
+	}
+	return &url.URL{Path: path.Join(loader.rootDir, resolvedRef)}
+
+}
+
 func (loader *Loader) resolveRef(doc *T, ref string, path *url.URL) (*T, string, *url.URL, error) {
 	if ref != "" && ref[0] == '#' {
 		return doc, ref, path, nil
@@ -474,6 +485,7 @@ func (loader *Loader) resolveHeaderRef(doc *T, component *HeaderRef, documentPat
 				return err
 			}
 			component.Value = resolved.Value
+			documentPath = loader.documentPathForRecursiveRef(documentPath, resolved.Ref)
 		}
 	}
 	value := component.Value
@@ -521,6 +533,7 @@ func (loader *Loader) resolveParameterRef(doc *T, component *ParameterRef, docum
 				return err
 			}
 			component.Value = resolved.Value
+			documentPath = loader.documentPathForRecursiveRef(documentPath, resolved.Ref)
 		}
 	}
 	value := component.Value
@@ -577,6 +590,7 @@ func (loader *Loader) resolveRequestBodyRef(doc *T, component *RequestBodyRef, d
 				return err
 			}
 			component.Value = resolved.Value
+			documentPath = loader.documentPathForRecursiveRef(documentPath, resolved.Ref)
 		}
 	}
 	value := component.Value
@@ -632,6 +646,7 @@ func (loader *Loader) resolveResponseRef(doc *T, component *ResponseRef, documen
 				return err
 			}
 			component.Value = resolved.Value
+			documentPath = loader.documentPathForRecursiveRef(documentPath, resolved.Ref)
 		}
 	}
 	value := component.Value
@@ -701,6 +716,7 @@ func (loader *Loader) resolveSchemaRef(doc *T, component *SchemaRef, documentPat
 				return err
 			}
 			component.Value = resolved.Value
+			documentPath = loader.documentPathForRecursiveRef(documentPath, resolved.Ref)
 		}
 	}
 	value := component.Value
@@ -778,6 +794,7 @@ func (loader *Loader) resolveSecuritySchemeRef(doc *T, component *SecurityScheme
 				return err
 			}
 			component.Value = resolved.Value
+			documentPath = loader.documentPathForRecursiveRef(documentPath, resolved.Ref)
 		}
 	}
 	return nil
@@ -814,6 +831,7 @@ func (loader *Loader) resolveExampleRef(doc *T, component *ExampleRef, documentP
 				return err
 			}
 			component.Value = resolved.Value
+			documentPath = loader.documentPathForRecursiveRef(documentPath, resolved.Ref)
 		}
 	}
 	return nil
@@ -841,6 +859,7 @@ func (loader *Loader) resolveCallbackRef(doc *T, component *CallbackRef, documen
 				return err
 			}
 			component.Value = resolved.Value
+			documentPath = loader.documentPathForRecursiveRef(documentPath, resolved.Ref)
 		}
 	}
 	value := component.Value
@@ -938,6 +957,7 @@ func (loader *Loader) resolveLinkRef(doc *T, component *LinkRef, documentPath *u
 				return err
 			}
 			component.Value = resolved.Value
+			documentPath = loader.documentPathForRecursiveRef(documentPath, resolved.Ref)
 		}
 	}
 	return nil
diff --git a/openapi3/schema.go b/openapi3/schema.go
index 0010627..6878ad3 100644
--- a/openapi3/schema.go
+++ b/openapi3/schema.go
@@ -824,10 +824,7 @@ func (schema *Schema) visitSetOperations(settings *schemaValidationSettings, val
 		if v == nil {
 			return foundUnresolvedRef(ref.Ref)
 		}
-		var oldfailfast bool
-		oldfailfast, settings.failfast = settings.failfast, true
 		err := v.visitJSON(settings, value)
-		settings.failfast = oldfailfast
 		if err == nil {
 			if settings.failfast {
 				return errSchema
@@ -841,33 +838,53 @@ func (schema *Schema) visitSetOperations(settings *schemaValidationSettings, val
 	}
 
 	if v := schema.OneOf; len(v) > 0 {
+		var discriminatorRef string
+		if schema.Discriminator != nil {
+			pn := schema.Discriminator.PropertyName
+			if valuemap, okcheck := value.(map[string]interface{}); okcheck {
+				discriminatorVal, okcheck := valuemap[pn]
+				if !okcheck {
+					return errors.New("input does not contain the discriminator property")
+				}
+
+				if discriminatorRef, okcheck = schema.Discriminator.Mapping[discriminatorVal.(string)]; len(schema.Discriminator.Mapping) > 0 && !okcheck {
+					return errors.New("input does not contain a valid discriminator value")
+				}
+			}
+		}
+
 		ok := 0
+		validationErrors := []error{}
 		for _, item := range v {
 			v := item.Value
 			if v == nil {
 				return foundUnresolvedRef(item.Ref)
 			}
-			var oldfailfast bool
-			oldfailfast, settings.failfast = settings.failfast, true
+
+			if discriminatorRef != "" && discriminatorRef != item.Ref {
+				continue
+			}
+
 			err := v.visitJSON(settings, value)
-			settings.failfast = oldfailfast
-			if err == nil {
-				if schema.Discriminator != nil {
-					pn := schema.Discriminator.PropertyName
-					if valuemap, okcheck := value.(map[string]interface{}); okcheck {
-						if discriminatorVal, okcheck := valuemap[pn]; okcheck == true {
-							mapref, okcheck := schema.Discriminator.Mapping[discriminatorVal.(string)]
-							if okcheck && mapref == item.Ref {
-								ok++
-							}
-						}
-					}
-				} else {
-					ok++
-				}
+			if err != nil {
+				validationErrors = append(validationErrors, err)
+				continue
 			}
+
+			ok++
 		}
+
 		if ok != 1 {
+			if len(validationErrors) > 1 {
+				errorMessage := ""
+				for _, err := range validationErrors {
+					if errorMessage != "" {
+						errorMessage += " Or "
+					}
+					errorMessage += err.Error()
+				}
+				return errors.New("doesn't match schema due to: " + errorMessage)
+			}
 			if settings.failfast {
 				return errSchema
 			}
@@ -878,7 +895,10 @@ func (schema *Schema) visitSetOperations(settings *schemaValidationSettings, val
 			}
 			if ok > 1 {
 				e.Origin = ErrOneOfConflict
+			} else if len(validationErrors) == 1 {
+				e.Origin = validationErrors[0]
 			}
+
 			return e
 		}
 	}
@@ -890,10 +910,7 @@ func (schema *Schema) visitSetOperations(settings *schemaValidationSettings, val
 			if v == nil {
 				return foundUnresolvedRef(item.Ref)
 			}
-			var oldfailfast bool
-			oldfailfast, settings.failfast = settings.failfast, true
 			err := v.visitJSON(settings, value)
-			settings.failfast = oldfailfast
 			if err == nil {
 				ok = true
 				break
@@ -916,10 +933,7 @@ func (schema *Schema) visitSetOperations(settings *schemaValidationSettings, val
 		if v == nil {
 			return foundUnresolvedRef(item.Ref)
 		}
-		var oldfailfast bool
-		oldfailfast, settings.failfast = settings.failfast, false
 		err := v.visitJSON(settings, value)
-		settings.failfast = oldfailfast
 		if err != nil {
 			if settings.failfast {
 				return errSchema
diff --git a/openapi3filter/req_resp_decoder.go b/openapi3filter/req_resp_decoder.go
index b9b0cd8..2d2728b 100644
--- a/openapi3filter/req_resp_decoder.go
+++ b/openapi3filter/req_resp_decoder.go
@@ -809,6 +809,11 @@ const prefixUnsupportedCT = "unsupported content type"
 // The function returns ParseError when a body is invalid.
 func decodeBody(body io.Reader, header http.Header, schema *openapi3.SchemaRef, encFn EncodingFn) (interface{}, error) {
 	contentType := header.Get(headerCT)
+	if contentType == "" {
+		if _, ok := body.(*multipart.Part); ok {
+			contentType = "text/plain"
+		}
+	}
 	mediaType := parseMediaType(contentType)
 	decoder, ok := bodyDecoders[mediaType]
 	if !ok {
diff --git a/routers/gorillamux/router.go b/routers/gorillamux/router.go
index 4011932..31d8dc8 100644
--- a/routers/gorillamux/router.go
+++ b/routers/gorillamux/router.go
@@ -43,7 +43,7 @@ func NewRouter(doc *openapi3.T) (routers.Router, error) {
 			return nil, err
 		}
 		path := bDecode(u.EscapedPath())
-		if path[len(path)-1] == '/' {
+		if len(path) > 0 && path[len(path)-1] == '/' {
 			path = path[:len(path)-1]
 		}
 		servers = append(servers, srv{
@@ -56,7 +56,7 @@ func NewRouter(doc *openapi3.T) (routers.Router, error) {
 	if len(servers) == 0 {
 		servers = append(servers, srv{})
 	}
-	muxRouter := mux.NewRouter() /*.UseEncodedPath()?*/
+	muxRouter := mux.NewRouter().UseEncodedPath()
 	r := &Router{}
 	for _, path := range orderedPaths(doc.Paths) {
 		pathItem := doc.Paths[path]


Test patch:
diff --git a/openapi3/loader_read_from_uri_func_test.go b/openapi3/loader_read_from_uri_func_test.go
index 72d4a95..8fee2f4 100644
--- a/openapi3/loader_read_from_uri_func_test.go
+++ b/openapi3/loader_read_from_uri_func_test.go
@@ -20,7 +20,7 @@ func TestLoaderReadFromURIFunc(t *testing.T) {
 	require.NoError(t, err)
 	require.NotNil(t, doc)
 	require.NoError(t, doc.Validate(loader.Context))
-	require.Equal(t, "bar", doc.Paths["/foo"].Get.Responses.Get(200).Value.Content.Get("application/json").Schema.Value.Properties["foo"].Value.Properties["bar"].Value.Items.Value.Example)
+	require.Equal(t, "bar", doc.Paths["/foo"].Get.Responses.Get(200).Value.Content.Get("application/json").Schema.Value.Properties["foo2"].Value.Properties["foo"].Value.Properties["bar"].Value.Example)
 }
 
 type multipleSourceLoaderExample struct {
diff --git a/openapi3/loader_recursive_ref_test.go b/openapi3/loader_recursive_ref_test.go
index bc1f24b..bd65903 100644
--- a/openapi3/loader_recursive_ref_test.go
+++ b/openapi3/loader_recursive_ref_test.go
@@ -13,5 +13,5 @@ func TestLoaderSupportsRecursiveReference(t *testing.T) {
 	require.NoError(t, err)
 	require.NotNil(t, doc)
 	require.NoError(t, doc.Validate(loader.Context))
-	require.Equal(t, "bar", doc.Paths["/foo"].Get.Responses.Get(200).Value.Content.Get("application/json").Schema.Value.Properties["foo"].Value.Properties["bar"].Value.Items.Value.Example)
+	require.Equal(t, "bar", doc.Paths["/foo"].Get.Responses.Get(200).Value.Content.Get("application/json").Schema.Value.Properties["foo2"].Value.Properties["foo"].Value.Properties["bar"].Value.Example)
 }
diff --git a/openapi3/schema_oneOf_test.go b/openapi3/schema_oneOf_test.go
new file mode 100644
index 0000000..03fb670
--- /dev/null
+++ b/openapi3/schema_oneOf_test.go
@@ -0,0 +1,118 @@
+package openapi3
+
+import (
+	"testing"
+
+	"github.com/stretchr/testify/require"
+)
+
+var oneofSpec = []byte(`components:
+  schemas:
+    Cat:
+      type: object
+      properties:
+        name:
+          type: string
+        scratches:
+          type: boolean
+        $type:
+          type: string
+          enum:
+            - cat
+      required:
+        - name
+        - scratches
+        - $type
+    Dog:
+      type: object
+      properties:
+        name:
+          type: string
+        barks:
+          type: boolean
+        $type:
+          type: string
+          enum:
+            - dog
+      required:
+        - name
+        - barks
+        - $type
+    Animal:
+      type: object
+      oneOf:
+        - $ref: "#/components/schemas/Cat"
+        - $ref: "#/components/schemas/Dog"
+      discriminator:
+        propertyName: $type
+        mapping:
+          cat: "#/components/schemas/Cat"
+          dog: "#/components/schemas/Dog"
+`)
+
+var oneofNoDiscriminatorSpec = []byte(`components:
+  schemas:
+    Cat:
+      type: object
+      properties:
+        name:
+          type: string
+        scratches:
+          type: boolean
+      required:
+        - name
+        - scratches
+    Dog:
+      type: object
+      properties:
+        name:
+          type: string
+        barks:
+          type: boolean
+      required:
+        - name
+        - barks
+    Animal:
+      type: object
+      oneOf:
+        - $ref: "#/components/schemas/Cat"
+        - $ref: "#/components/schemas/Dog"
+`)
+
+func TestVisitJSON_OneOf_MissingDiscriptorProperty(t *testing.T) {
+	s, err := NewLoader().LoadFromData(oneofSpec)
+	require.NoError(t, err)
+	err = s.Components.Schemas["Animal"].Value.VisitJSON(map[string]interface{}{
+		"name": "snoopy",
+	})
+	require.EqualError(t, err, "input does not contain the discriminator property")
+}
+
+func TestVisitJSON_OneOf_MissingDiscriptorValue(t *testing.T) {
+	s, err := NewLoader().LoadFromData(oneofSpec)
+	require.NoError(t, err)
+	err = s.Components.Schemas["Animal"].Value.VisitJSON(map[string]interface{}{
+		"name":  "snoopy",
+		"$type": "snake",
+	})
+	require.EqualError(t, err, "input does not contain a valid discriminator value")
+}
+
+func TestVisitJSON_OneOf_MissingField(t *testing.T) {
+	s, err := NewLoader().LoadFromData(oneofSpec)
+	require.NoError(t, err)
+	err = s.Components.Schemas["Animal"].Value.VisitJSON(map[string]interface{}{
+		"name":  "snoopy",
+		"$type": "dog",
+	})
+	require.EqualError(t, err, "Error at \"/barks\": property \"barks\" is missing\nSchema:\n  {\n    \"properties\": {\n      \"$type\": {\n        \"enum\": [\n          \"dog\"\n        ],\n        \"type\": \"string\"\n      },\n      \"barks\": {\n        \"type\": \"boolean\"\n      },\n      \"name\": {\n        \"type\": \"string\"\n      }\n    },\n    \"required\": [\n      \"name\",\n      \"barks\",\n      \"$type\"\n    ],\n    \"type\": \"object\"\n  }\n\nValue:\n  {\n    \"$type\": \"dog\",\n    \"name\": \"snoopy\"\n  }\n")
+}
+
+func TestVisitJSON_OneOf_NoDiscriptor_MissingField(t *testing.T) {
+	s, err := NewLoader().LoadFromData(oneofNoDiscriminatorSpec)
+	require.NoError(t, err)
+	err = s.Components.Schemas["Animal"].Value.VisitJSON(map[string]interface{}{
+		"name": "snoopy",
+	})
+	require.EqualError(t, err, "doesn't match schema due to: Error at \"/scratches\": property \"scratches\" is missing\nSchema:\n  {\n    \"properties\": {\n      \"name\": {\n        \"type\": \"string\"\n      },\n      \"scratches\": {\n        \"type\": \"boolean\"\n      }\n    },\n    \"required\": [\n      \"name\",\n      \"scratches\"\n    ],\n    \"type\": \"object\"\n  }\n\nValue:\n  {\n    \"name\": \"snoopy\"\n  }\n Or Error at \"/barks\": property \"barks\" is missing\nSchema:\n  {\n    \"properties\": {\n      \"barks\": {\n        \"type\": \"boolean\"\n      },\n      \"name\": {\n        \"type\": \"string\"\n      }\n    },\n    \"required\": [\n      \"name\",\n      \"barks\"\n    ],\n    \"type\": \"object\"\n  }\n\nValue:\n  {\n    \"name\": \"snoopy\"\n  }\n")
+}
diff --git a/openapi3/testdata/go.mod b/openapi3/testdata/go.mod
deleted file mode 100644
index 21eab28..0000000
--- a/openapi3/testdata/go.mod
+++ /dev/null
@@ -1,5 +0,0 @@
-module github.com/getkin/kin-openapi/openapi3/testdata.test
-
-go 1.16
-
-require github.com/getkin/kin-openapi v0.63.0
diff --git a/openapi3/testdata/go.sum b/openapi3/testdata/go.sum
deleted file mode 100644
index 2795d3c..0000000
--- a/openapi3/testdata/go.sum
+++ /dev/null
@@ -1,38 +0,0 @@
-github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
-github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
-github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
-github.com/getkin/kin-openapi v0.52.0 h1:6WqsF5d6PfJ8AscdD+9Rtb2RP2iBWyC7V6GcjssWg7M=
-github.com/getkin/kin-openapi v0.52.0/go.mod h1:fRpo2Nw4Czgy0QnrIesRrEXs5+15N1F9mGZLP/aIomE=
-github.com/getkin/kin-openapi v0.61.0 h1:6awGqF5nG5zkVpMsAih1QH4VgzS8phTxECUWIFo7zko=
-github.com/getkin/kin-openapi v0.61.0/go.mod h1:7Yn5whZr5kJi6t+kShccXS8ae1APpYTW6yheSwk8Yi4=
-github.com/getkin/kin-openapi v0.62.0 h1:qDGdXTLo20ANSgflJEYotfNzHGvYvilNogWOryEwRrI=
-github.com/getkin/kin-openapi v0.62.0/go.mod h1:7Yn5whZr5kJi6t+kShccXS8ae1APpYTW6yheSwk8Yi4=
-github.com/getkin/kin-openapi v0.63.0 h1:27zYoKAuHDSquDYRpfmgsDu9TKC5z5G4vlu/XmIdsa8=
-github.com/getkin/kin-openapi v0.63.0/go.mod h1:7Yn5whZr5kJi6t+kShccXS8ae1APpYTW6yheSwk8Yi4=
-github.com/ghodss/yaml v1.0.0 h1:wQHKEahhL6wmXdzwWG11gIVCkOv05bNOh+Rxn0yngAk=
-github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
-github.com/go-openapi/jsonpointer v0.19.5 h1:gZr+CIYByUqjcgeLXnQu2gHYQC9o73G2XUeOFYEICuY=
-github.com/go-openapi/jsonpointer v0.19.5/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg=
-github.com/go-openapi/swag v0.19.5 h1:lTz6Ys4CmqqCQmZPBlbQENR1/GucA2bzYTE12Pw4tFY=
-github.com/go-openapi/swag v0.19.5/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh66Z9tfKk=
-github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So=
-github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI=
-github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
-github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
-github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
-github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
-github.com/mailru/easyjson v0.0.0-20190614124828-94de47d64c63/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
-github.com/mailru/easyjson v0.0.0-20190626092158-b2ccc519800e h1:hB2xlXdHp/pmPZq0y3QnmWAArdw9PqbmotexnWx/FU8=
-github.com/mailru/easyjson v0.0.0-20190626092158-b2ccc519800e/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
-github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
-github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
-github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
-github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
-github.com/stretchr/testify v1.5.1 h1:nOGnQDM7FYENwehXlg/kFVnos3rEvtKTjRvOWSzb6H4=
-github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
-gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
-gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY=
-gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
-gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
-gopkg.in/yaml.v2 v2.3.0 h1:clyUAQHOM3G0M3f5vQj7LuJrETvjVot3Z5el9nffUtU=
-gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
diff --git a/openapi3/testdata/load_with_go_embed_test.go b/openapi3/testdata/load_with_go_embed_test.go
index 9cdace5..56b274c 100644
--- a/openapi3/testdata/load_with_go_embed_test.go
+++ b/openapi3/testdata/load_with_go_embed_test.go
@@ -1,5 +1,3 @@
-//+build with_embed
-
 package openapi3_test
 
 import (
@@ -10,7 +8,7 @@ import (
 	"github.com/getkin/kin-openapi/openapi3"
 )
 
-//go:embed recursiveRef/*
+//go:embed testdata/recursiveRef/*
 var fs embed.FS
 
 func Example() {
@@ -20,7 +18,7 @@ func Example() {
 		return fs.ReadFile(uri.Path)
 	}
 
-	doc, err := loader.LoadFromFile("recursiveRef/openapi.yml")
+	doc, err := loader.LoadFromFile("testdata/recursiveRef/openapi.yml")
 	if err != nil {
 		panic(err)
 	}
@@ -29,6 +27,6 @@ func Example() {
 		panic(err)
 	}
 
-	fmt.Println(doc.Paths["/foo"].Get.Responses["200"].Value.Content["application/json"].Schema.Value.Properties["foo"].Value.Properties["bar"].Value.Type)
-	// Output: array
+	fmt.Println(doc.Paths["/foo"].Get.Responses["200"].Value.Content["application/json"].Schema.Value.Properties["foo2"].Value.Properties["foo"].Value.Properties["bar"].Value.Type)
+	// Output: string
 }
diff --git a/openapi3/testdata/recursiveRef/components/Foo.yml b/openapi3/testdata/recursiveRef/components/Foo.yml
index 0c08992..53a2336 100644
--- a/openapi3/testdata/recursiveRef/components/Foo.yml
+++ b/openapi3/testdata/recursiveRef/components/Foo.yml
@@ -1,6 +1,4 @@
 type: object
 properties:
   bar:
-    type: array
-    items:
-      $ref: ../openapi.yml#/components/schemas/Bar
+    $ref: ../openapi.yml#/components/schemas/Bar
diff --git a/openapi3/testdata/recursiveRef/components/Foo/Foo2.yml b/openapi3/testdata/recursiveRef/components/Foo/Foo2.yml
new file mode 100644
index 0000000..aeac81f
--- /dev/null
+++ b/openapi3/testdata/recursiveRef/components/Foo/Foo2.yml
@@ -0,0 +1,4 @@
+type: object
+properties:
+  foo:
+    $ref: ../../openapi.yml#/components/schemas/Foo
diff --git a/openapi3/testdata/recursiveRef/openapi.yml b/openapi3/testdata/recursiveRef/openapi.yml
index 5dfcfbf..3559c8e 100644
--- a/openapi3/testdata/recursiveRef/openapi.yml
+++ b/openapi3/testdata/recursiveRef/openapi.yml
@@ -9,5 +9,7 @@ components:
   schemas:
     Foo:
       $ref: ./components/Foo.yml
+    Foo2:
+      $ref: ./components/Foo/Foo2.yml
     Bar:
       $ref: ./components/Bar.yml
diff --git a/openapi3/testdata/recursiveRef/paths/foo.yml b/openapi3/testdata/recursiveRef/paths/foo.yml
index dd6c15d..1653c7a 100644
--- a/openapi3/testdata/recursiveRef/paths/foo.yml
+++ b/openapi3/testdata/recursiveRef/paths/foo.yml
@@ -7,5 +7,5 @@ get:
           schema:
             type: object
             properties:
-              foo:
-                $ref: ../openapi.yml#/components/schemas/Foo
+              foo2:
+                $ref: ../openapi.yml#/components/schemas/Foo2
diff --git a/openapi3filter/req_resp_decoder_test.go b/openapi3filter/req_resp_decoder_test.go
index 397c9e9..7ae863b 100644
--- a/openapi3filter/req_resp_decoder_test.go
+++ b/openapi3filter/req_resp_decoder_test.go
@@ -974,6 +974,7 @@ func TestDecodeBody(t *testing.T) {
 		{name: "c", contentType: "text/plain", data: strings.NewReader("c2")},
 		{name: "d", contentType: "application/json", data: bytes.NewReader(d)},
 		{name: "f", contentType: "application/octet-stream", data: strings.NewReader("foo"), filename: "f1"},
+		{name: "g", data: strings.NewReader("g1")},
 	})
 	require.NoError(t, err)
 
@@ -987,10 +988,14 @@ func TestDecodeBody(t *testing.T) {
 		{name: "a", contentType: "text/plain", data: strings.NewReader("a1")},
 		{name: "x", contentType: "text/plain", data: strings.NewReader("x1")},
 	})
+	require.NoError(t, err)
+
 	multipartAdditionalProps, multipartMimeAdditionalProps, err := newTestMultipartForm([]*testFormPart{
 		{name: "a", contentType: "text/plain", data: strings.NewReader("a1")},
 		{name: "x", contentType: "text/plain", data: strings.NewReader("x1")},
 	})
+	require.NoError(t, err)
+
 	multipartAdditionalPropsErr, multipartMimeAdditionalPropsErr, err := newTestMultipartForm([]*testFormPart{
 		{name: "a", contentType: "text/plain", data: strings.NewReader("a1")},
 		{name: "x", contentType: "text/plain", data: strings.NewReader("x1")},
@@ -1075,8 +1080,9 @@ func TestDecodeBody(t *testing.T) {
 				WithProperty("b", openapi3.NewIntegerSchema()).
 				WithProperty("c", openapi3.NewArraySchema().WithItems(openapi3.NewStringSchema())).
 				WithProperty("d", openapi3.NewObjectSchema().WithProperty("d1", openapi3.NewStringSchema())).
-				WithProperty("f", openapi3.NewStringSchema().WithFormat("binary")),
-			want: map[string]interface{}{"a": "a1", "b": float64(10), "c": []interface{}{"c1", "c2"}, "d": map[string]interface{}{"d1": "d1"}, "f": "foo"},
+				WithProperty("f", openapi3.NewStringSchema().WithFormat("binary")).
+				WithProperty("g", openapi3.NewStringSchema()),
+			want: map[string]interface{}{"a": "a1", "b": float64(10), "c": []interface{}{"c1", "c2"}, "d": map[string]interface{}{"d1": "d1"}, "f": "foo", "g": "g1"},
 		},
 		{
 			name: "multipartExtraPart",
diff --git a/openapi3gen/openapi3gen_test.go b/openapi3gen/openapi3gen_test.go
index 0062e2e..0b7fde9 100644
--- a/openapi3gen/openapi3gen_test.go
+++ b/openapi3gen/openapi3gen_test.go
@@ -85,6 +85,36 @@ func TestEmbeddedStructs(t *testing.T) {
 	require.Equal(t, true, ok)
 }
 
+func TestEmbeddedPointerStructs(t *testing.T) {
+	type EmbeddedStruct struct {
+		ID string
+	}
+
+	type ContainerStruct struct {
+		Name string
+		*EmbeddedStruct
+	}
+
+	instance := &ContainerStruct{
+		Name: "Container",
+		EmbeddedStruct: &EmbeddedStruct{
+			ID: "Embedded",
+		},
+	}
+
+	generator := NewGenerator(UseAllExportedFields())
+
+	schemaRef, err := generator.GenerateSchemaRef(reflect.TypeOf(instance))
+	require.NoError(t, err)
+
+	var ok bool
+	_, ok = schemaRef.Value.Properties["Name"]
+	require.Equal(t, true, ok)
+
+	_, ok = schemaRef.Value.Properties["ID"]
+	require.Equal(t, true, ok)
+}
+
 func TestCyclicReferences(t *testing.T) {
 	type ObjectDiff struct {
 		FieldCycle *ObjectDiff
diff --git a/routers/gorillamux/router_test.go b/routers/gorillamux/router_test.go
index d9ad2c9..8dc0a2e 100644
--- a/routers/gorillamux/router_test.go
+++ b/routers/gorillamux/router_test.go
@@ -146,7 +146,7 @@ func TestRouter(t *testing.T) {
 	expect(r, http.MethodGet, "/params/a/b/c%2Fd", paramsGET, map[string]string{
 		"x": "a",
 		"y": "b",
-		"z": "c/d",
+		"z": "c%2Fd",
 	})
 	expect(r, http.MethodGet, "/books/War.and.Peace", paramsGET, map[string]string{
 		"bookid": "War.and.Peace",
@@ -206,3 +206,16 @@ func TestPermuteScheme(t *testing.T) {
 	perms := permutePart(scheme0, server)
 	require.Equal(t, []string{"http", "https"}, perms)
 }
+
+func TestServerPath(t *testing.T) {
+	server := &openapi3.Server{URL: "http://example.com"}
+	err := server.Validate(context.Background())
+	require.NoError(t, err)
+
+	_, err = NewRouter(&openapi3.T{Servers: openapi3.Servers{
+		server,
+		&openapi3.Server{URL: "http://example.com/"},
+		&openapi3.Server{URL: "http://example.com/path"}},
+	})
+	require.NoError(t, err)
+}
diff --git a/routers/legacy/validate_request_test.go b/routers/legacy/validate_request_test.go
index 7737f50..5b9518c 100644
--- a/routers/legacy/validate_request_test.go
+++ b/routers/legacy/validate_request_test.go
@@ -107,26 +107,6 @@ func Example() {
 		fmt.Println(err)
 	}
 	// Output:
-	// request body has an error: doesn't match the schema: Doesn't match schema "oneOf"
-	// Schema:
-	//   {
-	//     "discriminator": {
-	//       "propertyName": "pet_type"
-	//     },
-	//     "oneOf": [
-	//       {
-	//         "$ref": "#/components/schemas/Cat"
-	//       },
-	//       {
-	//         "$ref": "#/components/schemas/Dog"
-	//       }
-	//     ]
-	//   }
-	//
-	// Value:
-	//   {
-	//     "bark": true,
-	//     "breed": "Dingo",
-	//     "pet_type": "Cat"
-	//   }
+	// request body has an error: doesn't match the schema: input matches more than one oneOf schemas
+
 }