Repository: getkin/kin-openapi
Pull request #131 (2019-10-16 06:29:08)

Resolve relative documents
This pull request builds on this [proof of concept](https://github.com/getkin/kin-openapi/pull/81) to give `kin-openapi` the feature to resolve relative documents for OpenAPI 3 specs.

It can resolve relative document references for:

- examples
- headers
- parameters
- request bodies
- responses
- schemas
- security schemes
- paths

and includes tests for all of these.

fenollp: Also please run `go fmt ./...`

Patch:
diff --git a/openapi3/header.go b/openapi3/header.go
index 91b5f3f..f572166 100644
--- a/openapi3/header.go
+++ b/openapi3/header.go
@@ -2,6 +2,7 @@ package openapi3
 
 import (
 	"context"
+	"github.com/getkin/kin-openapi/jsoninfo"
 )
 
 type Header struct {
@@ -14,6 +15,10 @@ type Header struct {
 	Schema *SchemaRef `json:"schema,omitempty" yaml:"schema,omitempty"`
 }
 
+func (value *Header) UnmarshalJSON(data []byte) error {
+	return jsoninfo.UnmarshalStrictStruct(data, value)
+}
+
 func (value *Header) Validate(c context.Context) error {
 	if v := value.Schema; v != nil {
 		if err := v.Validate(c); err != nil {
diff --git a/openapi3/swagger_loader.go b/openapi3/swagger_loader.go
index 43a0ead..2e6d699 100644
--- a/openapi3/swagger_loader.go
+++ b/openapi3/swagger_loader.go
@@ -2,6 +2,7 @@ package openapi3
 
 import (
 	"context"
+	"encoding/json"
 	"errors"
 	"fmt"
 	"io/ioutil"
@@ -58,6 +59,38 @@ func (swaggerLoader *SwaggerLoader) loadSwaggerFromURIInternal(location *url.URL
 	return swaggerLoader.loadSwaggerFromDataWithPathInternal(data, location)
 }
 
+// loadSingleElementFromURI read the data from ref and unmarshal to JSON to the
+// passed element.
+func (swaggerLoader *SwaggerLoader) loadSingleElementFromURI(ref string, rootPath *url.URL, element json.Unmarshaler) error {
+	if !swaggerLoader.IsExternalRefsAllowed {
+		return fmt.Errorf("encountered non-allowed external reference: '%s'", ref)
+	}
+
+	parsedURL, err := url.Parse(ref)
+	if err != nil {
+		return err
+	}
+
+	if parsedURL.Fragment != "" {
+		return errors.New("references to files which contain more than one element definition are not supported")
+	}
+
+	resolvedPath, err := resolvePath(rootPath, parsedURL)
+	if err != nil {
+		return fmt.Errorf("could not resolve path: %v", err)
+	}
+
+	data, err := readUrl(resolvedPath)
+	if err != nil {
+		return err
+	}
+	if err := yaml.Unmarshal(data, element); err != nil {
+		return err
+	}
+
+	return nil
+}
+
 func readUrl(location *url.URL) ([]byte, error) {
 	if location.Scheme != "" && location.Host != "" {
 		resp, err := http.Get(location.String())
@@ -115,6 +148,8 @@ func (swaggerLoader *SwaggerLoader) loadSwaggerFromDataInternal(data []byte) (*S
 	return swagger, swaggerLoader.ResolveRefsIn(swagger, nil)
 }
 
+// LoadSwaggerFromDataWithPath takes the OpenApi spec data in bytes and a path where the resolver can find referred
+// elements and returns a *Swagger with all resolved data or an error if unable to load data or resolve refs.
 func (swaggerLoader *SwaggerLoader) LoadSwaggerFromDataWithPath(data []byte, path *url.URL) (*Swagger, error) {
 	swaggerLoader.reset()
 	return swaggerLoader.loadSwaggerFromDataWithPathInternal(data, path)
@@ -208,6 +243,10 @@ func resolvePath(basePath *url.URL, componentPath *url.URL) (*url.URL, error) {
 	return componentPath, nil
 }
 
+func isSingleRefElement(ref string) bool {
+	return !strings.Contains(ref, "#")
+}
+
 func (swaggerLoader *SwaggerLoader) resolveComponent(swagger *Swagger, ref string, prefix string, path *url.URL) (
 	components *Components,
 	id string,
@@ -266,22 +305,31 @@ func (swaggerLoader *SwaggerLoader) resolveHeaderRef(swagger *Swagger, component
 	// Resolve ref
 	const prefix = "#/components/headers/"
 	if ref := component.Ref; len(ref) > 0 {
-		components, id, componentPath, err := swaggerLoader.resolveComponent(swagger, ref, prefix, path)
-		if err != nil {
-			return err
-		}
-		definitions := components.Headers
-		if definitions == nil {
-			return failedToResolveRefFragment(ref)
-		}
-		resolved := definitions[id]
-		if resolved == nil {
-			return failedToResolveRefFragment(ref)
-		}
-		if err := swaggerLoader.resolveHeaderRef(swagger, resolved, componentPath); err != nil {
-			return err
+		if isSingleRefElement(ref) {
+			var header Header
+			if err := swaggerLoader.loadSingleElementFromURI(ref, path, &header); err != nil {
+				return err
+			}
+
+			component.Value = &header
+		} else {
+			components, id, componentPath, err := swaggerLoader.resolveComponent(swagger, ref, prefix, path)
+			if err != nil {
+				return err
+			}
+			definitions := components.Headers
+			if definitions == nil {
+				return failedToResolveRefFragment(ref)
+			}
+			resolved := definitions[id]
+			if resolved == nil {
+				return failedToResolveRefFragment(ref)
+			}
+			if err := swaggerLoader.resolveHeaderRef(swagger, resolved, componentPath); err != nil {
+				return err
+			}
+			component.Value = resolved.Value
 		}
-		component.Value = resolved.Value
 	}
 	value := component.Value
 	if value == nil {
@@ -306,22 +354,30 @@ func (swaggerLoader *SwaggerLoader) resolveParameterRef(swagger *Swagger, compon
 	// Resolve ref
 	const prefix = "#/components/parameters/"
 	if ref := component.Ref; len(ref) > 0 {
-		components, id, componentPath, err := swaggerLoader.resolveComponent(swagger, ref, prefix, path)
-		if err != nil {
-			return err
-		}
-		definitions := components.Parameters
-		if definitions == nil {
-			return failedToResolveRefFragmentPart(ref, "parameters")
-		}
-		resolved := definitions[id]
-		if resolved == nil {
-			return failedToResolveRefFragmentPart(ref, id)
-		}
-		if err := swaggerLoader.resolveParameterRef(swagger, resolved, componentPath); err != nil {
-			return err
+		if isSingleRefElement(ref) {
+			var param Parameter
+			if err := swaggerLoader.loadSingleElementFromURI(ref, path, &param); err != nil {
+				return err
+			}
+			component.Value = &param
+		} else {
+			components, id, componentPath, err := swaggerLoader.resolveComponent(swagger, ref, prefix, path)
+			if err != nil {
+				return err
+			}
+			definitions := components.Parameters
+			if definitions == nil {
+				return failedToResolveRefFragmentPart(ref, "parameters")
+			}
+			resolved := definitions[id]
+			if resolved == nil {
+				return failedToResolveRefFragmentPart(ref, id)
+			}
+			if err := swaggerLoader.resolveParameterRef(swagger, resolved, componentPath); err != nil {
+				return err
+			}
+			component.Value = resolved.Value
 		}
-		component.Value = resolved.Value
 	}
 	value := component.Value
 	if value == nil {
@@ -356,22 +412,31 @@ func (swaggerLoader *SwaggerLoader) resolveRequestBodyRef(swagger *Swagger, comp
 	// Resolve ref
 	const prefix = "#/components/requestBodies/"
 	if ref := component.Ref; len(ref) > 0 {
-		components, id, componentPath, err := swaggerLoader.resolveComponent(swagger, ref, prefix, path)
-		if err != nil {
-			return err
-		}
-		definitions := components.RequestBodies
-		if definitions == nil {
-			return failedToResolveRefFragmentPart(ref, "requestBodies")
-		}
-		resolved := definitions[id]
-		if resolved == nil {
-			return failedToResolveRefFragmentPart(ref, id)
-		}
-		if err = swaggerLoader.resolveRequestBodyRef(swagger, resolved, componentPath); err != nil {
-			return err
+		if isSingleRefElement(ref) {
+			var requestBody RequestBody
+			if err := swaggerLoader.loadSingleElementFromURI(ref, path, &requestBody); err != nil {
+				return err
+			}
+
+			component.Value = &requestBody
+		} else {
+			components, id, componentPath, err := swaggerLoader.resolveComponent(swagger, ref, prefix, path)
+			if err != nil {
+				return err
+			}
+			definitions := components.RequestBodies
+			if definitions == nil {
+				return failedToResolveRefFragmentPart(ref, "requestBodies")
+			}
+			resolved := definitions[id]
+			if resolved == nil {
+				return failedToResolveRefFragmentPart(ref, id)
+			}
+			if err = swaggerLoader.resolveRequestBodyRef(swagger, resolved, componentPath); err != nil {
+				return err
+			}
+			component.Value = resolved.Value
 		}
-		component.Value = resolved.Value
 	}
 	value := component.Value
 	if value == nil {
@@ -404,22 +469,32 @@ func (swaggerLoader *SwaggerLoader) resolveResponseRef(swagger *Swagger, compone
 	// Resolve ref
 	const prefix = "#/components/responses/"
 	if ref := component.Ref; len(ref) > 0 {
-		components, id, componentPath, err := swaggerLoader.resolveComponent(swagger, ref, prefix, path)
-		if err != nil {
-			return err
-		}
-		definitions := components.Responses
-		if definitions == nil {
-			return failedToResolveRefFragmentPart(ref, "responses")
-		}
-		resolved := definitions[id]
-		if resolved == nil {
-			return failedToResolveRefFragmentPart(ref, id)
-		}
-		if err := swaggerLoader.resolveResponseRef(swagger, resolved, componentPath); err != nil {
-			return err
+
+		if isSingleRefElement(ref) {
+			var resp Response
+			if err := swaggerLoader.loadSingleElementFromURI(ref, path, &resp); err != nil {
+				return err
+			}
+
+			component.Value = &resp
+		} else {
+			components, id, componentPath, err := swaggerLoader.resolveComponent(swagger, ref, prefix, path)
+			if err != nil {
+				return err
+			}
+			definitions := components.Responses
+			if definitions == nil {
+				return failedToResolveRefFragmentPart(ref, "responses")
+			}
+			resolved := definitions[id]
+			if resolved == nil {
+				return failedToResolveRefFragmentPart(ref, id)
+			}
+			if err := swaggerLoader.resolveResponseRef(swagger, resolved, componentPath); err != nil {
+				return err
+			}
+			component.Value = resolved.Value
 		}
-		component.Value = resolved.Value
 	}
 	value := component.Value
 	if value == nil {
@@ -461,22 +536,30 @@ func (swaggerLoader *SwaggerLoader) resolveSchemaRef(swagger *Swagger, component
 	// Resolve ref
 	const prefix = "#/components/schemas/"
 	if ref := component.Ref; len(ref) > 0 {
-		components, id, componentPath, err := swaggerLoader.resolveComponent(swagger, ref, prefix, path)
-		if err != nil {
-			return err
-		}
-		definitions := components.Schemas
-		if definitions == nil {
-			return failedToResolveRefFragmentPart(ref, "schemas")
-		}
-		resolved := definitions[id]
-		if resolved == nil {
-			return failedToResolveRefFragmentPart(ref, id)
-		}
-		if err := swaggerLoader.resolveSchemaRef(swagger, resolved, componentPath); err != nil {
-			return err
+		if isSingleRefElement(ref) {
+			var schema Schema
+			if err := swaggerLoader.loadSingleElementFromURI(ref, path, &schema); err != nil {
+				return err
+			}
+			component.Value = &schema
+		} else {
+			components, id, componentPath, err := swaggerLoader.resolveComponent(swagger, ref, prefix, path)
+			if err != nil {
+				return err
+			}
+			definitions := components.Schemas
+			if definitions == nil {
+				return failedToResolveRefFragmentPart(ref, "schemas")
+			}
+			resolved := definitions[id]
+			if resolved == nil {
+				return failedToResolveRefFragmentPart(ref, id)
+			}
+			if err := swaggerLoader.resolveSchemaRef(swagger, resolved, componentPath); err != nil {
+				return err
+			}
+			component.Value = resolved.Value
 		}
-		component.Value = resolved.Value
 	}
 	value := component.Value
 	if value == nil {
@@ -534,22 +617,31 @@ func (swaggerLoader *SwaggerLoader) resolveSecuritySchemeRef(swagger *Swagger, c
 	// Resolve ref
 	const prefix = "#/components/securitySchemes/"
 	if ref := component.Ref; len(ref) > 0 {
-		components, id, componentPath, err := swaggerLoader.resolveComponent(swagger, ref, prefix, path)
-		if err != nil {
-			return err
-		}
-		definitions := components.SecuritySchemes
-		if definitions == nil {
-			return failedToResolveRefFragmentPart(ref, "securitySchemes")
-		}
-		resolved := definitions[id]
-		if resolved == nil {
-			return failedToResolveRefFragmentPart(ref, id)
-		}
-		if err := swaggerLoader.resolveSecuritySchemeRef(swagger, resolved, componentPath); err != nil {
-			return err
+		if isSingleRefElement(ref) {
+			var scheme SecurityScheme
+			if err := swaggerLoader.loadSingleElementFromURI(ref, path, &scheme); err != nil {
+				return err
+			}
+
+			component.Value = &scheme
+		} else {
+			components, id, componentPath, err := swaggerLoader.resolveComponent(swagger, ref, prefix, path)
+			if err != nil {
+				return err
+			}
+			definitions := components.SecuritySchemes
+			if definitions == nil {
+				return failedToResolveRefFragmentPart(ref, "securitySchemes")
+			}
+			resolved := definitions[id]
+			if resolved == nil {
+				return failedToResolveRefFragmentPart(ref, id)
+			}
+			if err := swaggerLoader.resolveSecuritySchemeRef(swagger, resolved, componentPath); err != nil {
+				return err
+			}
+			component.Value = resolved.Value
 		}
-		component.Value = resolved.Value
 	}
 	return nil
 }
@@ -564,22 +656,31 @@ func (swaggerLoader *SwaggerLoader) resolveExampleRef(swagger *Swagger, componen
 
 	const prefix = "#/components/examples/"
 	if ref := component.Ref; len(ref) > 0 {
-		components, id, componentPath, err := swaggerLoader.resolveComponent(swagger, ref, prefix, path)
-		if err != nil {
-			return err
-		}
-		definitions := components.Examples
-		if definitions == nil {
-			return failedToResolveRefFragmentPart(ref, "examples")
-		}
-		resolved := definitions[id]
-		if resolved == nil {
-			return failedToResolveRefFragmentPart(ref, id)
-		}
-		if err := swaggerLoader.resolveExampleRef(swagger, resolved, componentPath); err != nil {
-			return err
+		if isSingleRefElement(ref) {
+			var example Example
+			if err := swaggerLoader.loadSingleElementFromURI(ref, path, &example); err != nil {
+				return err
+			}
+
+			component.Value = &example
+		} else {
+			components, id, componentPath, err := swaggerLoader.resolveComponent(swagger, ref, prefix, path)
+			if err != nil {
+				return err
+			}
+			definitions := components.Examples
+			if definitions == nil {
+				return failedToResolveRefFragmentPart(ref, "examples")
+			}
+			resolved := definitions[id]
+			if resolved == nil {
+				return failedToResolveRefFragmentPart(ref, id)
+			}
+			if err := swaggerLoader.resolveExampleRef(swagger, resolved, componentPath); err != nil {
+				return err
+			}
+			component.Value = resolved.Value
 		}
-		component.Value = resolved.Value
 	}
 	return nil
 }
@@ -599,27 +700,35 @@ func (swaggerLoader *SwaggerLoader) resolvePathItemRef(swagger *Swagger, entrypo
 
 	ref := pathItem.Ref
 	if ref != "" {
-		if swagger, ref, path, err = swaggerLoader.resolveRefSwagger(swagger, ref, path); err != nil {
-			return
-		}
+		if isSingleRefElement(ref) {
+			var p PathItem
+			if err := swaggerLoader.loadSingleElementFromURI(ref, path, &p); err != nil {
+				return err
+			}
+			*pathItem = p
+		} else {
+			if swagger, ref, path, err = swaggerLoader.resolveRefSwagger(swagger, ref, path); err != nil {
+				return
+			}
 
-		prefix := "#/paths/"
-		if !strings.HasPrefix(ref, prefix) {
-			err = fmt.Errorf("expected prefix '%s' in URI '%s'", prefix, ref)
-			return
-		}
-		id := unescapeRefString(ref[len(prefix):])
+			prefix := "#/paths/"
+			if !strings.HasPrefix(ref, prefix) {
+				err = fmt.Errorf("expected prefix '%s' in URI '%s'", prefix, ref)
+				return
+			}
+			id := unescapeRefString(ref[len(prefix):])
 
-		definitions := swagger.Paths
-		if definitions == nil {
-			return failedToResolveRefFragmentPart(ref, "paths")
-		}
-		resolved := definitions[id]
-		if resolved == nil {
-			return failedToResolveRefFragmentPart(ref, id)
-		}
+			definitions := swagger.Paths
+			if definitions == nil {
+				return failedToResolveRefFragmentPart(ref, "paths")
+			}
+			resolved := definitions[id]
+			if resolved == nil {
+				return failedToResolveRefFragmentPart(ref, id)
+			}
 
-		*pathItem = *resolved
+			*pathItem = *resolved
+		}
 	}
 
 	for _, parameter := range pathItem.Parameters {


Test patch:
diff --git a/openapi3/swagger_loader_relative_refs_test.go b/openapi3/swagger_loader_relative_refs_test.go
index 9b3a6f9..49ebba1 100644
--- a/openapi3/swagger_loader_relative_refs_test.go
+++ b/openapi3/swagger_loader_relative_refs_test.go
@@ -716,3 +716,190 @@ const externalRequestResponseHeaderRefTemplate = `
         }
     }
 }`
+
+// Relative Schema Documents Tests
+var relativeDocRefsTestDataEntries = []refTestDataEntry{
+	{
+		name:            "SchemaRef",
+		contentTemplate: relativeSchemaDocsRefTemplate,
+		testFunc: func(t *testing.T, swagger *openapi3.Swagger) {
+			require.NotNil(t, swagger.Components.Schemas["TestSchema"].Value.Type)
+			require.Equal(t, "string", swagger.Components.Schemas["TestSchema"].Value.Type)
+		},
+	},
+	{
+		name:            "ResponseRef",
+		contentTemplate: relativeResponseDocsRefTemplate,
+		testFunc: func(t *testing.T, swagger *openapi3.Swagger) {
+			require.NotNil(t, swagger.Components.Responses["TestResponse"].Value.Description)
+			require.Equal(t, "description", swagger.Components.Responses["TestResponse"].Value.Description)
+		},
+	},
+	{
+		name:            "ParameterRef",
+		contentTemplate: relativeParameterDocsRefTemplate,
+		testFunc: func(t *testing.T, swagger *openapi3.Swagger) {
+			require.NotNil(t, swagger.Components.Parameters["TestParameter"].Value.Name)
+			require.Equal(t, "param", swagger.Components.Parameters["TestParameter"].Value.Name)
+			require.Equal(t, true, swagger.Components.Parameters["TestParameter"].Value.Required)
+		},
+	},
+	{
+		name:            "ExampleRef",
+		contentTemplate: relativeExampleDocsRefTemplate,
+		testFunc: func(t *testing.T, swagger *openapi3.Swagger) {
+			require.NotNil(t, "param", swagger.Components.Examples["TestExample"].Value.Summary)
+			require.NotNil(t, "param", swagger.Components.Examples["TestExample"].Value.Value)
+			require.Equal(t, "An example", swagger.Components.Examples["TestExample"].Value.Summary)
+		},
+	},
+	{
+		name:            "RequestRef",
+		contentTemplate: relativeRequestDocsRefTemplate,
+		testFunc: func(t *testing.T, swagger *openapi3.Swagger) {
+			require.NotNil(t, "param", swagger.Components.RequestBodies["TestRequestBody"].Value.Description)
+			require.Equal(t, "example request", swagger.Components.RequestBodies["TestRequestBody"].Value.Description)
+		},
+	},
+	{
+		name:            "HeaderRef",
+		contentTemplate: relativeHeaderDocsRefTemplate,
+		testFunc: func(t *testing.T, swagger *openapi3.Swagger) {
+			require.NotNil(t, "param", swagger.Components.Headers["TestHeader"].Value.Description)
+			require.Equal(t, "description", swagger.Components.Headers["TestHeader"].Value.Description)
+		},
+	},
+	{
+		name:            "HeaderRef",
+		contentTemplate: relativeHeaderDocsRefTemplate,
+		testFunc: func(t *testing.T, swagger *openapi3.Swagger) {
+			require.NotNil(t, "param", swagger.Components.Headers["TestHeader"].Value.Description)
+			require.Equal(t, "description", swagger.Components.Headers["TestHeader"].Value.Description)
+		},
+	},
+	{
+		name:            "SecuritySchemeRef",
+		contentTemplate: relativeSecuritySchemeDocsRefTemplate,
+		testFunc: func(t *testing.T, swagger *openapi3.Swagger) {
+			require.NotNil(t, swagger.Components.SecuritySchemes["TestSecurityScheme"].Value.Type)
+			require.NotNil(t, swagger.Components.SecuritySchemes["TestSecurityScheme"].Value.Scheme)
+			require.Equal(t, "http", swagger.Components.SecuritySchemes["TestSecurityScheme"].Value.Type)
+			require.Equal(t, "basic", swagger.Components.SecuritySchemes["TestSecurityScheme"].Value.Scheme)
+		},
+	},
+	{
+		name:            "PathRef",
+		contentTemplate: relativePathDocsRefTemplate,
+		testFunc: func(t *testing.T, swagger *openapi3.Swagger) {
+			require.NotNil(t, swagger.Paths["/pets"])
+			require.NotNil(t, swagger.Paths["/pets"].Get.Responses["200"])
+			require.NotNil(t, swagger.Paths["/pets"].Get.Responses["200"].Value.Content["application/json"])
+		},
+	},
+}
+
+func TestLoadSpecWithRelativeDocumentRefs(t *testing.T) {
+	for _, td := range relativeDocRefsTestDataEntries {
+		t.Logf("testcase '%s'", td.name)
+
+		spec := []byte(td.contentTemplate)
+		loader := openapi3.NewSwaggerLoader()
+		loader.IsExternalRefsAllowed = true
+		swagger, err := loader.LoadSwaggerFromDataWithPath(spec, &url.URL{Path: "testdata/"})
+		require.NoError(t, err)
+		td.testFunc(t, swagger)
+	}
+}
+
+const relativeSchemaDocsRefTemplate = `
+openapi: 3.0.0
+info: 
+  title: ""
+  version: "1.0"
+paths: {}
+components: 
+  schemas: 
+    TestSchema: 
+      $ref: relativeDocs/CustomTestSchema.yml
+`
+
+const relativeResponseDocsRefTemplate = `
+openapi: 3.0.0
+info: 
+  title: ""
+  version: "1.0"
+paths: {}
+components: 
+  responses: 
+    TestResponse: 
+      $ref: relativeDocs/CustomTestResponse.yml
+`
+
+const relativeParameterDocsRefTemplate = `
+openapi: 3.0.0
+info:
+  title: ""
+  version: "1.0"
+paths: {}
+components:
+  parameters:
+    TestParameter: 
+      $ref: relativeDocs/CustomTestParameter.yml
+`
+
+const relativeExampleDocsRefTemplate = `
+openapi: 3.0.0
+info:
+  title: ""
+  version: "1.0"
+paths: {}
+components:
+  examples:
+    TestExample:
+      $ref: relativeDocs/CustomTestExample.yml
+`
+
+const relativeRequestDocsRefTemplate = `
+openapi: 3.0.0
+info:
+  title: ""
+  version: "1.0"
+paths: {}
+components:
+  requestBodies:
+    TestRequestBody:
+      $ref: relativeDocs/CustomTestRequestBody.yml
+`
+
+const relativeHeaderDocsRefTemplate = `
+openapi: 3.0.0
+info:
+  title: ""
+  version: "1.0"
+paths: {}
+components:
+  headers:
+    TestHeader:
+      $ref: relativeDocs/CustomTestHeader.yml
+`
+
+const relativeSecuritySchemeDocsRefTemplate = `
+openapi: 3.0.0
+info:
+  title: ""
+  version: "1.0"
+paths: {}
+components:
+  securitySchemes:
+    TestSecurityScheme:
+      $ref: relativeDocs/CustomTestSecurityScheme.yml
+`
+const relativePathDocsRefTemplate = `
+openapi: 3.0.0
+info:
+  title: ""
+  version: "2.0"
+paths:
+  /pets:
+    $ref: relativeDocs/CustomTestPath.yml
+`
diff --git a/openapi3/swagger_loader_test.go b/openapi3/swagger_loader_test.go
index c238742..b5d1194 100644
--- a/openapi3/swagger_loader_test.go
+++ b/openapi3/swagger_loader_test.go
@@ -325,6 +325,16 @@ func TestLoadFileWithExternalSchemaRef(t *testing.T) {
 	require.NotNil(t, swagger.Components.Schemas["AnotherTestSchema"].Value.Type)
 }
 
+func TestLoadFileWithExternalSchemaRefSingleComponent(t *testing.T) {
+	loader := openapi3.NewSwaggerLoader()
+	loader.IsExternalRefsAllowed = true
+	swagger, err := loader.LoadSwaggerFromFile("testdata/testrefsinglecomponent.openapi.json")
+	require.NoError(t, err)
+
+	require.NotNil(t, swagger.Components.Responses["SomeResponse"])
+	require.Equal(t, "this is a single response definition", swagger.Components.Responses["SomeResponse"].Value.Description)
+}
+
 func TestLoadRequestResponseHeaderRef(t *testing.T) {
 	spec := []byte(`
 {
diff --git a/openapi3/testdata/relativeDocs/CustomTestExample.yml b/openapi3/testdata/relativeDocs/CustomTestExample.yml
new file mode 100644
index 0000000..748636a
--- /dev/null
+++ b/openapi3/testdata/relativeDocs/CustomTestExample.yml
@@ -0,0 +1,5 @@
+summary: An example
+value: |
+        {
+          "example": "hello"
+        }
\ No newline at end of file
diff --git a/openapi3/testdata/relativeDocs/CustomTestHeader.yml b/openapi3/testdata/relativeDocs/CustomTestHeader.yml
new file mode 100644
index 0000000..afa4b97
--- /dev/null
+++ b/openapi3/testdata/relativeDocs/CustomTestHeader.yml
@@ -0,0 +1,1 @@
+description: description
\ No newline at end of file
diff --git a/openapi3/testdata/relativeDocs/CustomTestParameter.yml b/openapi3/testdata/relativeDocs/CustomTestParameter.yml
new file mode 100644
index 0000000..66aec88
--- /dev/null
+++ b/openapi3/testdata/relativeDocs/CustomTestParameter.yml
@@ -0,0 +1,3 @@
+name: param
+in: path
+required: true
diff --git a/openapi3/testdata/relativeDocs/CustomTestPath.yml b/openapi3/testdata/relativeDocs/CustomTestPath.yml
new file mode 100644
index 0000000..40c99cd
--- /dev/null
+++ b/openapi3/testdata/relativeDocs/CustomTestPath.yml
@@ -0,0 +1,14 @@
+get:
+  operationId: findAllDefinitions
+  responses:
+    "200":
+      content:
+        application/json:
+          schema:
+            properties:
+              pets:
+                items:
+                  type: array
+                  properties:
+                    repository:
+                      name: string
diff --git a/openapi3/testdata/relativeDocs/CustomTestRequestBody.yml b/openapi3/testdata/relativeDocs/CustomTestRequestBody.yml
new file mode 100644
index 0000000..521ad36
--- /dev/null
+++ b/openapi3/testdata/relativeDocs/CustomTestRequestBody.yml
@@ -0,0 +1,1 @@
+description: example request
\ No newline at end of file
diff --git a/openapi3/testdata/relativeDocs/CustomTestResponse.yml b/openapi3/testdata/relativeDocs/CustomTestResponse.yml
new file mode 100644
index 0000000..afa4b97
--- /dev/null
+++ b/openapi3/testdata/relativeDocs/CustomTestResponse.yml
@@ -0,0 +1,1 @@
+description: description
\ No newline at end of file
diff --git a/openapi3/testdata/relativeDocs/CustomTestSchema.yml b/openapi3/testdata/relativeDocs/CustomTestSchema.yml
new file mode 100644
index 0000000..2d8bb0e
--- /dev/null
+++ b/openapi3/testdata/relativeDocs/CustomTestSchema.yml
@@ -0,0 +1,1 @@
+type: string
\ No newline at end of file
diff --git a/openapi3/testdata/relativeDocs/CustomTestSecurityScheme.yml b/openapi3/testdata/relativeDocs/CustomTestSecurityScheme.yml
new file mode 100644
index 0000000..909d558
--- /dev/null
+++ b/openapi3/testdata/relativeDocs/CustomTestSecurityScheme.yml
@@ -0,0 +1,2 @@
+type: http
+scheme: basic
\ No newline at end of file
diff --git a/openapi3/testdata/singleresponse.openapi.json b/openapi3/testdata/singleresponse.openapi.json
new file mode 100644
index 0000000..fed1b0d
--- /dev/null
+++ b/openapi3/testdata/singleresponse.openapi.json
@@ -0,0 +1,3 @@
+{
+  "description": "this is a single response definition"
+}
diff --git a/openapi3/testdata/singleresponse.openapi.yml b/openapi3/testdata/singleresponse.openapi.yml
new file mode 100644
index 0000000..038e390
--- /dev/null
+++ b/openapi3/testdata/singleresponse.openapi.yml
@@ -0,0 +1,2 @@
+---
+description: this is a single response definition
diff --git a/openapi3/testdata/testrefsinglecomponent.openapi.json b/openapi3/testdata/testrefsinglecomponent.openapi.json
new file mode 100644
index 0000000..2f72497
--- /dev/null
+++ b/openapi3/testdata/testrefsinglecomponent.openapi.json
@@ -0,0 +1,15 @@
+{
+    "openapi": "3.0.0",
+    "info": {
+        "title": "",
+        "version": "1"
+    },
+    "paths": {},
+    "components": {
+        "responses": {
+            "SomeResponse": {
+                "$ref": "singleresponse.openapi.json"
+            }
+        }
+    }
+}
diff --git a/openapi3/testdata/testrefsinglecomponent.openapi.yml b/openapi3/testdata/testrefsinglecomponent.openapi.yml
new file mode 100644
index 0000000..9ecee5a
--- /dev/null
+++ b/openapi3/testdata/testrefsinglecomponent.openapi.yml
@@ -0,0 +1,10 @@
+---
+openapi: 3.0.0
+info:
+  title: 'OAI Specification w/ refs in YAML'
+  version: '1'
+paths: {}
+components:
+  responses:
+    SomeResponse:
+      "$ref": singleresponse.openapi.yml