Repository: getkin/kin-openapi
Pull request #945 (2024-04-22 20:08:00)

openapi3: introduce `ReferencesComponentInRootDocument(doc *T, ref componentRef) (string, bool)`
This MR adds a function (`ReferencesComponentInRootDocument`) to check if a component reference references the same document or element as another component reference in the root document's `#/components/<type>`. If it does, it returns the name of it in the form `#/components/<type>/NameXXX`.

It does this by keeping track of the source file paths of schemas in multi-file'd specs.

This enables the amalgamation of schemas which reference the same underlying model which removes *very annoying* to use anonymous structs in generated models when using codegen tools like oapi-codegen.

TODO: ~~add MR in oapi-codegen displaying improved model generation.~~ https://github.com/deepmap/oapi-codegen/pull/1573

percivalalb: > Hello! 
> 
> How about a func that, given a `doc` and a `Schema`, returns a `*SchemaRef` (a ptr under `doc`) or `nil`? Would this not cover your use case?
> 
> 
> 
> Note: the `refs.go` file is generated.

Yes, if it also returned the name in the #components/schemas/...

Input: doc, Schema
Output: name, SchemaRef

I'll have a look at moving it out of the gen'ed file
percivalalb: I've moved the alternations out of the code gen'ed files, sorry I'm still getting familar with the project setup and just thrown everything in the helper file. I'd like to ad some unit tests of couse but hopefully this is more to your liking.
fenollp: This is looking pretty good!
Could you re-push the docs? It fails CI.
Also please send your tests away :) And an `Example` that shows usage of the function.
percivalalb: Things to do:
- I think these snippets https://github.com/getkin/kin-openapi/pull/945/files#diff-035b395ae81006bbe6cd7144c686238113762fccdb051ddaef5d507560039ee6R540 which construct the refPath should be moved to a single function in `resolveComponent`. I believe componentPath should be the value we need already.
- I'm not sure how this plays well with schema's loaded from memory (i.e. no paths)
- Also not sure if it supports external referencing other spec files or URL references
- Need to add unit tests
- Add example usage of function
percivalalb: Address most of the bullet points. WHere is the best place for the example?

Patch:
diff --git a/.github/docs/openapi3.txt b/.github/docs/openapi3.txt
index 7c95093..214af2a 100644
--- a/.github/docs/openapi3.txt
+++ b/.github/docs/openapi3.txt
@@ -110,6 +110,48 @@ func Int64Ptr(value int64) *int64
 func ReadFromFile(loader *Loader, location *url.URL) ([]byte, error)
     ReadFromFile is a ReadFromURIFunc which reads local file URIs.
 
+func ReferencesComponentInRootDocument(doc *T, ref componentRef) (string, bool)
+    ReferencesComponentInRootDocument returns if the given component reference
+    references the same document or element as another component reference in
+    the root document's '#/components/<type>'. If it does, it returns the name
+    of it in the form '#/components/<type>/NameXXX'
+
+    Of course given a component from the root document will always match itself.
+
+    https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.0.3.md#reference-object
+    https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.0.3.md#relative-references-in-urls
+
+    Example. Take the spec with directory structure:
+
+        openapi.yaml
+        schemas/
+        ├─ record.yaml
+        ├─ records.yaml
+
+    In openapi.yaml we have:
+
+        components:
+          schemas:
+            Record:
+              $ref: schemas/record.yaml
+
+    Case 1: records.yml references a component in the root document
+
+        $ref: ../openapi.yaml#/components/schemas/Record
+
+    This would return...
+
+        #/components/schemas/Record
+
+    Case 2: records.yml indirectly refers to the same schema as a schema the
+    root document's '#/components/schemas'.
+
+        $ref: ./record.yaml
+
+    This would also return...
+
+        #/components/schemas/Record
+
 func RegisterArrayUniqueItemsChecker(fn SliceUniqueItemsChecker)
     RegisterArrayUniqueItemsChecker is used to register a customized function
     used to check if JSON array have unique items.
@@ -191,11 +233,16 @@ func (callback *Callback) Value(key string) *PathItem
 type CallbackRef struct {
 	Ref   string
 	Value *Callback
+
 	// Has unexported fields.
 }
     CallbackRef represents either a Callback or a $ref to a Callback. When
     serializing and both fields are set, Ref is preferred over Value.
 
+func (x *CallbackRef) CollectionName() string
+    CollectionName returns the JSON string used for a collection of these
+    components.
+
 func (x *CallbackRef) JSONLookup(token string) (interface{}, error)
     JSONLookup implements
     https://pkg.go.dev/github.com/go-openapi/jsonpointer#JSONPointable
@@ -206,6 +253,12 @@ func (x CallbackRef) MarshalJSON() ([]byte, error)
 func (x CallbackRef) MarshalYAML() (interface{}, error)
     MarshalYAML returns the YAML encoding of CallbackRef.
 
+func (x *CallbackRef) RefPath() *url.URL
+    RefPath returns the path of the $ref relative to the root document.
+
+func (x *CallbackRef) RefString() string
+    RefString returns the $ref value.
+
 func (x *CallbackRef) UnmarshalJSON(data []byte) error
     UnmarshalJSON sets CallbackRef to a copy of data.
 
@@ -379,11 +432,16 @@ func (example *Example) Validate(ctx context.Context, opts ...ValidationOption)
 type ExampleRef struct {
 	Ref   string
 	Value *Example
+
 	// Has unexported fields.
 }
     ExampleRef represents either a Example or a $ref to a Example. When
     serializing and both fields are set, Ref is preferred over Value.
 
+func (x *ExampleRef) CollectionName() string
+    CollectionName returns the JSON string used for a collection of these
+    components.
+
 func (x *ExampleRef) JSONLookup(token string) (interface{}, error)
     JSONLookup implements
     https://pkg.go.dev/github.com/go-openapi/jsonpointer#JSONPointable
@@ -394,6 +452,12 @@ func (x ExampleRef) MarshalJSON() ([]byte, error)
 func (x ExampleRef) MarshalYAML() (interface{}, error)
     MarshalYAML returns the YAML encoding of ExampleRef.
 
+func (x *ExampleRef) RefPath() *url.URL
+    RefPath returns the path of the $ref relative to the root document.
+
+func (x *ExampleRef) RefString() string
+    RefString returns the $ref value.
+
 func (x *ExampleRef) UnmarshalJSON(data []byte) error
     UnmarshalJSON sets ExampleRef to a copy of data.
 
@@ -466,11 +530,16 @@ func (header *Header) Validate(ctx context.Context, opts ...ValidationOption) er
 type HeaderRef struct {
 	Ref   string
 	Value *Header
+
 	// Has unexported fields.
 }
     HeaderRef represents either a Header or a $ref to a Header. When serializing
     and both fields are set, Ref is preferred over Value.
 
+func (x *HeaderRef) CollectionName() string
+    CollectionName returns the JSON string used for a collection of these
+    components.
+
 func (x *HeaderRef) JSONLookup(token string) (interface{}, error)
     JSONLookup implements
     https://pkg.go.dev/github.com/go-openapi/jsonpointer#JSONPointable
@@ -481,6 +550,12 @@ func (x HeaderRef) MarshalJSON() ([]byte, error)
 func (x HeaderRef) MarshalYAML() (interface{}, error)
     MarshalYAML returns the YAML encoding of HeaderRef.
 
+func (x *HeaderRef) RefPath() *url.URL
+    RefPath returns the path of the $ref relative to the root document.
+
+func (x *HeaderRef) RefString() string
+    RefString returns the $ref value.
+
 func (x *HeaderRef) UnmarshalJSON(data []byte) error
     UnmarshalJSON sets HeaderRef to a copy of data.
 
@@ -568,11 +643,16 @@ func (link *Link) Validate(ctx context.Context, opts ...ValidationOption) error
 type LinkRef struct {
 	Ref   string
 	Value *Link
+
 	// Has unexported fields.
 }
     LinkRef represents either a Link or a $ref to a Link. When serializing and
     both fields are set, Ref is preferred over Value.
 
+func (x *LinkRef) CollectionName() string
+    CollectionName returns the JSON string used for a collection of these
+    components.
+
 func (x *LinkRef) JSONLookup(token string) (interface{}, error)
     JSONLookup implements
     https://pkg.go.dev/github.com/go-openapi/jsonpointer#JSONPointable
@@ -583,6 +663,12 @@ func (x LinkRef) MarshalJSON() ([]byte, error)
 func (x LinkRef) MarshalYAML() (interface{}, error)
     MarshalYAML returns the YAML encoding of LinkRef.
 
+func (x *LinkRef) RefPath() *url.URL
+    RefPath returns the path of the $ref relative to the root document.
+
+func (x *LinkRef) RefString() string
+    RefString returns the $ref value.
+
 func (x *LinkRef) UnmarshalJSON(data []byte) error
     UnmarshalJSON sets LinkRef to a copy of data.
 
@@ -879,11 +965,16 @@ func (parameter *Parameter) WithSchema(value *Schema) *Parameter
 type ParameterRef struct {
 	Ref   string
 	Value *Parameter
+
 	// Has unexported fields.
 }
     ParameterRef represents either a Parameter or a $ref to a Parameter.
     When serializing and both fields are set, Ref is preferred over Value.
 
+func (x *ParameterRef) CollectionName() string
+    CollectionName returns the JSON string used for a collection of these
+    components.
+
 func (x *ParameterRef) JSONLookup(token string) (interface{}, error)
     JSONLookup implements
     https://pkg.go.dev/github.com/go-openapi/jsonpointer#JSONPointable
@@ -894,6 +985,12 @@ func (x ParameterRef) MarshalJSON() ([]byte, error)
 func (x ParameterRef) MarshalYAML() (interface{}, error)
     MarshalYAML returns the YAML encoding of ParameterRef.
 
+func (x *ParameterRef) RefPath() *url.URL
+    RefPath returns the path of the $ref relative to the root document.
+
+func (x *ParameterRef) RefString() string
+    RefString returns the $ref value.
+
 func (x *ParameterRef) UnmarshalJSON(data []byte) error
     UnmarshalJSON sets ParameterRef to a copy of data.
 
@@ -1112,11 +1209,16 @@ func (requestBody *RequestBody) WithSchemaRef(value *SchemaRef, consumes []strin
 type RequestBodyRef struct {
 	Ref   string
 	Value *RequestBody
+
 	// Has unexported fields.
 }
     RequestBodyRef represents either a RequestBody or a $ref to a RequestBody.
     When serializing and both fields are set, Ref is preferred over Value.
 
+func (x *RequestBodyRef) CollectionName() string
+    CollectionName returns the JSON string used for a collection of these
+    components.
+
 func (x *RequestBodyRef) JSONLookup(token string) (interface{}, error)
     JSONLookup implements
     https://pkg.go.dev/github.com/go-openapi/jsonpointer#JSONPointable
@@ -1127,6 +1229,12 @@ func (x RequestBodyRef) MarshalJSON() ([]byte, error)
 func (x RequestBodyRef) MarshalYAML() (interface{}, error)
     MarshalYAML returns the YAML encoding of RequestBodyRef.
 
+func (x *RequestBodyRef) RefPath() *url.URL
+    RefPath returns the path of the $ref relative to the root document.
+
+func (x *RequestBodyRef) RefString() string
+    RefString returns the $ref value.
+
 func (x *RequestBodyRef) UnmarshalJSON(data []byte) error
     UnmarshalJSON sets RequestBodyRef to a copy of data.
 
@@ -1176,11 +1284,16 @@ func (m ResponseBodies) JSONLookup(token string) (interface{}, error)
 type ResponseRef struct {
 	Ref   string
 	Value *Response
+
 	// Has unexported fields.
 }
     ResponseRef represents either a Response or a $ref to a Response. When
     serializing and both fields are set, Ref is preferred over Value.
 
+func (x *ResponseRef) CollectionName() string
+    CollectionName returns the JSON string used for a collection of these
+    components.
+
 func (x *ResponseRef) JSONLookup(token string) (interface{}, error)
     JSONLookup implements
     https://pkg.go.dev/github.com/go-openapi/jsonpointer#JSONPointable
@@ -1191,6 +1304,12 @@ func (x ResponseRef) MarshalJSON() ([]byte, error)
 func (x ResponseRef) MarshalYAML() (interface{}, error)
     MarshalYAML returns the YAML encoding of ResponseRef.
 
+func (x *ResponseRef) RefPath() *url.URL
+    RefPath returns the path of the $ref relative to the root document.
+
+func (x *ResponseRef) RefString() string
+    RefString returns the $ref value.
+
 func (x *ResponseRef) UnmarshalJSON(data []byte) error
     UnmarshalJSON sets ResponseRef to a copy of data.
 
@@ -1471,6 +1590,7 @@ func (err SchemaError) Unwrap() error
 type SchemaRef struct {
 	Ref   string
 	Value *Schema
+
 	// Has unexported fields.
 }
     SchemaRef represents either a Schema or a $ref to a Schema. When serializing
@@ -1479,6 +1599,10 @@ type SchemaRef struct {
 func NewSchemaRef(ref string, value *Schema) *SchemaRef
     NewSchemaRef simply builds a SchemaRef
 
+func (x *SchemaRef) CollectionName() string
+    CollectionName returns the JSON string used for a collection of these
+    components.
+
 func (x *SchemaRef) JSONLookup(token string) (interface{}, error)
     JSONLookup implements
     https://pkg.go.dev/github.com/go-openapi/jsonpointer#JSONPointable
@@ -1489,6 +1613,12 @@ func (x SchemaRef) MarshalJSON() ([]byte, error)
 func (x SchemaRef) MarshalYAML() (interface{}, error)
     MarshalYAML returns the YAML encoding of SchemaRef.
 
+func (x *SchemaRef) RefPath() *url.URL
+    RefPath returns the path of the $ref relative to the root document.
+
+func (x *SchemaRef) RefString() string
+    RefString returns the $ref value.
+
 func (x *SchemaRef) UnmarshalJSON(data []byte) error
     UnmarshalJSON sets SchemaRef to a copy of data.
 
@@ -1620,12 +1750,17 @@ func (ss *SecurityScheme) WithType(value string) *SecurityScheme
 type SecuritySchemeRef struct {
 	Ref   string
 	Value *SecurityScheme
+
 	// Has unexported fields.
 }
     SecuritySchemeRef represents either a SecurityScheme or a $ref to a
     SecurityScheme. When serializing and both fields are set, Ref is preferred
     over Value.
 
+func (x *SecuritySchemeRef) CollectionName() string
+    CollectionName returns the JSON string used for a collection of these
+    components.
+
 func (x *SecuritySchemeRef) JSONLookup(token string) (interface{}, error)
     JSONLookup implements
     https://pkg.go.dev/github.com/go-openapi/jsonpointer#JSONPointable
@@ -1636,6 +1771,12 @@ func (x SecuritySchemeRef) MarshalJSON() ([]byte, error)
 func (x SecuritySchemeRef) MarshalYAML() (interface{}, error)
     MarshalYAML returns the YAML encoding of SecuritySchemeRef.
 
+func (x *SecuritySchemeRef) RefPath() *url.URL
+    RefPath returns the path of the $ref relative to the root document.
+
+func (x *SecuritySchemeRef) RefString() string
+    RefString returns the $ref value.
+
 func (x *SecuritySchemeRef) UnmarshalJSON(data []byte) error
     UnmarshalJSON sets SecuritySchemeRef to a copy of data.
 
diff --git a/README.md b/README.md
index aea156c..6e910bb 100644
--- a/README.md
+++ b/README.md
@@ -274,6 +274,31 @@ func safeErrorMessage(err *openapi3.SchemaError) string {
 }
 ```
 
+## Reconciling component $ref types
+
+`ReferencesComponentInRootDocument` is a useful helper function to check if a component reference
+coincides with a reference in the root document's component objects fixed fields.
+
+This can be used to determine if two schema definitions are of the same structure, helpful for
+code generation tools when generating go type models.
+
+```go
+doc, err = loader.LoadFromFile("openapi.yml")
+
+for _, path := range doc.Paths.InMatchingOrder() {
+	pathItem := doc.Paths.Find(path)
+
+	if pathItem.Get == nil || pathItem.Get.Responses.Status(200) {
+		continue
+	}
+
+	for _, s := range pathItem.Get.Responses.Status(200).Value.Content {
+		name, match := ReferencesComponentInRootDocument(doc, s.Schema)
+		fmt.Println(path, match, name) // /record true #/components/schemas/BookRecord
+	}
+}
+```
+
 This will change the schema validation errors to return only the `Reason` field, which is guaranteed to not include the original value.
 
 ## CHANGELOG: Sub-v1 breaking API changes
@@ -281,6 +306,9 @@ This will change the schema validation errors to return only the `Reason` field,
 ### v0.125.0
 * The `openapi3filter.ErrFunc` and `openapi3filter.LogFunc` func types now take the validated request's context as first argument.
 
+### v0.124.0
+* `openapi3.Schema.Type` & `openapi2.Parameter.Type` fields went from a `string` to the type `*Type` with methods: `Includes`, `Is`, `Permits` & `Slice`.
+
 ### v0.122.0
 * `Paths` field of `openapi3.T` is now a pointer
 * `Responses` field of `openapi3.Operation` is now a pointer
diff --git a/openapi3/helpers.go b/openapi3/helpers.go
index d160eb1..25da8a7 100644
--- a/openapi3/helpers.go
+++ b/openapi3/helpers.go
@@ -2,7 +2,13 @@ package openapi3
 
 import (
 	"fmt"
+	"net/url"
+	"path"
+	"reflect"
 	"regexp"
+	"strings"
+
+	"github.com/go-openapi/jsonpointer"
 )
 
 const identifierPattern = `^[a-zA-Z0-9._-]+$`
@@ -39,3 +45,192 @@ func Int64Ptr(value int64) *int64 {
 func Uint64Ptr(value uint64) *uint64 {
 	return &value
 }
+
+type componentRef interface {
+	RefString() string
+	RefPath() *url.URL
+	CollectionName() string
+}
+
+// refersToSameDocument returns if the $ref refers to the same document.
+//
+// Documents in different directories will have distinct $ref values that resolve to
+// the same document.
+// For example, consider the 3 files:
+//
+//	/records.yaml
+//	/root.yaml         $ref: records.yaml
+//	/schema/other.yaml $ref: ../records.yaml
+//
+// The records.yaml reference in the 2 latter refers to the same document.
+func refersToSameDocument(o1 componentRef, o2 componentRef) bool {
+	if o1 == nil || o2 == nil {
+		return false
+	}
+
+	r1 := o1.RefPath()
+	r2 := o2.RefPath()
+
+	if r1 == nil || r2 == nil {
+		return false
+	}
+
+	// refURL is relative to the working directory & base spec file.
+	return referenceURIMatch(r1, r2)
+}
+
+// referencesRootDocument returns if the $ref points to the root document of the OpenAPI spec.
+//
+// If the document has no location, perhaps loaded from data in memory, it always returns false.
+func referencesRootDocument(doc *T, ref componentRef) bool {
+	if doc.url == nil || ref == nil || ref.RefPath() == nil {
+		return false
+	}
+
+	refURL := *ref.RefPath()
+	refURL.Fragment = ""
+
+	// Check referenced element was in the root document.
+	return referenceURIMatch(doc.url, &refURL)
+}
+
+func referenceURIMatch(u1 *url.URL, u2 *url.URL) bool {
+	s1, s2 := *u1, *u2
+	if s1.Scheme == "" {
+		s1.Scheme = "file"
+	}
+	if s2.Scheme == "" {
+		s2.Scheme = "file"
+	}
+
+	return s1.String() == s2.String()
+}
+
+// ReferencesComponentInRootDocument returns if the given component reference references
+// the same document or element as another component reference in the root document's
+// '#/components/<type>'. If it does, it returns the name of it in the form
+// '#/components/<type>/NameXXX'
+//
+// Of course given a component from the root document will always match itself.
+//
+// https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.0.3.md#reference-object
+// https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.0.3.md#relative-references-in-urls
+//
+// Example. Take the spec with directory structure:
+//
+//	openapi.yaml
+//	schemas/
+//	├─ record.yaml
+//	├─ records.yaml
+//
+// In openapi.yaml we have:
+//
+//	components:
+//	  schemas:
+//	    Record:
+//	      $ref: schemas/record.yaml
+//
+// Case 1: records.yml references a component in the root document
+//
+//	$ref: ../openapi.yaml#/components/schemas/Record
+//
+// This would return...
+//
+//	#/components/schemas/Record
+//
+// Case 2: records.yml indirectly refers to the same schema
+// as a schema the root document's '#/components/schemas'.
+//
+//	$ref: ./record.yaml
+//
+// This would also return...
+//
+//	#/components/schemas/Record
+func ReferencesComponentInRootDocument(doc *T, ref componentRef) (string, bool) {
+	if ref == nil || ref.RefString() == "" {
+		return "", false
+	}
+
+	// Case 1:
+	// Something like: ../another-folder/document.json#/myElement
+	if isRemoteReference(ref.RefString()) && isRootComponentReference(ref.RefString(), ref.CollectionName()) {
+		// Determine if it is *this* root doc.
+		if referencesRootDocument(doc, ref) {
+			_, name, _ := strings.Cut(ref.RefString(), path.Join("#/components/", ref.CollectionName()))
+
+			return path.Join("#/components/", ref.CollectionName(), name), true
+		}
+	}
+
+	// If there are no schemas defined in the root document return early.
+	if doc.Components == nil {
+		return "", false
+	}
+
+	collection, _, err := jsonpointer.GetForToken(doc.Components, ref.CollectionName())
+	if err != nil {
+		panic(err) // unreachable
+	}
+
+	var components map[string]componentRef
+
+	componentRefType := reflect.TypeOf(new(componentRef)).Elem()
+	if t := reflect.TypeOf(collection); t.Kind() == reflect.Map &&
+		t.Key().Kind() == reflect.String &&
+		t.Elem().AssignableTo(componentRefType) {
+		v := reflect.ValueOf(collection)
+
+		components = make(map[string]componentRef, v.Len())
+		for _, key := range v.MapKeys() {
+			strct := v.MapIndex(key)
+			// Type assertion safe, already checked via reflection above.
+			components[key.Interface().(string)] = strct.Interface().(componentRef)
+		}
+	} else {
+		return "", false
+	}
+
+	// Case 2:
+	// Something like: ../openapi.yaml#/components/schemas/myElement
+	for name, s := range components {
+		// Must be a reference to a YAML file.
+		if !isWholeDocumentReference(s.RefString()) {
+			continue
+		}
+
+		// Is the schema a ref to the same resource.
+		if !refersToSameDocument(s, ref) {
+			continue
+		}
+
+		// Transform the remote ref to the equivalent schema in the root document.
+		return path.Join("#/components/", ref.CollectionName(), name), true
+	}
+
+	return "", false
+}
+
+// isElementReference takes a $ref value and checks if it references a specific element.
+func isElementReference(ref string) bool {
+	return ref != "" && !isWholeDocumentReference(ref)
+}
+
+// isSchemaReference takes a $ref value and checks if it references a schema element.
+func isRootComponentReference(ref string, compType string) bool {
+	return isElementReference(ref) && strings.Contains(ref, path.Join("#/components/", compType))
+}
+
+// isWholeDocumentReference takes a $ref value and checks if it is whole document reference.
+func isWholeDocumentReference(ref string) bool {
+	return ref != "" && !strings.ContainsAny(ref, "#")
+}
+
+// isRemoteReference takes a $ref value and checks if it is remote reference.
+func isRemoteReference(ref string) bool {
+	return ref != "" && !strings.HasPrefix(ref, "#") && !isURLReference(ref)
+}
+
+// isURLReference takes a $ref value and checks if it is URL reference.
+func isURLReference(ref string) bool {
+	return strings.HasPrefix(ref, "http://") || strings.HasPrefix(ref, "https://") || strings.HasPrefix(ref, "//")
+}
diff --git a/openapi3/loader.go b/openapi3/loader.go
index 88ab566..e43f72b 100644
--- a/openapi3/loader.go
+++ b/openapi3/loader.go
@@ -173,6 +173,11 @@ func (loader *Loader) loadFromDataWithPathInternal(data []byte, location *url.UR
 	}
 
 	doc := &T{}
+	if location != nil {
+		specURL := *location
+		doc.url = &specURL // shallow-copy
+	}
+
 	loader.visitedDocuments[uri] = doc
 
 	if err := unmarshal(data, doc); err != nil {
@@ -547,6 +552,7 @@ func (loader *Loader) resolveHeaderRef(doc *T, component *HeaderRef, documentPat
 				return err
 			}
 			component.Value = &header
+			component.refPath = *documentPath
 		} else {
 			var resolved HeaderRef
 			doc, componentPath, err := loader.resolveComponent(doc, ref, documentPath, &resolved)
@@ -560,6 +566,7 @@ func (loader *Loader) resolveHeaderRef(doc *T, component *HeaderRef, documentPat
 				return err
 			}
 			component.Value = resolved.Value
+			component.refPath = resolved.refPath
 		}
 	}
 	value := component.Value
@@ -597,6 +604,7 @@ func (loader *Loader) resolveParameterRef(doc *T, component *ParameterRef, docum
 				return err
 			}
 			component.Value = &param
+			component.refPath = *documentPath
 		} else {
 			var resolved ParameterRef
 			doc, componentPath, err := loader.resolveComponent(doc, ref, documentPath, &resolved)
@@ -610,6 +618,7 @@ func (loader *Loader) resolveParameterRef(doc *T, component *ParameterRef, docum
 				return err
 			}
 			component.Value = resolved.Value
+			component.refPath = resolved.refPath
 		}
 	}
 	value := component.Value
@@ -657,6 +666,7 @@ func (loader *Loader) resolveRequestBodyRef(doc *T, component *RequestBodyRef, d
 				return err
 			}
 			component.Value = &requestBody
+			component.refPath = *documentPath
 		} else {
 			var resolved RequestBodyRef
 			doc, componentPath, err := loader.resolveComponent(doc, ref, documentPath, &resolved)
@@ -670,6 +680,7 @@ func (loader *Loader) resolveRequestBodyRef(doc *T, component *RequestBodyRef, d
 				return err
 			}
 			component.Value = resolved.Value
+			component.refPath = resolved.refPath
 		}
 	}
 	value := component.Value
@@ -724,6 +735,7 @@ func (loader *Loader) resolveResponseRef(doc *T, component *ResponseRef, documen
 				return err
 			}
 			component.Value = &resp
+			component.refPath = *documentPath
 		} else {
 			var resolved ResponseRef
 			doc, componentPath, err := loader.resolveComponent(doc, ref, documentPath, &resolved)
@@ -737,6 +749,7 @@ func (loader *Loader) resolveResponseRef(doc *T, component *ResponseRef, documen
 				return err
 			}
 			component.Value = resolved.Value
+			component.refPath = resolved.refPath
 		}
 	}
 	value := component.Value
@@ -802,6 +815,7 @@ func (loader *Loader) resolveSchemaRef(doc *T, component *SchemaRef, documentPat
 				return err
 			}
 			component.Value = &schema
+			component.refPath = *documentPath
 		} else {
 			if visitedLimit(visited, ref) {
 				visited = append(visited, ref)
@@ -821,6 +835,7 @@ func (loader *Loader) resolveSchemaRef(doc *T, component *SchemaRef, documentPat
 				return err
 			}
 			component.Value = resolved.Value
+			component.refPath = resolved.refPath
 		}
 		if loader.visitedSchema == nil {
 			loader.visitedSchema = make(map[*Schema]struct{})
@@ -893,6 +908,7 @@ func (loader *Loader) resolveSecuritySchemeRef(doc *T, component *SecurityScheme
 				return err
 			}
 			component.Value = &scheme
+			component.refPath = *documentPath
 		} else {
 			var resolved SecuritySchemeRef
 			doc, componentPath, err := loader.resolveComponent(doc, ref, documentPath, &resolved)
@@ -906,6 +922,7 @@ func (loader *Loader) resolveSecuritySchemeRef(doc *T, component *SecurityScheme
 				return err
 			}
 			component.Value = resolved.Value
+			component.refPath = resolved.refPath
 		}
 	}
 	return nil
@@ -933,6 +950,7 @@ func (loader *Loader) resolveExampleRef(doc *T, component *ExampleRef, documentP
 				return err
 			}
 			component.Value = &example
+			component.refPath = *documentPath
 		} else {
 			var resolved ExampleRef
 			doc, componentPath, err := loader.resolveComponent(doc, ref, documentPath, &resolved)
@@ -946,6 +964,7 @@ func (loader *Loader) resolveExampleRef(doc *T, component *ExampleRef, documentP
 				return err
 			}
 			component.Value = resolved.Value
+			component.refPath = resolved.refPath
 		}
 	}
 	return nil
@@ -973,6 +992,7 @@ func (loader *Loader) resolveCallbackRef(doc *T, component *CallbackRef, documen
 				return err
 			}
 			component.Value = &resolved
+			component.refPath = *documentPath
 		} else {
 			var resolved CallbackRef
 			doc, componentPath, err := loader.resolveComponent(doc, ref, documentPath, &resolved)
@@ -986,6 +1006,7 @@ func (loader *Loader) resolveCallbackRef(doc *T, component *CallbackRef, documen
 				return err
 			}
 			component.Value = resolved.Value
+			component.refPath = resolved.refPath
 		}
 	}
 	value := component.Value
@@ -1023,6 +1044,7 @@ func (loader *Loader) resolveLinkRef(doc *T, component *LinkRef, documentPath *u
 				return err
 			}
 			component.Value = &link
+			component.refPath = *documentPath
 		} else {
 			var resolved LinkRef
 			doc, componentPath, err := loader.resolveComponent(doc, ref, documentPath, &resolved)
@@ -1036,6 +1058,7 @@ func (loader *Loader) resolveLinkRef(doc *T, component *LinkRef, documentPath *u
 				return err
 			}
 			component.Value = resolved.Value
+			component.refPath = resolved.refPath
 		}
 	}
 	return nil
diff --git a/openapi3/openapi3.go b/openapi3/openapi3.go
index 04bac8f..ea3c985 100644
--- a/openapi3/openapi3.go
+++ b/openapi3/openapi3.go
@@ -5,6 +5,7 @@ import (
 	"encoding/json"
 	"errors"
 	"fmt"
+	"net/url"
 
 	"github.com/go-openapi/jsonpointer"
 )
@@ -24,6 +25,7 @@ type T struct {
 	ExternalDocs *ExternalDocs        `json:"externalDocs,omitempty" yaml:"externalDocs,omitempty"`
 
 	visited visitedComponent
+	url     *url.URL
 }
 
 var _ jsonpointer.JSONPointable = (*T)(nil)
diff --git a/openapi3/refs.go b/openapi3/refs.go
index 087e5ab..34f9552 100644
--- a/openapi3/refs.go
+++ b/openapi3/refs.go
@@ -5,6 +5,7 @@ import (
 	"context"
 	"encoding/json"
 	"fmt"
+	"net/url"
 	"sort"
 
 	"github.com/go-openapi/jsonpointer"
@@ -17,12 +18,23 @@ type CallbackRef struct {
 	Ref   string
 	Value *Callback
 	extra []string
+
+	refPath url.URL
 }
 
 var _ jsonpointer.JSONPointable = (*CallbackRef)(nil)
 
 func (x *CallbackRef) isEmpty() bool { return x == nil || x.Ref == "" && x.Value == nil }
 
+// RefString returns the $ref value.
+func (x *CallbackRef) RefString() string { return x.Ref }
+
+// CollectionName returns the JSON string used for a collection of these components.
+func (x *CallbackRef) CollectionName() string { return "callbacks" }
+
+// RefPath returns the path of the $ref relative to the root document.
+func (x *CallbackRef) RefPath() *url.URL { return &x.refPath }
+
 // MarshalYAML returns the YAML encoding of CallbackRef.
 func (x CallbackRef) MarshalYAML() (interface{}, error) {
 	if ref := x.Ref; ref != "" {
@@ -96,12 +108,23 @@ type ExampleRef struct {
 	Ref   string
 	Value *Example
 	extra []string
+
+	refPath url.URL
 }
 
 var _ jsonpointer.JSONPointable = (*ExampleRef)(nil)
 
 func (x *ExampleRef) isEmpty() bool { return x == nil || x.Ref == "" && x.Value == nil }
 
+// RefString returns the $ref value.
+func (x *ExampleRef) RefString() string { return x.Ref }
+
+// CollectionName returns the JSON string used for a collection of these components.
+func (x *ExampleRef) CollectionName() string { return "examples" }
+
+// RefPath returns the path of the $ref relative to the root document.
+func (x *ExampleRef) RefPath() *url.URL { return &x.refPath }
+
 // MarshalYAML returns the YAML encoding of ExampleRef.
 func (x ExampleRef) MarshalYAML() (interface{}, error) {
 	if ref := x.Ref; ref != "" {
@@ -175,12 +198,23 @@ type HeaderRef struct {
 	Ref   string
 	Value *Header
 	extra []string
+
+	refPath url.URL
 }
 
 var _ jsonpointer.JSONPointable = (*HeaderRef)(nil)
 
 func (x *HeaderRef) isEmpty() bool { return x == nil || x.Ref == "" && x.Value == nil }
 
+// RefString returns the $ref value.
+func (x *HeaderRef) RefString() string { return x.Ref }
+
+// CollectionName returns the JSON string used for a collection of these components.
+func (x *HeaderRef) CollectionName() string { return "headers" }
+
+// RefPath returns the path of the $ref relative to the root document.
+func (x *HeaderRef) RefPath() *url.URL { return &x.refPath }
+
 // MarshalYAML returns the YAML encoding of HeaderRef.
 func (x HeaderRef) MarshalYAML() (interface{}, error) {
 	if ref := x.Ref; ref != "" {
@@ -254,12 +288,23 @@ type LinkRef struct {
 	Ref   string
 	Value *Link
 	extra []string
+
+	refPath url.URL
 }
 
 var _ jsonpointer.JSONPointable = (*LinkRef)(nil)
 
 func (x *LinkRef) isEmpty() bool { return x == nil || x.Ref == "" && x.Value == nil }
 
+// RefString returns the $ref value.
+func (x *LinkRef) RefString() string { return x.Ref }
+
+// CollectionName returns the JSON string used for a collection of these components.
+func (x *LinkRef) CollectionName() string { return "links" }
+
+// RefPath returns the path of the $ref relative to the root document.
+func (x *LinkRef) RefPath() *url.URL { return &x.refPath }
+
 // MarshalYAML returns the YAML encoding of LinkRef.
 func (x LinkRef) MarshalYAML() (interface{}, error) {
 	if ref := x.Ref; ref != "" {
@@ -333,12 +378,23 @@ type ParameterRef struct {
 	Ref   string
 	Value *Parameter
 	extra []string
+
+	refPath url.URL
 }
 
 var _ jsonpointer.JSONPointable = (*ParameterRef)(nil)
 
 func (x *ParameterRef) isEmpty() bool { return x == nil || x.Ref == "" && x.Value == nil }
 
+// RefString returns the $ref value.
+func (x *ParameterRef) RefString() string { return x.Ref }
+
+// CollectionName returns the JSON string used for a collection of these components.
+func (x *ParameterRef) CollectionName() string { return "parameters" }
+
+// RefPath returns the path of the $ref relative to the root document.
+func (x *ParameterRef) RefPath() *url.URL { return &x.refPath }
+
 // MarshalYAML returns the YAML encoding of ParameterRef.
 func (x ParameterRef) MarshalYAML() (interface{}, error) {
 	if ref := x.Ref; ref != "" {
@@ -412,12 +468,23 @@ type RequestBodyRef struct {
 	Ref   string
 	Value *RequestBody
 	extra []string
+
+	refPath url.URL
 }
 
 var _ jsonpointer.JSONPointable = (*RequestBodyRef)(nil)
 
 func (x *RequestBodyRef) isEmpty() bool { return x == nil || x.Ref == "" && x.Value == nil }
 
+// RefString returns the $ref value.
+func (x *RequestBodyRef) RefString() string { return x.Ref }
+
+// CollectionName returns the JSON string used for a collection of these components.
+func (x *RequestBodyRef) CollectionName() string { return "requestBodies" }
+
+// RefPath returns the path of the $ref relative to the root document.
+func (x *RequestBodyRef) RefPath() *url.URL { return &x.refPath }
+
 // MarshalYAML returns the YAML encoding of RequestBodyRef.
 func (x RequestBodyRef) MarshalYAML() (interface{}, error) {
 	if ref := x.Ref; ref != "" {
@@ -491,12 +558,23 @@ type ResponseRef struct {
 	Ref   string
 	Value *Response
 	extra []string
+
+	refPath url.URL
 }
 
 var _ jsonpointer.JSONPointable = (*ResponseRef)(nil)
 
 func (x *ResponseRef) isEmpty() bool { return x == nil || x.Ref == "" && x.Value == nil }
 
+// RefString returns the $ref value.
+func (x *ResponseRef) RefString() string { return x.Ref }
+
+// CollectionName returns the JSON string used for a collection of these components.
+func (x *ResponseRef) CollectionName() string { return "responses" }
+
+// RefPath returns the path of the $ref relative to the root document.
+func (x *ResponseRef) RefPath() *url.URL { return &x.refPath }
+
 // MarshalYAML returns the YAML encoding of ResponseRef.
 func (x ResponseRef) MarshalYAML() (interface{}, error) {
 	if ref := x.Ref; ref != "" {
@@ -570,12 +648,23 @@ type SchemaRef struct {
 	Ref   string
 	Value *Schema
 	extra []string
+
+	refPath url.URL
 }
 
 var _ jsonpointer.JSONPointable = (*SchemaRef)(nil)
 
 func (x *SchemaRef) isEmpty() bool { return x == nil || x.Ref == "" && x.Value == nil }
 
+// RefString returns the $ref value.
+func (x *SchemaRef) RefString() string { return x.Ref }
+
+// CollectionName returns the JSON string used for a collection of these components.
+func (x *SchemaRef) CollectionName() string { return "schemas" }
+
+// RefPath returns the path of the $ref relative to the root document.
+func (x *SchemaRef) RefPath() *url.URL { return &x.refPath }
+
 // MarshalYAML returns the YAML encoding of SchemaRef.
 func (x SchemaRef) MarshalYAML() (interface{}, error) {
 	if ref := x.Ref; ref != "" {
@@ -649,12 +738,23 @@ type SecuritySchemeRef struct {
 	Ref   string
 	Value *SecurityScheme
 	extra []string
+
+	refPath url.URL
 }
 
 var _ jsonpointer.JSONPointable = (*SecuritySchemeRef)(nil)
 
 func (x *SecuritySchemeRef) isEmpty() bool { return x == nil || x.Ref == "" && x.Value == nil }
 
+// RefString returns the $ref value.
+func (x *SecuritySchemeRef) RefString() string { return x.Ref }
+
+// CollectionName returns the JSON string used for a collection of these components.
+func (x *SecuritySchemeRef) CollectionName() string { return "securitySchemes" }
+
+// RefPath returns the path of the $ref relative to the root document.
+func (x *SecuritySchemeRef) RefPath() *url.URL { return &x.refPath }
+
 // MarshalYAML returns the YAML encoding of SecuritySchemeRef.
 func (x SecuritySchemeRef) MarshalYAML() (interface{}, error) {
 	if ref := x.Ref; ref != "" {
diff --git a/openapi3/refs.tmpl b/openapi3/refs.tmpl
index 638d646..31e6a06 100644
--- a/openapi3/refs.tmpl
+++ b/openapi3/refs.tmpl
@@ -5,34 +5,46 @@ import (
 	"context"
 	"encoding/json"
 	"fmt"
+	"net/url"
 	"sort"
 
 	"github.com/go-openapi/jsonpointer"
 	"github.com/perimeterx/marshmallow"
 )
 {{ range $type := .Types }}
-// {{ $type }}Ref represents either a {{ $type }} or a $ref to a {{ $type }}.
+// {{ $type.Name }}Ref represents either a {{ $type.Name }} or a $ref to a {{ $type.Name }}.
 // When serializing and both fields are set, Ref is preferred over Value.
-type {{ $type }}Ref struct {
+type {{ $type.Name }}Ref struct {
 	Ref   string
-	Value *{{ $type }}
+	Value *{{ $type.Name }}
 	extra []string
+
+	refPath url.URL
 }
 
-var _ jsonpointer.JSONPointable = (*{{ $type }}Ref)(nil)
+var _ jsonpointer.JSONPointable = (*{{ $type.Name }}Ref)(nil)
+
+func (x *{{ $type.Name }}Ref) isEmpty() bool { return x == nil || x.Ref == "" && x.Value == nil }
+
+// RefString returns the $ref value.
+func (x *{{ $type.Name }}Ref) RefString() string { return x.Ref }
+
+// CollectionName returns the JSON string used for a collection of these components.
+func (x *{{ $type.Name }}Ref) CollectionName() string { return "{{ $type.CollectionName }}" }
 
-func (x *{{ $type }}Ref) isEmpty() bool { return x == nil || x.Ref == "" && x.Value == nil }
+// RefPath returns the path of the $ref relative to the root document.
+func (x *{{ $type.Name }}Ref) RefPath() *url.URL { return &x.refPath }
 
-// MarshalYAML returns the YAML encoding of {{ $type }}Ref.
-func (x {{ $type }}Ref) MarshalYAML() (interface{}, error) {
+// MarshalYAML returns the YAML encoding of {{ $type.Name }}Ref.
+func (x {{ $type.Name }}Ref) MarshalYAML() (interface{}, error) {
 	if ref := x.Ref; ref != "" {
 		return &Ref{Ref: ref}, nil
 	}
 	return x.Value.MarshalYAML()
 }
 
-// MarshalJSON returns the JSON encoding of {{ $type }}Ref.
-func (x {{ $type }}Ref) MarshalJSON() ([]byte, error) {
+// MarshalJSON returns the JSON encoding of {{ $type.Name }}Ref.
+func (x {{ $type.Name }}Ref) MarshalJSON() ([]byte, error) {
 	y, err := x.MarshalYAML()
 	if err != nil {
 		return nil, err
@@ -40,8 +52,8 @@ func (x {{ $type }}Ref) MarshalJSON() ([]byte, error) {
 	return json.Marshal(y)
 }
 
-// UnmarshalJSON sets {{ $type }}Ref to a copy of data.
-func (x *{{ $type }}Ref) UnmarshalJSON(data []byte) error {
+// UnmarshalJSON sets {{ $type.Name }}Ref to a copy of data.
+func (x *{{ $type.Name }}Ref) UnmarshalJSON(data []byte) error {
 	var refOnly Ref
 	if extra, err := marshmallow.Unmarshal(data, &refOnly, marshmallow.WithExcludeKnownFieldsFromMap(true)); err == nil && refOnly.Ref != "" {
 		x.Ref = refOnly.Ref
@@ -57,8 +69,8 @@ func (x *{{ $type }}Ref) UnmarshalJSON(data []byte) error {
 	return json.Unmarshal(data, &x.Value)
 }
 
-// Validate returns an error if {{ $type }}Ref does not comply with the OpenAPI spec.
-func (x *{{ $type }}Ref) Validate(ctx context.Context, opts ...ValidationOption) error {
+// Validate returns an error if {{ $type.Name }}Ref does not comply with the OpenAPI spec.
+func (x *{{ $type.Name }}Ref) Validate(ctx context.Context, opts ...ValidationOption) error {
 	ctx = WithValidationOptions(ctx, opts...)
 	if extra := x.extra; len(extra) != 0 {
 		extras := make([]string, 0, len(extra))
@@ -82,7 +94,7 @@ func (x *{{ $type }}Ref) Validate(ctx context.Context, opts ...ValidationOption)
 }
 
 // JSONLookup implements https://pkg.go.dev/github.com/go-openapi/jsonpointer#JSONPointable
-func (x *{{ $type }}Ref) JSONLookup(token string) (interface{}, error) {
+func (x *{{ $type.Name }}Ref) JSONLookup(token string) (interface{}, error) {
 	if token == "$ref" {
 		return x.Ref, nil
 	}
diff --git a/openapi3/refsgenerator.go b/openapi3/refsgenerator.go
index 5bddfe2..f3d66bb 100644
--- a/openapi3/refsgenerator.go
+++ b/openapi3/refsgenerator.go
@@ -27,21 +27,26 @@ func main() {
 
 	packageTemplate := template.Must(template.New("openapi3-refs").Parse(tmplData))
 
+	type componentType struct {
+		Name           string
+		CollectionName string
+	}
+
 	if err := packageTemplate.Execute(file, struct {
 		Package string
-		Types   []string
+		Types   []componentType
 	}{
 		Package: os.Getenv("GOPACKAGE"), // set by the go:generate directive
-		Types: []string{
-			"Callback",
-			"Example",
-			"Header",
-			"Link",
-			"Parameter",
-			"RequestBody",
-			"Response",
-			"Schema",
-			"SecurityScheme",
+		Types: []componentType{
+			{Name: "Callback", CollectionName: "callbacks"},
+			{Name: "Example", CollectionName: "examples"},
+			{Name: "Header", CollectionName: "headers"},
+			{Name: "Link", CollectionName: "links"},
+			{Name: "Parameter", CollectionName: "parameters"},
+			{Name: "RequestBody", CollectionName: "requestBodies"},
+			{Name: "Response", CollectionName: "responses"},
+			{Name: "Schema", CollectionName: "schemas"},
+			{Name: "SecurityScheme", CollectionName: "securitySchemes"},
 		},
 	}); err != nil {
 		panic(err)


Test patch:
diff --git a/openapi3/helpers_test.go b/openapi3/helpers_test.go
new file mode 100644
index 0000000..ff350e3
--- /dev/null
+++ b/openapi3/helpers_test.go
@@ -0,0 +1,83 @@
+package openapi3
+
+import (
+	"net/url"
+	"path/filepath"
+	"testing"
+
+	"github.com/stretchr/testify/assert"
+	"github.com/stretchr/testify/require"
+)
+
+func TestReferencesComponentInRootDocument(t *testing.T) {
+	loader := NewLoader()
+	loader.IsExternalRefsAllowed = true
+
+	runAssertions := func(doc *T) {
+		// The element type of ./records.yml references a document which is also in the root document.
+		v, ok := ReferencesComponentInRootDocument(doc, doc.Components.Schemas["BookRecords"].Value.Items)
+		assert.True(t, ok)
+		assert.Equal(t, "#/components/schemas/BookRecord", v)
+
+		// The array element type directly references the component in the root document.
+		v, ok = ReferencesComponentInRootDocument(doc, doc.Components.Schemas["CdRecords"].Value.Items)
+		assert.True(t, ok)
+		assert.Equal(t, "#/components/schemas/CdRecord", v)
+
+		// A component from the root document should
+		v, ok = ReferencesComponentInRootDocument(doc, doc.Components.Schemas["CdRecord"])
+		assert.True(t, ok)
+		assert.Equal(t, "#/components/schemas/CdRecord", v)
+
+		// The error response component is in the root doc.
+		v, ok = ReferencesComponentInRootDocument(doc, doc.Paths.Find("/records").Get.Responses.Value("500"))
+		assert.True(t, ok)
+		assert.Equal(t, "#/components/responses/ErrorResponse", v)
+
+		v, ok = ReferencesComponentInRootDocument(doc, doc.Paths.Find("/records").Get.Responses.Value("500").Value.Content.Get("application/json").Schema)
+		assert.False(t, ok)
+		assert.Empty(t, v)
+
+		// Ref path doesn't include a './'
+		v, ok = ReferencesComponentInRootDocument(doc, doc.Paths.Find("/record").Get.Parameters[0])
+		assert.True(t, ok)
+		assert.Equal(t, "#/components/parameters/BookIDParameter", v)
+
+		v, ok = ReferencesComponentInRootDocument(doc, doc.Paths.Find("/record").Get.Responses.Value("200").Value.Content.Get("application/json").Examples["first-example"])
+		assert.True(t, ok)
+		assert.Equal(t, "#/components/examples/RecordResponseExample", v)
+
+		// Matches equivalent paths where string is no equal.
+		v, ok = ReferencesComponentInRootDocument(doc, doc.Paths.Find("/record").Get.Responses.Value("200").Value.Headers["X-Custom-Header"])
+		assert.True(t, ok)
+		assert.Equal(t, "#/components/headers/CustomHeader", v)
+
+		// Same structure distinct definition of the same header
+		v, ok = ReferencesComponentInRootDocument(doc, doc.Paths.Find("/record").Get.Responses.Value("200").Value.Headers["X-Custom-Header2"])
+		assert.False(t, ok)
+		assert.Empty(t, v)
+	}
+
+	// Load from the file system
+	doc, err := loader.LoadFromFile("testdata/refsToRoot/openapi.yml")
+	require.NoError(t, err)
+
+	runAssertions(doc)
+
+	// Loading from a URL by mocking HTTP calls.
+	// Loads the data using the URI path from the testdata/ folder.
+	loader.ReadFromURIFunc = func(loader *Loader, url *url.URL) ([]byte, error) {
+		localURL := *url
+		localURL.Scheme = ""
+		localURL.Host = ""
+		localURL.Path = filepath.Join("testdata", localURL.Path)
+
+		return ReadFromFile(loader, &localURL)
+	}
+
+	u, _ := url.Parse("https://example.com/refsToRoot/openapi.yml")
+	doc, err = loader.LoadFromURI(u)
+	require.NoError(t, err)
+
+	runAssertions(doc)
+}
diff --git a/openapi3/testdata/refsToRoot/openapi.yml b/openapi3/testdata/refsToRoot/openapi.yml
new file mode 100644
index 0000000..ad9fc1e
--- /dev/null
+++ b/openapi3/testdata/refsToRoot/openapi.yml
@@ -0,0 +1,60 @@
+openapi: 3.0.3
+info:
+  version: 1.0.0
+  title: Mode ref resolution Example
+paths:
+  /records:
+    get:
+      operationId: getBookRecords
+      responses:
+        200:
+          content:
+            application/json:
+              schema:
+                $ref: '#/components/schemas/BookRecords'
+        500:
+          $ref: './other/response.yml'
+  /record:
+    get:
+      operationId: getBookRecord
+      parameters:
+        - $ref: 'other/parameter.yml'
+      responses:
+        200:
+          content:
+            application/json:
+              schema:
+                $ref: '#/components/schemas/BookRecord'
+              examples:
+                first-example:
+                  $ref: './other/example.yml'
+          headers:
+            X-Custom-Header:
+              $ref: 'schema/book/../../other/header.yml'
+            X-Custom-Header2:
+              schema:
+                type: string
+        500:
+          $ref: './other/response.yml'
+components:
+  schemas:
+    BookRecord:
+      $ref: './schemas/book/record.yml'
+    BookRecords:
+      $ref: './schemas/book/records.yml'
+    CdRecord:
+      $ref: './schemas/cd/record.yml'
+    CdRecords:
+      $ref: './schemas/cd/records.yml'
+  responses:
+    ErrorResponse:
+      $ref: './other/response.yml'
+  parameters:
+    BookIDParameter:
+      $ref: './other/parameter.yml'
+  headers:
+    CustomHeader:
+      $ref: './other/header.yml'
+  examples:
+    RecordResponseExample:
+      $ref: './other/example.yml'
diff --git a/openapi3/testdata/refsToRoot/other/example.yml b/openapi3/testdata/refsToRoot/other/example.yml
new file mode 100644
index 0000000..2137d10
--- /dev/null
+++ b/openapi3/testdata/refsToRoot/other/example.yml
@@ -0,0 +1,2 @@
+description: Example example
+id: 42
diff --git a/openapi3/testdata/refsToRoot/other/header.yml b/openapi3/testdata/refsToRoot/other/header.yml
new file mode 100644
index 0000000..5fe26bf
--- /dev/null
+++ b/openapi3/testdata/refsToRoot/other/header.yml
@@ -0,0 +1,3 @@
+description: Example
+schema:
+  type: string
diff --git a/openapi3/testdata/refsToRoot/other/parameter.yml b/openapi3/testdata/refsToRoot/other/parameter.yml
new file mode 100644
index 0000000..b30e0a3
--- /dev/null
+++ b/openapi3/testdata/refsToRoot/other/parameter.yml
@@ -0,0 +1,2 @@
+name: id
+in: query
diff --git a/openapi3/testdata/refsToRoot/other/response.yml b/openapi3/testdata/refsToRoot/other/response.yml
new file mode 100644
index 0000000..e1766af
--- /dev/null
+++ b/openapi3/testdata/refsToRoot/other/response.yml
@@ -0,0 +1,4 @@
+content:
+  application/json:
+    schema:
+      $ref: '../schemas/error.yml'
diff --git a/openapi3/testdata/refsToRoot/schemas/book/record.yml b/openapi3/testdata/refsToRoot/schemas/book/record.yml
new file mode 100644
index 0000000..0e383fc
--- /dev/null
+++ b/openapi3/testdata/refsToRoot/schemas/book/record.yml
@@ -0,0 +1,6 @@
+type: object
+required:
+  - id
+properties:
+  id:
+    type: number
diff --git a/openapi3/testdata/refsToRoot/schemas/book/records.yml b/openapi3/testdata/refsToRoot/schemas/book/records.yml
new file mode 100644
index 0000000..5097d39
--- /dev/null
+++ b/openapi3/testdata/refsToRoot/schemas/book/records.yml
@@ -0,0 +1,3 @@
+type: array
+items:
+  $ref: './record.yml'
diff --git a/openapi3/testdata/refsToRoot/schemas/cd/record.yml b/openapi3/testdata/refsToRoot/schemas/cd/record.yml
new file mode 100644
index 0000000..0e383fc
--- /dev/null
+++ b/openapi3/testdata/refsToRoot/schemas/cd/record.yml
@@ -0,0 +1,6 @@
+type: object
+required:
+  - id
+properties:
+  id:
+    type: number
diff --git a/openapi3/testdata/refsToRoot/schemas/cd/records.yml b/openapi3/testdata/refsToRoot/schemas/cd/records.yml
new file mode 100644
index 0000000..22ea033
--- /dev/null
+++ b/openapi3/testdata/refsToRoot/schemas/cd/records.yml
@@ -0,0 +1,3 @@
+type: array
+items:
+  $ref: '../../openapi.yml#/components/schemas/CdRecord'
diff --git a/openapi3/testdata/refsToRoot/schemas/error.yml b/openapi3/testdata/refsToRoot/schemas/error.yml
new file mode 100644
index 0000000..f2ed543
--- /dev/null
+++ b/openapi3/testdata/refsToRoot/schemas/error.yml
@@ -0,0 +1,6 @@
+type: object
+required:
+  - msg
+properties:
+  id:
+    type: string