package openapi3 import ( "context" "encoding/json" "errors" "fmt" "io" "net/url" "os" "path" "path/filepath" "reflect" "strconv" "strings" ) // IncludeOrigin specifies whether to include the origin of the OpenAPI elements. // Deprecated: set Loader.IncludeOrigin instead. This global is read by NewLoader // for backward compatibility but is not safe for concurrent use. var IncludeOrigin = false func failedToResolveRefFragmentPart(value, what string) error { return fmt.Errorf("failed to resolve %q in fragment in URI: %q", what, value) } // Loader helps deserialize an OpenAPIv3 document type Loader struct { // IsExternalRefsAllowed enables visiting other files. Enforced only when // ReadFromURIFunc is nil; a custom ReadFromURIFunc bypasses this flag and // owns the access policy itself — see ReadFromURIFunc. IsExternalRefsAllowed bool // IncludeOrigin enables recording the file/line/column of each OpenAPI element. // Prefer this over the package-level IncludeOrigin global, which is not safe for // concurrent use. IncludeOrigin bool // ReadFromURIFunc overrides how the loader reads a referenced file or URL. // // SECURITY: when a custom ReadFromURIFunc is set, IsExternalRefsAllowed is // NOT enforced — this function alone decides which locations may be read. A // func that reads whatever URI it is handed (e.g. by delegating to // DefaultReadFromURI) resolves external $refs even when IsExternalRefsAllowed // is false, which on untrusted documents enables local file reads // (`$ref: "/etc/passwd"`) and SSRF (`$ref: "http://169.254.169.254/..."`). // A custom func must apply its own scheme/host allowlist, or re-check // IsExternalRefsAllowed, before reading. ReadFromURIFunc ReadFromURIFunc // JoinFunc allows overriding how relative $ref paths are resolved against // a base path. When set, it is called instead of the default join logic // that uses path.Dir and path.Join. This is useful when loading specs from // non-filesystem sources (e.g. git objects, remote archives) where the base // path follows a different convention than filesystem paths. JoinFunc func(basePath *url.URL, relativePath *url.URL) *url.URL Context context.Context rootDir string rootLocation string visitedPathItemRefs map[string]struct{} visitedDocuments map[string]*T visitedRefs map[string]struct{} visitedPath []string backtrack map[string][]func(value any) } // NewLoader returns an empty Loader func NewLoader() *Loader { return &Loader{ Context: context.Background(), IncludeOrigin: IncludeOrigin, } } func (loader *Loader) resetVisitedPathItemRefs() { loader.visitedPathItemRefs = make(map[string]struct{}) loader.visitedRefs = make(map[string]struct{}) loader.visitedPath = nil loader.backtrack = make(map[string][]func(value any)) } // LoadFromURI loads a spec from a remote URL func (loader *Loader) LoadFromURI(location *url.URL) (*T, error) { loader.resetVisitedPathItemRefs() return loader.loadFromURIInternal(location) } // 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)}) } func (loader *Loader) loadFromURIInternal(location *url.URL) (*T, error) { data, err := loader.readURL(location) if err != nil { return nil, err } return loader.loadFromDataWithPathInternal(data, location) } func (loader *Loader) allowsExternalRefs(ref string) (err error) { if !loader.IsExternalRefsAllowed { err = fmt.Errorf("encountered disallowed external reference: %q", ref) } return } func (loader *Loader) loadSingleElementFromURI(ref string, rootPath *url.URL, element any) (*url.URL, error) { // IsExternalRefsAllowed is enforced here only when no custom ReadFromURIFunc // is installed; otherwise the custom func owns the access policy (see the // SECURITY note on the ReadFromURIFunc field). if loader.ReadFromURIFunc == nil { if err := loader.allowsExternalRefs(ref); err != nil { return nil, err } } resolvedPath, err := loader.resolvePathWithRef(ref, rootPath) if err != nil { return nil, err } if frag := resolvedPath.Fragment; frag != "" { return nil, fmt.Errorf("unexpected ref fragment %q", frag) } data, err := loader.readURL(resolvedPath) if err != nil { return nil, err } if err := unmarshal(data, element, loader.IncludeOrigin, resolvedPath); err != nil { return nil, err } return resolvedPath, nil } func (loader *Loader) readURL(location *url.URL) ([]byte, error) { if f := loader.ReadFromURIFunc; f != nil { return f(loader, location) } return DefaultReadFromURI(loader, location) } // LoadFromStdin loads a spec from stdin func (loader *Loader) LoadFromStdin() (*T, error) { return loader.LoadFromIoReader(os.Stdin) } // LoadFromStdin loads a spec from io.Reader func (loader *Loader) LoadFromIoReader(reader io.Reader) (*T, error) { if reader == nil { return nil, fmt.Errorf("invalid reader: %v", reader) } data, err := io.ReadAll(reader) if err != nil { return nil, err } return loader.LoadFromData(data) } // LoadFromData loads a spec from a byte array func (loader *Loader) LoadFromData(data []byte) (*T, error) { loader.resetVisitedPathItemRefs() doc := &T{} if err := unmarshal(data, doc, loader.IncludeOrigin, nil); err != nil { return nil, err } if err := loader.ResolveRefsIn(doc, nil); err != nil { return nil, err } return doc, nil } // LoadFromDataWithPath takes the OpenAPI document data in bytes and a path where the resolver can find referred // elements and returns a *T with all resolved data or an error if unable to load data or resolve refs. func (loader *Loader) LoadFromDataWithPath(data []byte, location *url.URL) (*T, error) { loader.resetVisitedPathItemRefs() return loader.loadFromDataWithPathInternal(data, location) } func (loader *Loader) loadFromDataWithPathInternal(data []byte, location *url.URL) (*T, error) { if loader.visitedDocuments == nil { loader.visitedDocuments = make(map[string]*T) loader.rootLocation = location.Path } uri := location.String() if doc, ok := loader.visitedDocuments[uri]; ok { return doc, nil } doc := &T{} loader.visitedDocuments[uri] = doc if err := unmarshal(data, doc, loader.IncludeOrigin, location); err != nil { return nil, err } doc.url = copyURI(location) if err := loader.ResolveRefsIn(doc, location); err != nil { return nil, err } return doc, nil } // ResolveRefsIn expands references if for instance spec was just unmarshaled func (loader *Loader) ResolveRefsIn(doc *T, location *url.URL) (err error) { if loader.Context == nil { loader.Context = context.Background() } if loader.visitedPathItemRefs == nil { loader.resetVisitedPathItemRefs() } if components := doc.Components; components != nil { for _, name := range componentNames(components.Headers) { component := components.Headers[name] if err = loader.resolveHeaderRef(doc, component, location); err != nil { return } } for _, name := range componentNames(components.Parameters) { component := components.Parameters[name] if err = loader.resolveParameterRef(doc, component, location); err != nil { return } } for _, name := range componentNames(components.RequestBodies) { component := components.RequestBodies[name] if err = loader.resolveRequestBodyRef(doc, component, location); err != nil { return } } for _, name := range componentNames(components.Responses) { component := components.Responses[name] if err = loader.resolveResponseRef(doc, component, location); err != nil { return } } for _, name := range componentNames(components.Schemas) { component := components.Schemas[name] if err = loader.resolveSchemaRef(doc, component, location, []string{}); err != nil { return } } for _, name := range componentNames(components.SecuritySchemes) { component := components.SecuritySchemes[name] if err = loader.resolveSecuritySchemeRef(doc, component, location); err != nil { return } } for _, name := range componentNames(components.Examples) { component := components.Examples[name] if err = loader.resolveExampleRef(doc, component, location); err != nil { return } } for _, name := range componentNames(components.Callbacks) { component := components.Callbacks[name] if err = loader.resolveCallbackRef(doc, component, location); err != nil { return } } } // Visit all operations pathItems := doc.Paths.Map() for _, name := range componentNames(pathItems) { pathItem := pathItems[name] if pathItem == nil { continue } if err = loader.resolvePathItemRef(doc, pathItem, location); err != nil { return } } for _, name := range componentNames(doc.Webhooks) { if pathItem := doc.Webhooks[name]; pathItem != nil { if err = loader.resolvePathItemRef(doc, pathItem, location); err != nil { return } } } return } func defaultJoin(basePath *url.URL, relativePath *url.URL) *url.URL { if basePath == nil { return relativePath } newPath := *basePath newPath.Path = path.Join(path.Dir(newPath.Path), relativePath.Path) return &newPath } func (loader *Loader) resolvePath(basePath *url.URL, componentPath *url.URL) *url.URL { if is_file(componentPath) { // support absolute paths if filepath.IsAbs(componentPath.Path) { return componentPath } if loader.JoinFunc != nil { return loader.JoinFunc(basePath, componentPath) } return defaultJoin(basePath, componentPath) } return componentPath } func (loader *Loader) resolvePathWithRef(ref string, rootPath *url.URL) (*url.URL, error) { parsedURL, err := url.Parse(ref) if err != nil { return nil, fmt.Errorf("cannot parse reference: %q: %w", ref, err) } resolvedPath := loader.resolvePath(rootPath, parsedURL) resolvedPath.Fragment = parsedURL.Fragment return resolvedPath, nil } func (loader *Loader) resolveRefPath(ref string, path *url.URL) (*url.URL, error) { if ref != "" && ref[0] == '#' { path = copyURI(path) // Resolving internal refs of a doc loaded from memory // has no path, so just set the Fragment. if path == nil { path = new(url.URL) } path.Fragment = ref return path, nil } // IsExternalRefsAllowed is enforced here only when no custom ReadFromURIFunc // is installed; otherwise the custom func owns the access policy (see the // SECURITY note on the ReadFromURIFunc field). if loader.ReadFromURIFunc == nil { if err := loader.allowsExternalRefs(ref); err != nil { return nil, err } } resolvedPath, err := loader.resolvePathWithRef(ref, path) if err != nil { return nil, err } return resolvedPath, nil } func isSingleRefElement(ref string) bool { return !strings.Contains(ref, "#") } func (loader *Loader) visitRef(ref string) { if loader.visitedRefs == nil { loader.visitedRefs = make(map[string]struct{}) loader.backtrack = make(map[string][]func(value any)) } loader.visitedPath = append(loader.visitedPath, ref) loader.visitedRefs[ref] = struct{}{} } func (loader *Loader) unvisitRef(ref string, value any) { if value != nil { for _, fn := range loader.backtrack[ref] { fn(value) } } delete(loader.visitedRefs, ref) delete(loader.backtrack, ref) loader.visitedPath = loader.visitedPath[:len(loader.visitedPath)-1] } func (loader *Loader) shouldVisitRef(ref string, fn func(value any)) bool { if _, ok := loader.visitedRefs[ref]; ok { loader.backtrack[ref] = append(loader.backtrack[ref], fn) return false } return true } func (loader *Loader) resolveComponent(doc *T, ref string, path *url.URL, resolved any) ( componentDoc *T, componentPath *url.URL, err error, ) { if componentDoc, ref, componentPath, err = loader.resolveRefAndDocument(doc, ref, path); err != nil { return nil, nil, err } parsedURL, err := url.Parse(ref) if err != nil { return nil, nil, fmt.Errorf("cannot parse reference: %q: %v", ref, parsedURL) } fragment := parsedURL.Fragment if fragment == "" { fragment = "/" } if fragment[0] != '/' { return nil, nil, fmt.Errorf("expected fragment prefix '#/' in URI %q", ref) } drill := func(cursor any) (any, error) { for pathPart := range strings.SplitSeq(fragment[1:], "/") { pathPart = unescapeRefString(pathPart) attempted := false switch c := cursor.(type) { // Special case of T // See issue856: a ref to doc => we assume that doc is a T => things live in T.Extensions case *T: if pathPart == "" { cursor = c.Extensions attempted = true } // Special case due to multijson case *SchemaRef: if pathPart == "additionalProperties" { if ap := c.Value.AdditionalProperties.Has; ap != nil { cursor = *ap } else { cursor = c.Value.AdditionalProperties.Schema } attempted = true } case *Responses: cursor = c.m // m map[string]*ResponseRef case *Callback: cursor = c.m // m map[string]*PathItem case *Paths: cursor = c.m // m map[string]*PathItem } if !attempted { if cursor, err = drillIntoField(cursor, pathPart); err != nil { e := failedToResolveRefFragmentPart(ref, pathPart) return nil, fmt.Errorf("%s: %w", e, err) } } if cursor == nil { return nil, failedToResolveRefFragmentPart(ref, pathPart) } } return cursor, nil } var cursor any if cursor, err = drill(componentDoc); err != nil { if path == nil { return nil, nil, err } var err2 error data, err2 := loader.readURL(path) if err2 != nil { return nil, nil, err } if err2 = unmarshal(data, &cursor, loader.IncludeOrigin, path); err2 != nil { return nil, nil, err } if cursor, err2 = drill(cursor); err2 != nil || cursor == nil { return nil, nil, err } err = nil } setPathRef := func(target any) { if i, ok := target.(interface { setRefPath(*url.URL) }); ok { pathRef := copyURI(componentPath) // Resolving internal refs of a doc loaded from memory // has no path, so just set the Fragment. if pathRef == nil { pathRef = new(url.URL) } pathRef.Fragment = fragment i.setRefPath(pathRef) } } switch { case reflect.TypeOf(cursor) == reflect.TypeOf(resolved): setPathRef(cursor) reflect.ValueOf(resolved).Elem().Set(reflect.ValueOf(cursor).Elem()) return componentDoc, componentPath, nil case reflect.TypeOf(cursor) == reflect.TypeFor[map[string]any](): codec := func(got, expect any) error { enc, err := json.Marshal(got) if err != nil { return err } if err = json.Unmarshal(enc, expect); err != nil { return err } setPathRef(expect) return nil } if err := codec(cursor, resolved); err != nil { return nil, nil, fmt.Errorf("bad data in %q (expecting %s)", ref, readableType(resolved)) } return componentDoc, componentPath, nil default: return nil, nil, fmt.Errorf("bad data in %q (expecting %s)", ref, readableType(resolved)) } } func readableType(x any) string { switch x.(type) { case *Callback: return "callback object" case *CallbackRef: return "ref to callback object" case *ExampleRef: return "ref to example object" case *HeaderRef: return "ref to header object" case *LinkRef: return "ref to link object" case *ParameterRef: return "ref to parameter object" case *PathItem: return "pathItem object" case *RequestBodyRef: return "ref to requestBody object" case *ResponseRef: return "ref to response object" case *SchemaRef: return "ref to schema object" case *SecuritySchemeRef: return "ref to securityScheme object" default: panic(fmt.Sprintf("unreachable %T", x)) } } func drillIntoField(cursor any, fieldName string) (any, error) { switch val := reflect.Indirect(reflect.ValueOf(cursor)); val.Kind() { case reflect.Map: elementValue := val.MapIndex(reflect.ValueOf(fieldName)) if !elementValue.IsValid() { return nil, fmt.Errorf("map key %q not found", fieldName) } return elementValue.Interface(), nil case reflect.Slice: i, err := strconv.ParseUint(fieldName, 10, 32) if err != nil { return nil, err } index := int(i) if 0 > index || index >= val.Len() { return nil, errors.New("slice index out of bounds") } return val.Index(index).Interface(), nil case reflect.Struct: hasFields := false for i := range val.NumField() { hasFields = true if yamlTag := val.Type().Field(i).Tag.Get("yaml"); yamlTag != "-" { if tagName, _, _ := strings.Cut(yamlTag, ","); tagName != "" { if fieldName == tagName { return val.Field(i).Interface(), nil } } } } // if cursor is a "ref wrapper" struct (e.g. RequestBodyRef), if _, ok := val.Type().FieldByName("Value"); ok { // try digging into its Value field return drillIntoField(val.FieldByName("Value").Interface(), fieldName) } if hasFields { if ff := val.Type().Field(0); ff.PkgPath == "" && ff.Name == "Extensions" { extensions := val.Field(0).Interface().(map[string]any) if enc, ok := extensions[fieldName]; ok { return enc, nil } } } return nil, fmt.Errorf("struct field %q not found", fieldName) default: return nil, errors.New("not a map, slice nor struct") } } func (loader *Loader) resolveRefAndDocument(doc *T, ref string, path *url.URL) (*T, string, *url.URL, error) { if ref != "" && ref[0] == '#' { return doc, ref, path, nil } fragment, resolvedPath, err := loader.resolveRef(ref, path) if err != nil { return nil, "", nil, err } if doc, err = loader.loadFromURIInternal(resolvedPath); err != nil { return nil, "", nil, fmt.Errorf("error resolving reference %q: %w", ref, err) } return doc, fragment, resolvedPath, nil } func (loader *Loader) resolveRef(ref string, path *url.URL) (string, *url.URL, error) { resolvedPathRef, err := loader.resolveRefPath(ref, path) if err != nil { return "", nil, err } fragment := "#" + resolvedPathRef.Fragment resolvedPathRef.Fragment = "" return fragment, resolvedPathRef, nil } var ( errMUSTCallback = errors.New("invalid callback: value MUST be an object") errMUSTExample = errors.New("invalid example: value MUST be an object") errMUSTHeader = errors.New("invalid header: value MUST be an object") errMUSTLink = errors.New("invalid link: value MUST be an object") errMUSTParameter = errors.New("invalid parameter: value MUST be an object") errMUSTPathItem = errors.New("invalid path item: value MUST be an object") errMUSTRequestBody = errors.New("invalid requestBody: value MUST be an object") errMUSTResponse = errors.New("invalid response: value MUST be an object") errMUSTSchema = errors.New("invalid schema: value MUST be an object") errMUSTSecurityScheme = errors.New("invalid securityScheme: value MUST be an object") ) func (loader *Loader) resolveHeaderRef(doc *T, component *HeaderRef, documentPath *url.URL) (err error) { if component.isEmpty() { return errMUSTHeader } if ref := component.Ref; ref != "" { if component.Value != nil { return nil } if !loader.shouldVisitRef(ref, func(value any) { component.Value = value.(*Header) refPath, _ := loader.resolveRefPath(ref, documentPath) component.setRefPath(refPath) }) { return nil } loader.visitRef(ref) if isSingleRefElement(ref) { var header Header if documentPath, err = loader.loadSingleElementFromURI(ref, documentPath, &header); err != nil { return err } component.Value = &header component.setRefPath(documentPath) } else { var resolved HeaderRef doc, componentPath, err := loader.resolveComponent(doc, ref, documentPath, &resolved) if err != nil { return err } if err := loader.resolveHeaderRef(doc, &resolved, componentPath); err != nil { if err == errMUSTHeader { return nil } return err } component.Value = resolved.Value component.setRefPath(resolved.RefPath()) } defer loader.unvisitRef(ref, component.Value) } value := component.Value if value == nil { return nil } if schema := value.Schema; schema != nil { if err := loader.resolveSchemaRef(doc, schema, documentPath, []string{}); err != nil { return err } } for _, k := range componentNames(value.Examples) { if err := loader.resolveExampleRef(doc, value.Examples[k], documentPath); err != nil { return err } } return nil } func (loader *Loader) resolveParameterRef(doc *T, component *ParameterRef, documentPath *url.URL) (err error) { if component.isEmpty() { return errMUSTParameter } if ref := component.Ref; ref != "" { if component.Value != nil { return nil } if !loader.shouldVisitRef(ref, func(value any) { component.Value = value.(*Parameter) refPath, _ := loader.resolveRefPath(ref, documentPath) component.setRefPath(refPath) }) { return nil } loader.visitRef(ref) if isSingleRefElement(ref) { var param Parameter if documentPath, err = loader.loadSingleElementFromURI(ref, documentPath, ¶m); err != nil { return err } component.Value = ¶m component.setRefPath(documentPath) } else { var resolved ParameterRef doc, componentPath, err := loader.resolveComponent(doc, ref, documentPath, &resolved) if err != nil { return err } if err := loader.resolveParameterRef(doc, &resolved, componentPath); err != nil { if err == errMUSTParameter { return nil } return err } component.Value = resolved.Value component.setRefPath(resolved.RefPath()) } defer loader.unvisitRef(ref, component.Value) } value := component.Value if value == nil { return nil } if value.Content != nil && value.Schema != nil { return errors.New("cannot contain both schema and content in a parameter") } for _, name := range componentNames(value.Content) { contentType := value.Content[name] if schema := contentType.Schema; schema != nil { if err := loader.resolveSchemaRef(doc, schema, documentPath, []string{}); err != nil { return err } } for _, k := range componentNames(contentType.Examples) { if err := loader.resolveExampleRef(doc, contentType.Examples[k], documentPath); err != nil { return err } } } if schema := value.Schema; schema != nil { if err := loader.resolveSchemaRef(doc, schema, documentPath, []string{}); err != nil { return err } } for _, k := range componentNames(value.Examples) { if err := loader.resolveExampleRef(doc, value.Examples[k], documentPath); err != nil { return err } } return nil } func (loader *Loader) resolveRequestBodyRef(doc *T, component *RequestBodyRef, documentPath *url.URL) (err error) { if component.isEmpty() { return errMUSTRequestBody } if ref := component.Ref; ref != "" { if component.Value != nil { return nil } if !loader.shouldVisitRef(ref, func(value any) { component.Value = value.(*RequestBody) refPath, _ := loader.resolveRefPath(ref, documentPath) component.setRefPath(refPath) }) { return nil } loader.visitRef(ref) if isSingleRefElement(ref) { var requestBody RequestBody if documentPath, err = loader.loadSingleElementFromURI(ref, documentPath, &requestBody); err != nil { return err } component.Value = &requestBody component.setRefPath(documentPath) } else { var resolved RequestBodyRef doc, componentPath, err := loader.resolveComponent(doc, ref, documentPath, &resolved) if err != nil { return err } if err = loader.resolveRequestBodyRef(doc, &resolved, componentPath); err != nil { if err == errMUSTRequestBody { return nil } return err } component.Value = resolved.Value component.setRefPath(resolved.RefPath()) } defer loader.unvisitRef(ref, component.Value) } value := component.Value if value == nil { return nil } for _, name := range componentNames(value.Content) { contentType := value.Content[name] if contentType == nil { continue } for _, name := range componentNames(contentType.Examples) { example := contentType.Examples[name] if err := loader.resolveExampleRef(doc, example, documentPath); err != nil { return err } contentType.Examples[name] = example } if schema := contentType.Schema; schema != nil { if err := loader.resolveSchemaRef(doc, schema, documentPath, []string{}); err != nil { return err } } } return nil } func (loader *Loader) resolveResponseRef(doc *T, component *ResponseRef, documentPath *url.URL) (err error) { if component.isEmpty() { return errMUSTResponse } if ref := component.Ref; ref != "" { if component.Value != nil { return nil } if !loader.shouldVisitRef(ref, func(value any) { component.Value = value.(*Response) refPath, _ := loader.resolveRefPath(ref, documentPath) component.setRefPath(refPath) }) { return nil } loader.visitRef(ref) if isSingleRefElement(ref) { var resp Response if documentPath, err = loader.loadSingleElementFromURI(ref, documentPath, &resp); err != nil { return err } component.Value = &resp component.setRefPath(documentPath) } else { var resolved ResponseRef doc, componentPath, err := loader.resolveComponent(doc, ref, documentPath, &resolved) if err != nil { return err } if err := loader.resolveResponseRef(doc, &resolved, componentPath); err != nil { if err == errMUSTResponse { return nil } return err } component.Value = resolved.Value component.setRefPath(resolved.RefPath()) } defer loader.unvisitRef(ref, component.Value) } value := component.Value if value == nil { return nil } for _, name := range componentNames(value.Headers) { header := value.Headers[name] if err := loader.resolveHeaderRef(doc, header, documentPath); err != nil { return err } } for _, name := range componentNames(value.Content) { contentType := value.Content[name] if contentType == nil { continue } for _, name := range componentNames(contentType.Examples) { example := contentType.Examples[name] if err := loader.resolveExampleRef(doc, example, documentPath); err != nil { return err } contentType.Examples[name] = example } if schema := contentType.Schema; schema != nil { if err := loader.resolveSchemaRef(doc, schema, documentPath, []string{}); err != nil { return err } contentType.Schema = schema } } for _, name := range componentNames(value.Links) { link := value.Links[name] if err := loader.resolveLinkRef(doc, link, documentPath); err != nil { return err } } return nil } func (loader *Loader) resolveSchemaRef(doc *T, component *SchemaRef, documentPath *url.URL, visited []string) (err error) { if component.isEmpty() { return errMUSTSchema } if ref := component.Ref; ref != "" { if component.Value != nil { return nil } if !loader.shouldVisitRef(ref, func(value any) { component.Value = value.(*Schema) refPath, _ := loader.resolveRefPath(ref, documentPath) component.setRefPath(refPath) }) { return nil } loader.visitRef(ref) if isSingleRefElement(ref) { var schema Schema if documentPath, err = loader.loadSingleElementFromURI(ref, documentPath, &schema); err != nil { return err } component.Value = &schema component.setRefPath(documentPath) } else { var resolved SchemaRef doc, componentPath, err := loader.resolveComponent(doc, ref, documentPath, &resolved) if err != nil { return err } if err := loader.resolveSchemaRef(doc, &resolved, componentPath, visited); err != nil { if err == errMUSTSchema { return nil } return err } component.Value = resolved.Value component.setRefPath(resolved.RefPath()) } defer loader.unvisitRef(ref, component.Value) // OAS 3.1 / JSON Schema 2020-12: apply sibling keywords from the original schema // object on top of the resolved $ref value. In 3.1, siblings are not ignored — // they augment the referenced schema (e.g. deprecated:true alongside $ref). // Only apply for OAS 3.1+ — in 3.0 $ref replaces its entire object and siblings // are (validly) ignored. if doc.IsOpenAPI31OrLater() && component.sibling != nil && component.Value != nil { // Work on a copy so we don't mutate a schema shared by other references. schemaCopy := *component.Value applySiblingSchemaFields(&schemaCopy, component.sibling, component.extra) component.Value = &schemaCopy } } value := component.Value if value == nil { return nil } // ResolveRefs referred schemas if v := value.Items; v != nil { if err := loader.resolveSchemaRef(doc, v, documentPath, visited); err != nil { return err } } for _, name := range componentNames(value.Properties) { v := value.Properties[name] if err := loader.resolveSchemaRef(doc, v, documentPath, visited); err != nil { return err } } if v := value.AdditionalProperties.Schema; v != nil { if err := loader.resolveSchemaRef(doc, v, documentPath, visited); err != nil { return err } } if v := value.Not; v != nil { if err := loader.resolveSchemaRef(doc, v, documentPath, visited); err != nil { return err } } for _, v := range value.AllOf { if err := loader.resolveSchemaRef(doc, v, documentPath, visited); err != nil { return err } } for _, v := range value.AnyOf { if err := loader.resolveSchemaRef(doc, v, documentPath, visited); err != nil { return err } } for _, v := range value.OneOf { if err := loader.resolveSchemaRef(doc, v, documentPath, visited); err != nil { return err } } // Discriminator mapping refs are a special case since they are not full // ref objects but are plain strings that reference schema objects. // Only resolve refs that look like external references (contain a path). // Plain schema names like "Dog" or internal refs like "#/components/schemas/Dog" // don't need to be resolved by the loader. if value.Discriminator != nil { for _, k := range componentNames(value.Discriminator.Mapping) { v := value.Discriminator.Mapping[k] // Only resolve if it looks like an external ref (contains path separator) if strings.Contains(v.Ref, "/") && !strings.HasPrefix(v.Ref, "#") { if err := loader.resolveSchemaRef(doc, (*SchemaRef)(&v), documentPath, visited); err != nil { return err } value.Discriminator.Mapping[k] = v } } } // OpenAPI 3.1 / JSON Schema 2020-12 fields for _, v := range value.PrefixItems { if err := loader.resolveSchemaRef(doc, v, documentPath, visited); err != nil { return err } } if v := value.Contains; v != nil { if err := loader.resolveSchemaRef(doc, v, documentPath, visited); err != nil { return err } } for _, name := range componentNames(value.PatternProperties) { v := value.PatternProperties[name] if err := loader.resolveSchemaRef(doc, v, documentPath, visited); err != nil { return err } } for _, name := range componentNames(value.DependentSchemas) { v := value.DependentSchemas[name] if err := loader.resolveSchemaRef(doc, v, documentPath, visited); err != nil { return err } } for _, name := range componentNames(value.Defs) { v := value.Defs[name] if err := loader.resolveSchemaRef(doc, v, documentPath, visited); err != nil { return err } } if v := value.PropertyNames; v != nil { if err := loader.resolveSchemaRef(doc, v, documentPath, visited); err != nil { return err } } if v := value.UnevaluatedItems.Schema; v != nil { if err := loader.resolveSchemaRef(doc, v, documentPath, visited); err != nil { return err } } if v := value.UnevaluatedProperties.Schema; v != nil { if err := loader.resolveSchemaRef(doc, v, documentPath, visited); err != nil { return err } } if v := value.If; v != nil { if err := loader.resolveSchemaRef(doc, v, documentPath, visited); err != nil { return err } } if v := value.Then; v != nil { if err := loader.resolveSchemaRef(doc, v, documentPath, visited); err != nil { return err } } if v := value.Else; v != nil { if err := loader.resolveSchemaRef(doc, v, documentPath, visited); err != nil { return err } } if v := value.ContentSchema; v != nil { if err := loader.resolveSchemaRef(doc, v, documentPath, visited); err != nil { return err } } return nil } func (loader *Loader) resolveSecuritySchemeRef(doc *T, component *SecuritySchemeRef, documentPath *url.URL) (err error) { if component.isEmpty() { return errMUSTSecurityScheme } if ref := component.Ref; ref != "" { if component.Value != nil { return nil } if !loader.shouldVisitRef(ref, func(value any) { component.Value = value.(*SecurityScheme) refPath, _ := loader.resolveRefPath(ref, documentPath) component.setRefPath(refPath) }) { return nil } loader.visitRef(ref) if isSingleRefElement(ref) { var scheme SecurityScheme if _, err = loader.loadSingleElementFromURI(ref, documentPath, &scheme); err != nil { return err } component.Value = &scheme component.setRefPath(documentPath) } else { var resolved SecuritySchemeRef doc, componentPath, err := loader.resolveComponent(doc, ref, documentPath, &resolved) if err != nil { return err } if err := loader.resolveSecuritySchemeRef(doc, &resolved, componentPath); err != nil { if err == errMUSTSecurityScheme { return nil } return err } component.Value = resolved.Value component.setRefPath(resolved.RefPath()) } defer loader.unvisitRef(ref, component.Value) } return nil } func (loader *Loader) resolveExampleRef(doc *T, component *ExampleRef, documentPath *url.URL) (err error) { if ref := component.Ref; ref != "" { if component.Value != nil { return nil } if !loader.shouldVisitRef(ref, func(value any) { component.Value = value.(*Example) refPath, _ := loader.resolveRefPath(ref, documentPath) component.setRefPath(refPath) }) { return nil } loader.visitRef(ref) if isSingleRefElement(ref) { var example Example if _, err = loader.loadSingleElementFromURI(ref, documentPath, &example); err != nil { return err } component.Value = &example component.setRefPath(documentPath) } else { var resolved ExampleRef doc, componentPath, err := loader.resolveComponent(doc, ref, documentPath, &resolved) if err != nil { return err } if err := loader.resolveExampleRef(doc, &resolved, componentPath); err != nil { if err == errMUSTExample { return nil } return err } component.Value = resolved.Value component.setRefPath(resolved.RefPath()) } defer loader.unvisitRef(ref, component.Value) } return nil } func (loader *Loader) resolveCallbackRef(doc *T, component *CallbackRef, documentPath *url.URL) (err error) { if component.isEmpty() { return errMUSTCallback } if ref := component.Ref; ref != "" { if component.Value != nil { return nil } if !loader.shouldVisitRef(ref, func(value any) { component.Value = value.(*Callback) refPath, _ := loader.resolveRefPath(ref, documentPath) component.setRefPath(refPath) }) { return nil } loader.visitRef(ref) if isSingleRefElement(ref) { var resolved Callback if documentPath, err = loader.loadSingleElementFromURI(ref, documentPath, &resolved); err != nil { return err } component.Value = &resolved component.setRefPath(documentPath) } else { var resolved CallbackRef doc, componentPath, err := loader.resolveComponent(doc, ref, documentPath, &resolved) if err != nil { return err } if err = loader.resolveCallbackRef(doc, &resolved, componentPath); err != nil { if err == errMUSTCallback { return nil } return err } component.Value = resolved.Value component.setRefPath(resolved.RefPath()) } defer loader.unvisitRef(ref, component.Value) } value := component.Value if value == nil { return nil } pathItems := value.Map() for _, name := range componentNames(pathItems) { pathItem := pathItems[name] if err = loader.resolvePathItemRef(doc, pathItem, documentPath); err != nil { return err } } return nil } func (loader *Loader) resolveLinkRef(doc *T, component *LinkRef, documentPath *url.URL) (err error) { if component.isEmpty() { return errMUSTLink } if ref := component.Ref; ref != "" { if component.Value != nil { return nil } if !loader.shouldVisitRef(ref, func(value any) { component.Value = value.(*Link) refPath, _ := loader.resolveRefPath(ref, documentPath) component.setRefPath(refPath) }) { return nil } loader.visitRef(ref) if isSingleRefElement(ref) { var link Link if _, err = loader.loadSingleElementFromURI(ref, documentPath, &link); err != nil { return err } component.Value = &link component.setRefPath(documentPath) } else { var resolved LinkRef doc, componentPath, err := loader.resolveComponent(doc, ref, documentPath, &resolved) if err != nil { return err } if err := loader.resolveLinkRef(doc, &resolved, componentPath); err != nil { if err == errMUSTLink { return nil } return err } component.Value = resolved.Value component.setRefPath(resolved.RefPath()) } defer loader.unvisitRef(ref, component.Value) } return nil } func (loader *Loader) resolvePathItemRef(doc *T, pathItem *PathItem, documentPath *url.URL) (err error) { if pathItem == nil { err = errMUSTPathItem return } if ref := pathItem.Ref; ref != "" { if !pathItem.isEmpty() { return } if !loader.shouldVisitRef(ref, func(value any) { *pathItem = *value.(*PathItem) }) { return nil } loader.visitRef(ref) if isSingleRefElement(ref) { var p PathItem if documentPath, err = loader.loadSingleElementFromURI(ref, documentPath, &p); err != nil { return } *pathItem = p } else { var resolved PathItem if doc, documentPath, err = loader.resolveComponent(doc, ref, documentPath, &resolved); err != nil { if err == errMUSTPathItem { return nil } return } *pathItem = resolved } pathItem.Ref = ref defer loader.unvisitRef(ref, pathItem) } for _, parameter := range pathItem.Parameters { if err = loader.resolveParameterRef(doc, parameter, documentPath); err != nil { return } } operations := pathItem.Operations() for _, name := range componentNames(operations) { operation := operations[name] for _, parameter := range operation.Parameters { if err = loader.resolveParameterRef(doc, parameter, documentPath); err != nil { return } } if requestBody := operation.RequestBody; requestBody != nil { if err = loader.resolveRequestBodyRef(doc, requestBody, documentPath); err != nil { return } } responses := operation.Responses.Map() for _, name := range componentNames(responses) { response := responses[name] if err = loader.resolveResponseRef(doc, response, documentPath); err != nil { return } } for _, name := range componentNames(operation.Callbacks) { callback := operation.Callbacks[name] if err = loader.resolveCallbackRef(doc, callback, documentPath); err != nil { return } } } return } func unescapeRefString(ref string) string { return strings.ReplaceAll(strings.ReplaceAll(ref, "~1", "/"), "~0", "~") } // applySiblingSchemaFields overlays the fields listed in presentFields from sibling onto dst. // It is used to honour keyword siblings of $ref in OpenAPI 3.1 / JSON Schema 2020-12, where // sibling keywords are applied in addition to (not instead of) the referenced schema. // Only fields that were explicitly present in the original YAML/JSON are applied; the presentFields // slice (derived from SchemaRef.extra) carries this information. func applySiblingSchemaFields(dst, sibling *Schema, presentFields []string) { for _, field := range presentFields { switch field { case "oneOf": dst.OneOf = sibling.OneOf case "anyOf": dst.AnyOf = sibling.AnyOf case "allOf": dst.AllOf = sibling.AllOf case "not": dst.Not = sibling.Not case "type": dst.Type = sibling.Type case "title": dst.Title = sibling.Title case "format": dst.Format = sibling.Format case "description": dst.Description = sibling.Description case "enum": dst.Enum = sibling.Enum case "default": dst.Default = sibling.Default case "example": dst.Example = sibling.Example case "externalDocs": dst.ExternalDocs = sibling.ExternalDocs case "uniqueItems": dst.UniqueItems = sibling.UniqueItems case "exclusiveMinimum": dst.ExclusiveMin = sibling.ExclusiveMin case "exclusiveMaximum": dst.ExclusiveMax = sibling.ExclusiveMax case "nullable": dst.Nullable = sibling.Nullable case "readOnly": dst.ReadOnly = sibling.ReadOnly case "writeOnly": dst.WriteOnly = sibling.WriteOnly case "allowEmptyValue": dst.AllowEmptyValue = sibling.AllowEmptyValue case "deprecated": dst.Deprecated = sibling.Deprecated case "xml": dst.XML = sibling.XML case "minimum": dst.Min = sibling.Min case "maximum": dst.Max = sibling.Max case "multipleOf": dst.MultipleOf = sibling.MultipleOf case "minLength": dst.MinLength = sibling.MinLength case "maxLength": dst.MaxLength = sibling.MaxLength case "pattern": dst.Pattern = sibling.Pattern case "minItems": dst.MinItems = sibling.MinItems case "maxItems": dst.MaxItems = sibling.MaxItems case "items": dst.Items = sibling.Items case "required": dst.Required = sibling.Required case "properties": dst.Properties = sibling.Properties case "minProperties": dst.MinProps = sibling.MinProps case "maxProperties": dst.MaxProps = sibling.MaxProps case "additionalProperties": dst.AdditionalProperties = sibling.AdditionalProperties case "discriminator": dst.Discriminator = sibling.Discriminator case "const": dst.Const = sibling.Const case "examples": dst.Examples = sibling.Examples case "prefixItems": dst.PrefixItems = sibling.PrefixItems case "contains": dst.Contains = sibling.Contains case "minContains": dst.MinContains = sibling.MinContains case "maxContains": dst.MaxContains = sibling.MaxContains case "patternProperties": dst.PatternProperties = sibling.PatternProperties case "dependentSchemas": dst.DependentSchemas = sibling.DependentSchemas case "propertyNames": dst.PropertyNames = sibling.PropertyNames case "unevaluatedItems": dst.UnevaluatedItems = sibling.UnevaluatedItems case "unevaluatedProperties": dst.UnevaluatedProperties = sibling.UnevaluatedProperties case "if": dst.If = sibling.If case "then": dst.Then = sibling.Then case "else": dst.Else = sibling.Else case "dependentRequired": dst.DependentRequired = sibling.DependentRequired case "$defs": dst.Defs = sibling.Defs case "$schema": dst.SchemaDialect = sibling.SchemaDialect case "$comment": dst.Comment = sibling.Comment case "$id": dst.SchemaID = sibling.SchemaID case "$anchor": dst.Anchor = sibling.Anchor case "$dynamicRef": dst.DynamicRef = sibling.DynamicRef case "$dynamicAnchor": dst.DynamicAnchor = sibling.DynamicAnchor case "contentMediaType": dst.ContentMediaType = sibling.ContentMediaType case "contentEncoding": dst.ContentEncoding = sibling.ContentEncoding case "contentSchema": dst.ContentSchema = sibling.ContentSchema } } }