Repository: ogen-go/ogen
Pull request #680 (2022-11-17 11:42:20)

fix(gen): use `RefKey` as type storage key


Patch:
diff --git a/cmd/jschemagen/main.go b/cmd/jschemagen/main.go
index 7c4ea53c..9704a511 100644
--- a/cmd/jschemagen/main.go
+++ b/cmd/jschemagen/main.go
@@ -13,7 +13,9 @@ import (
 
 	"github.com/ogen-go/ogen/gen"
 	"github.com/ogen-go/ogen/gen/genfs"
+	"github.com/ogen-go/ogen/internal/jsonpointer"
 	"github.com/ogen-go/ogen/internal/ogenzap"
+	"github.com/ogen-go/ogen/internal/urlpath"
 	"github.com/ogen-go/ogen/jsonschema"
 )
 
@@ -80,6 +82,15 @@ func run() error {
 	if flag.NArg() < 1 || specPath == "" {
 		return errors.New("no spec provided")
 	}
+	specPath = filepath.Clean(specPath)
+	abs, err := filepath.Abs(specPath)
+	if err != nil {
+		return err
+	}
+	rootURL, err := urlpath.URLFromFilePath(abs)
+	if err != nil {
+		return errors.Wrap(err, "convert file path to url")
+	}
 
 	data, err := os.ReadFile(specPath)
 	if err != nil {
@@ -99,7 +110,7 @@ func run() error {
 	if err := root.Decode(&rawSchema); err != nil {
 		return errors.Wrap(err, "unmarshal")
 	}
-	schema, err := p.Parse(&rawSchema)
+	schema, err := p.Parse(&rawSchema, jsonpointer.NewResolveCtx(rootURL, jsonpointer.DefaultDepthLimit))
 	if err != nil {
 		return errors.Wrap(err, "parse")
 	}
diff --git a/gen/_template/schema/schema.tmpl b/gen/_template/schema/schema.tmpl
index 6165bec5..003ca994 100644
--- a/gen/_template/schema/schema.tmpl
+++ b/gen/_template/schema/schema.tmpl
@@ -6,8 +6,8 @@
 {{- end }}
 {{- template "godoc" $.GoDoc }}
 {{- if $.Schema }}
-	{{- if $.Schema.Ref }}
-	// Ref: {{ $.Schema.Ref }}
+	{{- with $ptr := $.Schema.Ref.Ptr }}
+	// Ref: {{ $ptr }}
 	{{- end }}
 {{- end }}
 
diff --git a/gen/gen_responses.go b/gen/gen_responses.go
index 3f336b42..cad418ec 100644
--- a/gen/gen_responses.go
+++ b/gen/gen_responses.go
@@ -153,7 +153,7 @@ func (g *Generator) responseToIR(
 	resp *openapi.Response,
 	withStatusCode bool,
 ) (ret *ir.Response, rerr error) {
-	if ref := resp.Ref; ref != "" {
+	if ref := resp.Ref; !ref.IsZero() {
 		if r, ok := ctx.lookupResponse(ref); ok {
 			return r, nil
 		}
@@ -163,7 +163,7 @@ func (g *Generator) responseToIR(
 			return nil, errors.Wrapf(err, "response name: %q", ref)
 		}
 		name = n
-		doc = fmt.Sprintf("Ref: %s", ref)
+		doc = fmt.Sprintf("Ref: %s", ref.Ptr)
 		defer func() {
 			if rerr != nil {
 				return
@@ -259,7 +259,7 @@ func wrapResponseType(
 		return t, nil
 	}
 
-	if schema := t.Schema; schema != nil && schema.Ref != "" {
+	if schema := t.Schema; schema != nil && !schema.Ref.IsZero() {
 		if t, ok := ctx.lookupWType(schema.Ref); ok {
 			return t, nil
 		}
diff --git a/gen/gen_schema.go b/gen/gen_schema.go
index fcdc687f..de491ae4 100644
--- a/gen/gen_schema.go
+++ b/gen/gen_schema.go
@@ -27,7 +27,7 @@ func saveSchemaTypes(ctx *genctx, gen *schemaGen) error {
 	for ref, t := range gen.localRefs {
 		if t.IsStruct() {
 			if err := checkStructRecursions(ctx, t); err != nil {
-				return errors.Wrap(err, ref)
+				return errors.Wrap(err, ref.String())
 			}
 		}
 		if err := ctx.saveRef(ref, t); err != nil {
@@ -99,19 +99,23 @@ func GenerateSchema(schema *jsonschema.Schema, fs FileSystem, opts GenerateSchem
 	}
 
 	// TODO(tdakkota): pass input filename
-	gen := newSchemaGen(func(ref string) (*ir.Type, bool) {
+	gen := newSchemaGen(func(ref jsonschema.Ref) (*ir.Type, bool) {
 		return nil, false
 	})
 	gen.log = opts.Logger.Named("schemagen")
 
 	{
 		prev := gen.nameRef
-		gen.nameRef = func(ref string) (string, error) {
+		gen.nameRef = func(ref jsonschema.Ref) (string, error) {
+			nref := ref
 			for _, trim := range opts.TrimPrefix {
-				ref = strings.TrimPrefix(ref, trim)
+				nref.Ptr = strings.TrimPrefix(nref.Ptr, trim)
+			}
+			if !strings.HasPrefix(nref.Ptr, "#") {
+				nref.Ptr = "#" + nref.Ptr
 			}
 
-			result, err := prev(ref)
+			result, err := prev(nref)
 			if err != nil {
 				return "", err
 			}
diff --git a/gen/genctx.go b/gen/genctx.go
index f94f5070..d7193432 100644
--- a/gen/genctx.go
+++ b/gen/genctx.go
@@ -2,6 +2,7 @@ package gen
 
 import (
 	"github.com/ogen-go/ogen/gen/ir"
+	"github.com/ogen-go/ogen/jsonschema"
 )
 
 // genctx is a generation context.
@@ -14,11 +15,11 @@ func (g *genctx) saveType(t *ir.Type) error {
 	return g.local.saveType(t)
 }
 
-func (g *genctx) saveRef(ref string, t *ir.Type) error {
+func (g *genctx) saveRef(ref jsonschema.Ref, t *ir.Type) error {
 	return g.local.saveRef(ref, t)
 }
 
-func (g *genctx) lookupRef(ref string) (*ir.Type, bool) {
+func (g *genctx) lookupRef(ref jsonschema.Ref) (*ir.Type, bool) {
 	if t, ok := g.global.refs[ref]; ok {
 		return t, true
 	}
@@ -28,15 +29,15 @@ func (g *genctx) lookupRef(ref string) (*ir.Type, bool) {
 	return nil, false
 }
 
-func (g *genctx) saveResponse(ref string, r *ir.Response) error {
+func (g *genctx) saveResponse(ref jsonschema.Ref, r *ir.Response) error {
 	return g.local.saveResponse(ref, r)
 }
 
-func (g *genctx) saveWType(ref string, t *ir.Type) error {
+func (g *genctx) saveWType(ref jsonschema.Ref, t *ir.Type) error {
 	return g.local.saveWType(ref, t)
 }
 
-func (g *genctx) lookupResponse(ref string) (*ir.Response, bool) {
+func (g *genctx) lookupResponse(ref jsonschema.Ref) (*ir.Response, bool) {
 	if r, ok := g.global.responses[ref]; ok {
 		return r, true
 	}
@@ -46,7 +47,7 @@ func (g *genctx) lookupResponse(ref string) (*ir.Response, bool) {
 	return nil, false
 }
 
-func (g *genctx) lookupWType(ref string) (*ir.Type, bool) {
+func (g *genctx) lookupWType(ref jsonschema.Ref) (*ir.Type, bool) {
 	if t, ok := g.global.wtypes[ref]; ok {
 		return t, true
 	}
diff --git a/gen/ir/type.go b/gen/ir/type.go
index 7e713650..4144f180 100644
--- a/gen/ir/type.go
+++ b/gen/ir/type.go
@@ -125,10 +125,12 @@ func (t Type) String() string {
 	b.WriteRune('(')
 	b.WriteString(t.Go())
 	b.WriteRune(')')
-	if s := t.Schema; s != nil && s.Ref != "" {
-		b.WriteRune('(')
-		b.WriteString(s.Ref)
-		b.WriteRune(')')
+	if s := t.Schema; s != nil {
+		if ref := s.Ref.String(); ref != "" {
+			b.WriteRune('(')
+			b.WriteString(ref)
+			b.WriteRune(')')
+		}
 	}
 	return b.String()
 }
diff --git a/gen/names.go b/gen/names.go
index 634a2730..d9d4001a 100644
--- a/gen/names.go
+++ b/gen/names.go
@@ -10,9 +10,12 @@ import (
 	"github.com/go-faster/errors"
 
 	"github.com/ogen-go/ogen/internal/naming"
+	"github.com/ogen-go/ogen/jsonschema"
 )
 
-func cleanRef(ref string) string {
+func cleanRef(r jsonschema.Ref) string {
+	ref := r.String()
+
 	before, result, _ := strings.Cut(ref, "#")
 	if result == "" {
 		result = ref
diff --git a/gen/reduce.go b/gen/reduce.go
index a7f4a885..88e17dce 100644
--- a/gen/reduce.go
+++ b/gen/reduce.go
@@ -21,7 +21,7 @@ func (g *Generator) reduceDefault(ops []*openapi.Operation) error {
 		return nil
 	}
 	d := first.Responses["default"]
-	if d.Ref == "" {
+	if d.Ref.IsZero() {
 		// Not supported.
 		return nil
 	}
diff --git a/gen/schema_gen.go b/gen/schema_gen.go
index f9cbb909..b98a3928 100644
--- a/gen/schema_gen.go
+++ b/gen/schema_gen.go
@@ -14,20 +14,20 @@ import (
 
 type schemaGen struct {
 	side      []*ir.Type
-	localRefs map[string]*ir.Type
-	lookupRef func(ref string) (*ir.Type, bool)
-	nameRef   func(ref string) (string, error)
+	localRefs map[jsonschema.Ref]*ir.Type
+	lookupRef func(ref jsonschema.Ref) (*ir.Type, bool)
+	nameRef   func(ref jsonschema.Ref) (string, error)
 	fail      func(err error) error
 
 	log *zap.Logger
 }
 
-func newSchemaGen(lookupRef func(ref string) (*ir.Type, bool)) *schemaGen {
+func newSchemaGen(lookupRef func(ref jsonschema.Ref) (*ir.Type, bool)) *schemaGen {
 	return &schemaGen{
 		side:      nil,
-		localRefs: map[string]*ir.Type{},
+		localRefs: map[jsonschema.Ref]*ir.Type{},
 		lookupRef: lookupRef,
-		nameRef: func(ref string) (string, error) {
+		nameRef: func(ref jsonschema.Ref) (string, error) {
 			name, err := pascal(cleanRef(ref))
 			if err != nil {
 				return "", err
@@ -73,7 +73,7 @@ func (g *schemaGen) generate2(name string, schema *jsonschema.Schema) (ret *ir.T
 		return nil, &ErrNotImplemented{Name: "empty schema"}
 	}
 
-	if ref := schema.Ref; ref != "" {
+	if ref := schema.Ref; !ref.IsZero() {
 		if t, ok := g.lookupRef(ref); ok {
 			return t, nil
 		}
@@ -304,7 +304,7 @@ func (g *schemaGen) generate2(name string, schema *jsonschema.Schema) (ret *ir.T
 
 func (g *schemaGen) regtype(name string, t *ir.Type) *ir.Type {
 	if t.Schema != nil {
-		if ref := t.Schema.Ref; ref != "" {
+		if ref := t.Schema.Ref; !ref.IsZero() {
 			if t.Is(ir.KindPrimitive, ir.KindArray, ir.KindAny) {
 				t = ir.Alias(name, t)
 			}
diff --git a/gen/schema_gen_sum.go b/gen/schema_gen_sum.go
index b0ca591f..bbf6cd0e 100644
--- a/gen/schema_gen_sum.go
+++ b/gen/schema_gen_sum.go
@@ -62,14 +62,14 @@ func canUseTypeDiscriminator(sum []*ir.Type) bool {
 }
 
 func ensureNoInfiniteRecursion(parent *jsonschema.Schema) error {
-	var do func(map[string]struct{}, []*jsonschema.Schema) error
-	do = func(ctx map[string]struct{}, schemas []*jsonschema.Schema) error {
+	var do func(map[jsonschema.Ref]struct{}, []*jsonschema.Schema) error
+	do = func(ctx map[jsonschema.Ref]struct{}, schemas []*jsonschema.Schema) error {
 		for i, s := range schemas {
 			if s == nil {
 				// Just skip nil schemas. We handle them later.
 				continue
 			}
-			if ref := s.Ref; ref != "" {
+			if ref := s.Ref; !ref.IsZero() {
 				if _, ok := ctx[ref]; ok {
 					return errors.Errorf("reference %q [%d] leads to infinite recursion", ref, i)
 				}
@@ -94,7 +94,7 @@ func ensureNoInfiniteRecursion(parent *jsonschema.Schema) error {
 		return nil
 	}
 
-	return do(map[string]struct{}{}, []*jsonschema.Schema{parent})
+	return do(map[jsonschema.Ref]struct{}{}, []*jsonschema.Schema{parent})
 }
 
 func (g *schemaGen) collectSumVariants(
@@ -135,8 +135,8 @@ func (g *schemaGen) collectSumVariants(
 	return sum, nil
 }
 
-func schemaName(ref string) (string, bool) {
-	_, after, ok := strings.Cut(ref, "#/")
+func schemaName(k jsonschema.Ref) (string, bool) {
+	_, after, ok := strings.Cut(k.Ptr, "#/")
 	if !ok || after == "" {
 		return "", false
 	}
@@ -255,7 +255,7 @@ func (g *schemaGen) oneOf(name string, schema *jsonschema.Schema) (*ir.Type, err
 			// Implicit mapping, defaults to type name.
 			keys := map[string]struct{}{}
 			for i, s := range sum.SumOf {
-				var ref string
+				var ref jsonschema.Ref
 				if s.Schema != nil {
 					ref = s.Schema.Ref
 				} else {
@@ -420,7 +420,7 @@ func (g *schemaGen) allOf(name string, schema *jsonschema.Schema) (*ir.Type, err
 	}
 
 	// Do not modify reference fields, as they may still refer to the original schema.
-	if mergedSchema.Ref == "" {
+	if mergedSchema.Ref.IsZero() {
 		mergedSchema.Ref = schema.Ref
 	}
 	return g.generate(name, mergedSchema, false)
diff --git a/gen/tstorage.go b/gen/tstorage.go
index f23cf0ab..0db7d5b2 100644
--- a/gen/tstorage.go
+++ b/gen/tstorage.go
@@ -4,11 +4,12 @@ import (
 	"github.com/go-faster/errors"
 
 	"github.com/ogen-go/ogen/gen/ir"
+	"github.com/ogen-go/ogen/jsonschema"
 )
 
 // tstorage is a type storage.
 type tstorage struct {
-	refs map[string]*ir.Type // Key: ref
+	refs map[jsonschema.Ref]*ir.Type // Key: ref
 
 	// types map contains public types.
 	// Public type is any type that has a name:
@@ -28,22 +29,22 @@ type tstorage struct {
 	//                         in the map because
 	//                         the type is anonymous.
 	//
-	types     map[string]*ir.Type     // Key: type name
-	responses map[string]*ir.Response // Key: ref
+	types     map[string]*ir.Type             // Key: type name
+	responses map[jsonschema.Ref]*ir.Response // Key: ref
 
 	// wtypes stores references to wrapped types:
 	//  * [T]StatusCode
 	//  * [T]Headers
 	//  * [T]StatusCodeWithHeaders
-	wtypes map[string]*ir.Type // Key: ref
+	wtypes map[jsonschema.Ref]*ir.Type // Key: ref
 }
 
 func newTStorage() *tstorage {
 	return &tstorage{
-		refs:      map[string]*ir.Type{},
+		refs:      map[jsonschema.Ref]*ir.Type{},
 		types:     map[string]*ir.Type{},
-		responses: map[string]*ir.Response{},
-		wtypes:    map[string]*ir.Type{},
+		responses: map[jsonschema.Ref]*ir.Response{},
+		wtypes:    map[jsonschema.Ref]*ir.Type{},
 	}
 }
 
@@ -78,7 +79,7 @@ func (s *tstorage) saveType(t *ir.Type) error {
 	return nil
 }
 
-func (s *tstorage) saveRef(ref string, t *ir.Type) error {
+func (s *tstorage) saveRef(ref jsonschema.Ref, t *ir.Type) error {
 	if _, ok := s.refs[ref]; ok {
 		return errors.Errorf("reference conflict: %q", ref)
 	}
@@ -91,7 +92,7 @@ func (s *tstorage) saveRef(ref string, t *ir.Type) error {
 	return nil
 }
 
-func (s *tstorage) saveResponse(ref string, r *ir.Response) error {
+func (s *tstorage) saveResponse(ref jsonschema.Ref, r *ir.Response) error {
 	if _, ok := s.responses[ref]; ok {
 		return errors.Errorf("reference conflict: %q", ref)
 	}
@@ -100,7 +101,7 @@ func (s *tstorage) saveResponse(ref string, r *ir.Response) error {
 	return nil
 }
 
-func (s *tstorage) saveWType(ref string, t *ir.Type) error {
+func (s *tstorage) saveWType(ref jsonschema.Ref, t *ir.Type) error {
 	if _, ok := s.wtypes[ref]; ok {
 		return errors.Errorf("reference conflict: %q", ref)
 	}
diff --git a/internal/jsonpointer/resolve_ctx.go b/internal/jsonpointer/resolve_ctx.go
index b3c5ac2b..1ceac40c 100644
--- a/internal/jsonpointer/resolve_ctx.go
+++ b/internal/jsonpointer/resolve_ctx.go
@@ -9,10 +9,26 @@ import (
 	"github.com/ogen-go/ogen/internal/location"
 )
 
-// RefKey is JSON reference key.
+// RefKey is JSON Reference key.
 type RefKey struct {
+	// Loc is an URL of JSON document.
 	Loc string
-	Ref string
+	// Ptr is JSON Pointer.
+	Ptr string
+}
+
+// String returns string representation of reference.
+func (r RefKey) String() string {
+	return r.Loc + r.Ptr
+}
+
+// IsZero returns true if RefKey is zero.
+func (r RefKey) IsZero() bool {
+	var r0 struct {
+		Loc string
+		Ptr string
+	}
+	return r == r0
 }
 
 // FromURL sets RefKey from URL.
@@ -23,15 +39,15 @@ func (r *RefKey) FromURL(u *url.URL) {
 		u2.Fragment = ""
 		r.Loc = u2.String()
 	}
-	r.Ref = "#" + u.Fragment
+	r.Ptr = "#" + u.Fragment
 }
 
 type locstackItem struct {
-	loc  string
+	loc  *url.URL
 	file location.File
 }
 
-// ResolveCtx is JSON pointer resolve context.
+// ResolveCtx is JSON Reference resolve context.
 type ResolveCtx struct {
 	// Location stack. Used for context-depending resolving.
 	//
@@ -53,9 +69,12 @@ type ResolveCtx struct {
 // DefaultDepthLimit is default depth limit for ResolveCtx.
 const DefaultDepthLimit = 1000
 
-// DefaultCtx creates new ResolveCtx with default depth limit.
-func DefaultCtx() *ResolveCtx {
-	return NewResolveCtx(nil, DefaultDepthLimit)
+// DummyURL is dummy URL for testing purposes.
+func DummyURL() *url.URL {
+	return &url.URL{
+		Scheme: "jsonschema",
+		Host:   "dummy",
+	}
 }
 
 // NewResolveCtx creates new ResolveCtx.
@@ -68,28 +87,23 @@ func NewResolveCtx(root *url.URL, depthLimit int) *ResolveCtx {
 	}
 }
 
+func (r ResolveCtx) last() (last locstackItem, ok bool) {
+	s := r.locstack
+	if len(s) == 0 {
+		return last, ok
+	}
+	return s[len(s)-1], true
+}
+
 // Key creates new reference key.
 func (r *ResolveCtx) Key(ref string) (key RefKey, _ error) {
-	parser := url.Parse
-	if r.root != nil {
-		parser = r.root.Parse
-	}
-	if s := r.locstack; len(s) > 0 {
-		base, err := url.Parse(s[len(s)-1].loc)
-		if err != nil {
-			return key, err
-		}
-		parser = func(rawURL string) (*url.URL, error) {
-			u, err := base.Parse(rawURL)
-			if err != nil {
-				return nil, err
-			}
-			return u, nil
-		}
+	parser := r.root.Parse
+	if last, ok := r.last(); ok {
+		parser = last.loc.Parse
 	} else if strings.HasPrefix(ref, "#") {
-		return RefKey{
-			Ref: ref,
-		}, nil
+		key.Ptr = ref
+		key.Loc = r.root.String()
+		return key, nil
 	}
 
 	u, err := parser(ref)
@@ -111,12 +125,14 @@ func (r *ResolveCtx) AddKey(key RefKey, file location.File) error {
 	r.refs[key] = struct{}{}
 	r.depthLimit--
 
-	if loc := key.Loc; loc != "" {
-		r.locstack = append(r.locstack, locstackItem{
-			loc:  loc,
-			file: file,
-		})
+	loc, err := url.Parse(key.Loc)
+	if err != nil {
+		return errors.Wrap(err, "invalid location")
 	}
+	r.locstack = append(r.locstack, locstackItem{
+		loc:  loc,
+		file: file,
+	})
 	return nil
 }
 
@@ -124,14 +140,15 @@ func (r *ResolveCtx) AddKey(key RefKey, file location.File) error {
 func (r *ResolveCtx) Delete(key RefKey) {
 	r.depthLimit++
 	delete(r.refs, key)
-	if key.Loc != "" && len(r.locstack) > 0 {
+	if len(r.locstack) > 0 {
 		r.locstack = r.locstack[:len(r.locstack)-1]
 	}
 }
 
 // IsRoot returns true if location stack is empty.
-func (r *ResolveCtx) IsRoot() bool {
-	return len(r.locstack) == 0
+func (r *ResolveCtx) IsRoot(key RefKey) bool {
+	resolved := errors.Must(r.root.Parse(key.Loc))
+	return resolved.String() == r.root.String()
 }
 
 // File returns last file from stack.
diff --git a/jsonschema/errors.go b/jsonschema/errors.go
index 8752196d..6269a7a3 100644
--- a/jsonschema/errors.go
+++ b/jsonschema/errors.go
@@ -12,7 +12,7 @@ type LocationError = location.Error
 
 func (p *Parser) file(ctx *jsonpointer.ResolveCtx) location.File {
 	file := ctx.File()
-	if file.IsZero() || ctx.IsRoot() {
+	if file.IsZero() {
 		return p.rootFile
 	}
 	return file
diff --git a/jsonschema/external.go b/jsonschema/external.go
index 41b13bb8..957f92e2 100644
--- a/jsonschema/external.go
+++ b/jsonschema/external.go
@@ -33,8 +33,12 @@ func (n NoExternal) Get(context.Context, string) ([]byte, error) {
 type ExternalOptions struct {
 	// HTTPClient sets http client to use. Defaults to http.DefaultClient.
 	HTTPClient *http.Client
+
 	// ReadFile sets function for reading files from fs. Defaults to os.ReadFile.
 	ReadFile func(p string) ([]byte, error)
+	// URLToFilePath sets function for converting url to file path. Defaults to urlpath.URLToFilePath.
+	URLToFilePath func(u *url.URL) (string, error)
+
 	// Logger sets logger to use. Defaults to zap.NewNop().
 	Logger *zap.Logger
 }
@@ -46,6 +50,9 @@ func (r *ExternalOptions) setDefaults() {
 	if r.ReadFile == nil {
 		r.ReadFile = os.ReadFile
 	}
+	if r.URLToFilePath == nil {
+		r.URLToFilePath = urlpath.URLToFilePath
+	}
 	if r.Logger == nil {
 		r.Logger = zap.NewNop()
 	}
@@ -54,9 +61,10 @@ func (r *ExternalOptions) setDefaults() {
 var _ ExternalResolver = externalResolver{}
 
 type externalResolver struct {
-	client   *http.Client
-	readFile func(p string) ([]byte, error)
-	logger   *zap.Logger
+	client        *http.Client
+	readFile      func(p string) ([]byte, error)
+	urlToFilePath func(u *url.URL) (string, error)
+	logger        *zap.Logger
 }
 
 // NewExternalResolver creates new ExternalResolver.
@@ -66,9 +74,10 @@ func NewExternalResolver(opts ExternalOptions) ExternalResolver {
 	opts.setDefaults()
 
 	return externalResolver{
-		client:   opts.HTTPClient,
-		readFile: opts.ReadFile,
-		logger:   opts.Logger,
+		client:        opts.HTTPClient,
+		readFile:      opts.ReadFile,
+		urlToFilePath: opts.URLToFilePath,
+		logger:        opts.Logger,
 	}
 }
 
@@ -125,7 +134,7 @@ func (e externalResolver) Get(ctx context.Context, loc string) ([]byte, error) {
 		data, err = e.httpGet(ctx, u)
 	case "file", "":
 		var p string
-		p, err = urlpath.URLToFilePath(u)
+		p, err = e.urlToFilePath(u)
 		if err != nil {
 			err = errors.Wrap(err, "convert url to file path")
 			break
diff --git a/jsonschema/parser.go b/jsonschema/parser.go
index 93c69a55..57d8f7d5 100644
--- a/jsonschema/parser.go
+++ b/jsonschema/parser.go
@@ -44,12 +44,7 @@ func NewParser(s Settings) *Parser {
 }
 
 // Parse parses given RawSchema and returns parsed Schema.
-func (p *Parser) Parse(schema *RawSchema) (*Schema, error) {
-	return p.ParseWithContext(schema, jsonpointer.DefaultCtx())
-}
-
-// ParseWithContext parses given RawSchema and returns parsed Schema.
-func (p *Parser) ParseWithContext(schema *RawSchema, ctx *jsonpointer.ResolveCtx) (*Schema, error) {
+func (p *Parser) Parse(schema *RawSchema, ctx *jsonpointer.ResolveCtx) (*Schema, error) {
 	return p.parse(schema, ctx)
 }
 
@@ -513,7 +508,7 @@ func (p *Parser) parseDiscriminator(d *RawDiscriminator, ctx *jsonpointer.Resolv
 			err error
 		)
 		switch {
-		case !strings.ContainsRune(ref, '#') && ctx.IsRoot():
+		case !strings.ContainsRune(ref, '#'):
 			// JSON Reference usually contains a fragment, e.g. "#/components/schemas/Foo" or
 			// "foo.json#/definitions/Bar", but this looks like a schema name.
 			//
diff --git a/jsonschema/resolve.go b/jsonschema/resolve.go
index c1841ec8..df54c0f1 100644
--- a/jsonschema/resolve.go
+++ b/jsonschema/resolve.go
@@ -71,9 +71,12 @@ func (p *Parser) resolve(ref string, ctx *jsonpointer.ResolveCtx) (_ *Schema, re
 		return s, nil
 	}
 
-	r, err := p.getResolver(key.Loc)
-	if err != nil {
-		return nil, err
+	r := p.schemas[""]
+	if !ctx.IsRoot(key) {
+		r, err = p.getResolver(key.Loc)
+		if err != nil {
+			return nil, err
+		}
 	}
 
 	if err := ctx.AddKey(key, r.file); err != nil {
@@ -84,13 +87,13 @@ func (p *Parser) resolve(ref string, ctx *jsonpointer.ResolveCtx) (_ *Schema, re
 		ctx.Delete(key)
 	}()
 
-	raw, err := r.ResolveReference(key.Ref)
+	raw, err := r.ResolveReference(key.Ptr)
 	if err != nil {
 		return nil, err
 	}
 
 	return p.parse1(raw, ctx, func(s *Schema) *Schema {
-		s.Ref = ref
+		s.Ref = key
 		p.refcache[key] = s
 		return p.extendInfo(raw, s, p.file(ctx))
 	})
diff --git a/jsonschema/schema.go b/jsonschema/schema.go
index 6daf938e..b088fb08 100644
--- a/jsonschema/schema.go
+++ b/jsonschema/schema.go
@@ -1,6 +1,7 @@
 package jsonschema
 
 import (
+	"github.com/ogen-go/ogen/internal/jsonpointer"
 	"github.com/ogen-go/ogen/internal/location"
 	"github.com/ogen-go/ogen/ogenregex"
 )
@@ -27,11 +28,14 @@ const (
 	Null SchemaType = "null"
 )
 
+// Ref is a JSON Schema reference.
+type Ref = jsonpointer.RefKey
+
 // Schema is a JSON Schema.
 type Schema struct {
 	XOgenName string // Annotation to set type name.
 
-	Ref string // Whether schema is referenced.
+	Ref Ref // Whether schema is referenced.
 
 	Type             SchemaType
 	Format           string // Schema format, optional.
diff --git a/openapi/api.go b/openapi/api.go
index c7eec0f0..48e1a9ab 100644
--- a/openapi/api.go
+++ b/openapi/api.go
@@ -1,7 +1,13 @@
 // Package openapi represents OpenAPI v3 Specification in Go.
 package openapi
 
-import "github.com/ogen-go/ogen/jsonschema"
+import (
+	"github.com/ogen-go/ogen/internal/jsonpointer"
+	"github.com/ogen-go/ogen/jsonschema"
+)
+
+// Ref is a JSON Reference.
+type Ref = jsonpointer.RefKey
 
 // API represents parsed OpenAPI spec.
 type API struct {
diff --git a/openapi/example.go b/openapi/example.go
index 116b2453..2d055b24 100644
--- a/openapi/example.go
+++ b/openapi/example.go
@@ -7,7 +7,7 @@ import (
 
 // Example is an OpenAPI Example.
 type Example struct {
-	Ref string
+	Ref Ref
 
 	Summary       string
 	Description   string
diff --git a/openapi/operation.go b/openapi/operation.go
index a2d52048..c28393ec 100644
--- a/openapi/operation.go
+++ b/openapi/operation.go
@@ -1,6 +1,8 @@
 package openapi
 
-import "github.com/ogen-go/ogen/internal/location"
+import (
+	"github.com/ogen-go/ogen/internal/location"
+)
 
 // Operation is an OpenAPI Operation.
 type Operation struct {
@@ -31,7 +33,7 @@ type Operation struct {
 
 // RequestBody of an OpenAPI Operation.
 type RequestBody struct {
-	Ref         string
+	Ref         Ref
 	Description string
 	Required    bool
 	Content     map[string]*MediaType
@@ -44,7 +46,7 @@ type Header = Parameter
 
 // Response is an OpenAPI Response definition.
 type Response struct {
-	Ref         string
+	Ref         Ref
 	Description string
 	Headers     map[string]*Header
 	Content     map[string]*MediaType
diff --git a/openapi/parameter.go b/openapi/parameter.go
index e7483479..1501cac0 100644
--- a/openapi/parameter.go
+++ b/openapi/parameter.go
@@ -33,7 +33,7 @@ func (l ParameterLocation) Cookie() bool { return l == LocationCookie }
 
 // Parameter is an OpenAPI Operation Parameter.
 type Parameter struct {
-	Ref string
+	Ref Ref
 
 	Name          string
 	Description   string
diff --git a/openapi/parser/errors.go b/openapi/parser/errors.go
index 29e6b1a6..aafca650 100644
--- a/openapi/parser/errors.go
+++ b/openapi/parser/errors.go
@@ -12,7 +12,7 @@ type LocationError = location.Error
 
 func (p *parser) file(ctx *jsonpointer.ResolveCtx) location.File {
 	file := ctx.File()
-	if file.IsZero() || ctx.IsRoot() {
+	if file.IsZero() {
 		return p.rootFile
 	}
 	return file
diff --git a/openapi/parser/parse_schema.go b/openapi/parser/parse_schema.go
index f85a0fca..3d91d4ef 100644
--- a/openapi/parser/parse_schema.go
+++ b/openapi/parser/parse_schema.go
@@ -9,7 +9,7 @@ import (
 )
 
 func (p *parser) parseSchema(schema *ogen.Schema, ctx *jsonpointer.ResolveCtx) (*jsonschema.Schema, error) {
-	s, err := p.schemaParser.ParseWithContext(schema.ToJSONSchema(), ctx)
+	s, err := p.schemaParser.Parse(schema.ToJSONSchema(), ctx)
 	if err != nil {
 		return nil, errors.Wrap(err, "parse schema")
 	}
diff --git a/openapi/parser/resolve.go b/openapi/parser/resolve.go
index 835ffee7..644859cb 100644
--- a/openapi/parser/resolve.go
+++ b/openapi/parser/resolve.go
@@ -81,41 +81,41 @@ func resolveComponent[Raw, Target any](
 	cr componentResolve[Raw, Target],
 	ref string,
 	ctx *jsonpointer.ResolveCtx,
-) (zero Target, cached bool, _ error) {
+) (key refKey, zero Target, cached bool, _ error) {
 	key, err := ctx.Key(ref)
 	if err != nil {
-		return zero, false, err
+		return key, zero, false, err
 	}
 
 	if r, ok := cr.cache[key]; ok {
-		return r, true, nil
+		return key, r, true, nil
 	}
 
 	file := p.rootFile
 	var raw Raw
-	if key.Loc == "" && ctx.IsRoot() {
+	if ctx.IsRoot(key) {
 		name := strings.TrimPrefix(ref, cr.prefix)
 		c, found := cr.components[name]
 		if found {
 			raw = c
 		} else {
 			if err := resolvePointer(p.spec.Raw, ref, &raw); err != nil {
-				return zero, false, err
+				return key, zero, false, err
 			}
 		}
 	} else {
 		r, err := p.getResolver(key.Loc)
 		if err != nil {
-			return zero, false, err
+			return key, zero, false, err
 		}
 		file = r.file
-		if err := resolvePointer(r.node, key.Ref, &raw); err != nil {
-			return zero, false, err
+		if err := resolvePointer(r.node, key.Ptr, &raw); err != nil {
+			return key, zero, false, err
 		}
 	}
 
 	if err := ctx.AddKey(key, file); err != nil {
-		return zero, false, err
+		return key, zero, false, err
 	}
 	defer func() {
 		ctx.Delete(key)
@@ -123,11 +123,11 @@ func resolveComponent[Raw, Target any](
 
 	r, err := cr.parse(raw, ctx)
 	if err != nil {
-		return zero, false, err
+		return key, zero, false, err
 	}
 	cr.cache[key] = r
 
-	return r, false, nil
+	return key, r, false, nil
 }
 
 func (p *parser) resolveCtx() *jsonpointer.ResolveCtx {
@@ -136,7 +136,7 @@ func (p *parser) resolveCtx() *jsonpointer.ResolveCtx {
 
 func (p *parser) resolveRequestBody(ref string, ctx *jsonpointer.ResolveCtx) (*openapi.RequestBody, error) {
 	const prefix = "#/components/requestBodies/"
-	c, cached, err := resolveComponent(p, componentResolve[*ogen.RequestBody, *openapi.RequestBody]{
+	key, c, cached, err := resolveComponent(p, componentResolve[*ogen.RequestBody, *openapi.RequestBody]{
 		prefix:     prefix,
 		components: p.spec.Components.RequestBodies,
 		cache:      p.refs.requestBodies,
@@ -145,15 +145,15 @@ func (p *parser) resolveRequestBody(ref string, ctx *jsonpointer.ResolveCtx) (*o
 	if err != nil {
 		return nil, err
 	}
-	if !cached {
-		c.Ref = ref
+	if !cached && c.Ref.IsZero() {
+		c.Ref = key
 	}
 	return c, nil
 }
 
 func (p *parser) resolveResponse(ref string, ctx *jsonpointer.ResolveCtx) (*openapi.Response, error) {
 	const prefix = "#/components/responses/"
-	c, cached, err := resolveComponent(p, componentResolve[*ogen.Response, *openapi.Response]{
+	key, c, cached, err := resolveComponent(p, componentResolve[*ogen.Response, *openapi.Response]{
 		prefix:     prefix,
 		components: p.spec.Components.Responses,
 		cache:      p.refs.responses,
@@ -162,15 +162,15 @@ func (p *parser) resolveResponse(ref string, ctx *jsonpointer.ResolveCtx) (*open
 	if err != nil {
 		return nil, err
 	}
-	if !cached {
-		c.Ref = ref
+	if !cached && c.Ref.IsZero() {
+		c.Ref = key
 	}
 	return c, nil
 }
 
 func (p *parser) resolveParameter(ref string, ctx *jsonpointer.ResolveCtx) (*openapi.Parameter, error) {
 	const prefix = "#/components/parameters/"
-	c, cached, err := resolveComponent(p, componentResolve[*ogen.Parameter, *openapi.Parameter]{
+	key, c, cached, err := resolveComponent(p, componentResolve[*ogen.Parameter, *openapi.Parameter]{
 		prefix:     prefix,
 		components: p.spec.Components.Parameters,
 		cache:      p.refs.parameters,
@@ -179,15 +179,15 @@ func (p *parser) resolveParameter(ref string, ctx *jsonpointer.ResolveCtx) (*ope
 	if err != nil {
 		return nil, err
 	}
-	if !cached {
-		c.Ref = ref
+	if !cached && c.Ref.IsZero() {
+		c.Ref = key
 	}
 	return c, nil
 }
 
 func (p *parser) resolveHeader(headerName, ref string, ctx *jsonpointer.ResolveCtx) (*openapi.Header, error) {
 	const prefix = "#/components/headers/"
-	c, cached, err := resolveComponent(p, componentResolve[*ogen.Header, *openapi.Header]{
+	key, c, cached, err := resolveComponent(p, componentResolve[*ogen.Header, *openapi.Header]{
 		prefix:     prefix,
 		components: p.spec.Components.Headers,
 		cache:      p.refs.headers,
@@ -198,15 +198,15 @@ func (p *parser) resolveHeader(headerName, ref string, ctx *jsonpointer.ResolveC
 	if err != nil {
 		return nil, err
 	}
-	if !cached {
-		c.Ref = ref
+	if !cached && c.Ref.IsZero() {
+		c.Ref = key
 	}
 	return c, nil
 }
 
 func (p *parser) resolveExample(ref string, ctx *jsonpointer.ResolveCtx) (*openapi.Example, error) {
 	const prefix = "#/components/examples/"
-	c, cached, err := resolveComponent(p, componentResolve[*ogen.Example, *openapi.Example]{
+	key, c, cached, err := resolveComponent(p, componentResolve[*ogen.Example, *openapi.Example]{
 		prefix:     prefix,
 		components: p.spec.Components.Examples,
 		cache:      p.refs.examples,
@@ -215,15 +215,15 @@ func (p *parser) resolveExample(ref string, ctx *jsonpointer.ResolveCtx) (*opena
 	if err != nil {
 		return nil, err
 	}
-	if !cached && c != nil {
-		c.Ref = ref
+	if !cached && c != nil && c.Ref.IsZero() {
+		c.Ref = key
 	}
 	return c, nil
 }
 
 func (p *parser) resolveSecurityScheme(ref string, ctx *jsonpointer.ResolveCtx) (*ogen.SecurityScheme, error) {
 	const prefix = "#/components/securitySchemes/"
-	c, _, err := resolveComponent(p, componentResolve[*ogen.SecurityScheme, *ogen.SecurityScheme]{
+	_, c, _, err := resolveComponent(p, componentResolve[*ogen.SecurityScheme, *ogen.SecurityScheme]{
 		prefix:     prefix,
 		components: p.spec.Components.SecuritySchemes,
 		cache:      p.refs.securitySchemes,
@@ -240,7 +240,7 @@ func (p *parser) resolvePathItem(
 	ctx *jsonpointer.ResolveCtx,
 ) (pathItem, error) {
 	const prefix = "#/components/pathItems/"
-	c, _, err := resolveComponent(p, componentResolve[*ogen.PathItem, pathItem]{
+	_, c, _, err := resolveComponent(p, componentResolve[*ogen.PathItem, pathItem]{
 		prefix:     prefix,
 		components: p.spec.Components.PathItems,
 		cache:      p.refs.pathItems,
diff --git a/openapi/parser/settings.go b/openapi/parser/settings.go
index 0bf606f0..c744bd41 100644
--- a/openapi/parser/settings.go
+++ b/openapi/parser/settings.go
@@ -19,6 +19,8 @@ type Settings struct {
 	File location.File
 
 	// RootURL is the root URL of the spec.
+	//
+	// If nil, jsonpointer.DummyURL is used.
 	RootURL *url.URL
 
 	// DepthLimit limits the number of nested references. Default is 1000.
@@ -45,4 +47,7 @@ func (s *Settings) setDefaults() {
 	if s.DepthLimit == 0 {
 		s.DepthLimit = jsonpointer.DefaultDepthLimit
 	}
+	if s.RootURL == nil {
+		s.RootURL = jsonpointer.DummyURL()
+	}
 }


Test patch:
diff --git a/_testdata/positive/file_reference.yml b/_testdata/positive/file_reference.yml
new file mode 100644
index 00000000..dbd75443
--- /dev/null
+++ b/_testdata/positive/file_reference.yml
@@ -0,0 +1,21 @@
+openapi: 3.0.3
+info:
+  title: test
+  version: 0.0.1
+paths:
+  /internal:
+    get:
+      operationId: internal
+      responses:
+        "200":
+          description: ok
+          content:
+            application/json:
+              schema:
+                $ref: '#/components/schemas/TestSchema'
+  /external:
+    $ref: './file_reference_external/external.yml'
+components:
+  schemas:
+    TestSchema:
+      type: string
\ No newline at end of file
diff --git a/_testdata/positive/file_reference_external/external.yml b/_testdata/positive/file_reference_external/external.yml
new file mode 100644
index 00000000..d320de42
--- /dev/null
+++ b/_testdata/positive/file_reference_external/external.yml
@@ -0,0 +1,9 @@
+get:
+  operationId: external
+  responses:
+    "200":
+      description: ok
+      content:
+        application/json:
+          schema:
+            $ref: '../file_reference.yml#/components/schemas/TestSchema'
diff --git a/gen/gen_schema_test.go b/gen/gen_schema_test.go
index 587d2a13..2b10c0a7 100644
--- a/gen/gen_schema_test.go
+++ b/gen/gen_schema_test.go
@@ -3,6 +3,7 @@ package gen
 import (
 	"embed"
 	"io/fs"
+	"net/url"
 	"path/filepath"
 	"strings"
 	"testing"
@@ -13,6 +14,7 @@ import (
 	"go.uber.org/zap/zaptest"
 
 	"github.com/ogen-go/ogen/gen/genfs"
+	"github.com/ogen-go/ogen/internal/jsonpointer"
 	"github.com/ogen-go/ogen/jsonschema"
 )
 
@@ -45,7 +47,8 @@ func TestGenerateSchema(t *testing.T) {
 		if err := root.Decode(&rawSchema); err != nil {
 			return errors.Wrap(err, "unmarshal")
 		}
-		schema, err := p.Parse(&rawSchema)
+		ctx := jsonpointer.NewResolveCtx(&url.URL{Path: "/" + path}, jsonpointer.DefaultDepthLimit)
+		schema, err := p.Parse(&rawSchema, ctx)
 		if err != nil {
 			return errors.Wrap(err, "parse")
 		}
diff --git a/gen/names_test.go b/gen/names_test.go
index be2b4d5d..b6e74751 100644
--- a/gen/names_test.go
+++ b/gen/names_test.go
@@ -2,9 +2,12 @@ package gen
 
 import (
 	"fmt"
+	"net/url"
 	"testing"
 
 	"github.com/stretchr/testify/require"
+
+	"github.com/ogen-go/ogen/jsonschema"
 )
 
 func TestNames(t *testing.T) {
@@ -49,7 +52,13 @@ func Test_cleanRef(t *testing.T) {
 	for i, tt := range tests {
 		tt := tt
 		t.Run(fmt.Sprintf("Test%d", i), func(t *testing.T) {
-			require.Equal(t, tt.want, cleanRef(tt.ref))
+			u, err := url.Parse(tt.ref)
+			require.NoError(t, err)
+
+			var ref jsonschema.Ref
+			ref.FromURL(u)
+
+			require.Equal(t, tt.want, cleanRef(ref))
 		})
 	}
 }
diff --git a/gen/schema_gen_test.go b/gen/schema_gen_test.go
index f915b180..c5fa404b 100644
--- a/gen/schema_gen_test.go
+++ b/gen/schema_gen_test.go
@@ -15,7 +15,7 @@ func TestSchemaGenAnyWarn(t *testing.T) {
 	a := require.New(t)
 
 	core, ob := observer.New(zap.InfoLevel)
-	s := newSchemaGen(func(ref string) (*ir.Type, bool) {
+	s := newSchemaGen(func(ref jsonschema.Ref) (*ir.Type, bool) {
 		return nil, false
 	})
 	s.log = zap.New(core)
diff --git a/gen_test.go b/gen_test.go
index cf86dcd0..83eb69a2 100644
--- a/gen_test.go
+++ b/gen_test.go
@@ -1,6 +1,7 @@
 package ogen_test
 
 import (
+	"net/url"
 	"path"
 	"runtime/debug"
 	"strings"
@@ -17,7 +18,7 @@ import (
 	"github.com/ogen-go/ogen/openapi/parser"
 )
 
-func testGenerate(t *testing.T, filename string, data []byte, aliases ctAliases, ignore ...string) {
+func testGenerate(t *testing.T, dir, filename string, data []byte, aliases ctAliases, ignore ...string) {
 	t.Helper()
 	t.Parallel()
 	log := zaptest.NewLogger(t)
@@ -36,6 +37,28 @@ func testGenerate(t *testing.T, filename string, data []byte, aliases ctAliases,
 		File:               location.NewFile(filename, filename, data),
 		Logger:             log,
 	}
+	if filename == "file_reference.yml" { // HACK
+		opt.AllowRemote = true
+		opt.RootURL = &url.URL{
+			Scheme: "file",
+			Path:   "/" + path.Join(dir, filename),
+		}
+		opt.Remote = gen.RemoteOptions{
+			ReadFile: func(p string) ([]byte, error) {
+				p = strings.TrimPrefix(p, "/")
+				return testdata.ReadFile(p)
+			},
+			URLToFilePath: func(u *url.URL) (string, error) {
+				// By default, urlpath.URLToFilePath output depends on the OS.
+				//
+				// But we use virtual filesystem, so we should use the fs.FS path.
+				if u.Path == "" {
+					return u.Opaque, nil
+				}
+				return u.Path, nil
+			},
+		}
+	}
 	t.Run("Gen", func(t *testing.T) {
 		defer func() {
 			if rr := recover(); rr != nil {
@@ -77,9 +100,15 @@ func runPositive(root string,
 		}
 
 		walkTestdata(t, root, func(t *testing.T, file string, data []byte) {
+			dir := path.Dir(file)
+			if parent := path.Base(dir); parent == "file_reference_external" {
+				t.Skip("Special directory for testing remote references.")
+				return
+			}
+
 			file = strings.TrimPrefix(file, root+"/")
 			skip := skipSets[file]
-			testGenerate(t, file, data, aliases[file], skip...)
+			testGenerate(t, dir, file, data, aliases[file], skip...)
 		})
 	}
 }
diff --git a/jsonschema/jsonschema_test.go b/jsonschema/jsonschema_test.go
index 22efefa6..c4fc5104 100644
--- a/jsonschema/jsonschema_test.go
+++ b/jsonschema/jsonschema_test.go
@@ -2,6 +2,7 @@ package jsonschema_test
 
 import (
 	"embed"
+	"net/url"
 	"path"
 	"strings"
 	"testing"
@@ -9,6 +10,7 @@ import (
 	yaml "github.com/go-faster/yamlx"
 	"github.com/stretchr/testify/require"
 
+	"github.com/ogen-go/ogen/internal/jsonpointer"
 	"github.com/ogen-go/ogen/internal/location"
 	"github.com/ogen-go/ogen/internal/testutil"
 	"github.com/ogen-go/ogen/jsonschema"
@@ -34,7 +36,7 @@ func TestNegative(t *testing.T) {
 		p := jsonschema.NewParser(jsonschema.Settings{
 			File: location.NewFile(name, file, data),
 		})
-		_, err = p.Parse(&schema)
+		_, err = p.Parse(&schema, jsonpointer.NewResolveCtx(&url.URL{Path: "/" + file}, jsonpointer.DefaultDepthLimit))
 		a.Error(err)
 
 		var buf strings.Builder
diff --git a/jsonschema/parser_test.go b/jsonschema/parser_test.go
index 5418a84d..dc85b10a 100644
--- a/jsonschema/parser_test.go
+++ b/jsonschema/parser_test.go
@@ -2,6 +2,7 @@ package jsonschema
 
 import (
 	"fmt"
+	"net/url"
 	"strconv"
 	"strings"
 	"testing"
@@ -31,6 +32,10 @@ func (c components) ResolveReference(ref string) (*RawSchema, error) {
 	return s, nil
 }
 
+func testCtx() *jsonpointer.ResolveCtx {
+	return jsonpointer.NewResolveCtx(&url.URL{Path: "/root.json"}, jsonpointer.DefaultDepthLimit)
+}
+
 func TestSchemaSimple(t *testing.T) {
 	parser := NewParser(Settings{})
 
@@ -47,7 +52,7 @@ func TestSchemaSimple(t *testing.T) {
 			},
 		},
 		Required: []string{"id", "name"},
-	})
+	}, testCtx())
 	require.NoError(t, err)
 
 	expect := &Schema{
@@ -98,7 +103,7 @@ func TestSchemaRecursive(t *testing.T) {
 
 	pet := &Schema{
 		Type: Object,
-		Ref:  "#/components/schemas/Pet",
+		Ref:  Ref{Loc: "/root.json", Ptr: "#/components/schemas/Pet"},
 	}
 	pet.Properties = []Property{
 		{
@@ -122,9 +127,9 @@ func TestSchemaRecursive(t *testing.T) {
 	}
 
 	expectRefcache := map[jsonpointer.RefKey]*Schema{
-		{Ref: "#/components/schemas/Pet"}: {
+		{Loc: "/root.json", Ptr: "#/components/schemas/Pet"}: {
 			Type: Object,
-			Ref:  "#/components/schemas/Pet",
+			Ref:  Ref{Loc: "/root.json", Ptr: "#/components/schemas/Pet"},
 			Properties: []Property{
 				{
 					Name:     "id",
@@ -154,7 +159,7 @@ func TestSchemaRecursive(t *testing.T) {
 
 	out, err := parser.Parse(&RawSchema{
 		Ref: "#/components/schemas/Pet",
-	})
+	}, testCtx())
 	require.NoError(t, err)
 	require.Equal(t, expectRefcache, parser.refcache)
 	require.Equal(t, pet, out)
@@ -177,7 +182,7 @@ func TestSchemaInfiniteRecursion(t *testing.T) {
 		})
 		_, err := parser.Parse(&RawSchema{
 			Ref: "#/components/schemas/Type",
-		})
+		}, testCtx())
 		require.Error(t, err)
 	}
 }
@@ -213,7 +218,7 @@ func TestSchemaRefToRef(t *testing.T) {
 	})
 	_, err := parser.Parse(&RawSchema{
 		Ref: "#/components/schemas/referer",
-	})
+	}, testCtx())
 	require.NoError(t, err)
 }
 
@@ -289,7 +294,7 @@ func TestSchemaSideEffects(t *testing.T) {
 			},
 		},
 		Required: []string{"id", "name", "owner"},
-	})
+	}, testCtx())
 
 	require.NoError(t, err)
 	require.Equal(t, expect, out)
@@ -307,12 +312,12 @@ func TestSchemaReferencedArray(t *testing.T) {
 
 	pets := &Schema{
 		Type: Array,
-		Ref:  "#/components/schemas/Pets",
+		Ref:  Ref{Loc: "/root.json", Ptr: "#/components/schemas/Pets"},
 		Item: &Schema{Type: String},
 	}
 
 	expectRefcache := map[jsonpointer.RefKey]*Schema{
-		{Ref: "#/components/schemas/Pets"}: pets,
+		{Loc: "/root.json", Ptr: "#/components/schemas/Pets"}: pets,
 	}
 
 	expect := &Schema{
@@ -341,7 +346,7 @@ func TestSchemaReferencedArray(t *testing.T) {
 			},
 		},
 		Required: []string{"pets"},
-	})
+	}, testCtx())
 
 	require.NoError(t, err)
 	require.Equal(t, expectRefcache, parser.refcache)
@@ -377,7 +382,7 @@ func TestSchemaExtensions(t *testing.T) {
 			const filename = "test.yaml"
 			out, err := NewParser(Settings{
 				File: location.NewFile(filename, filename, data),
-			}).Parse(&raw)
+			}).Parse(&raw, testCtx())
 			if tt.expectErr {
 				a.Error(err)
 				return
@@ -405,14 +410,14 @@ func TestInvalidMultipleOf(t *testing.T) {
 				_, err := parser.Parse(&RawSchema{
 					Type:       typ,
 					MultipleOf: strconv.AppendInt(nil, int64(v), 10),
-				})
+				}, testCtx())
 				require.Errorf(t, err, "%d", v)
 			}
 		})
 		_, err := parser.Parse(&RawSchema{
 			Type:       typ,
 			MultipleOf: []byte("true"),
-		})
+		}, testCtx())
 		require.Error(t, err)
 	}
 }
diff --git a/jsonschema/resolve_test.go b/jsonschema/resolve_test.go
index 7d48a477..976b1df6 100644
--- a/jsonschema/resolve_test.go
+++ b/jsonschema/resolve_test.go
@@ -3,6 +3,7 @@ package jsonschema
 import (
 	"context"
 	"encoding/json"
+	"net/url"
 	"strings"
 	"testing"
 
@@ -98,26 +99,26 @@ func TestExternalReference(t *testing.T) {
 		Items: &RawSchema{
 			Ref: "#/components/schemas/LocalSchema",
 		},
-	})
+	}, testCtx())
 	require.NoError(t, err)
 
 	expect := &Schema{
 		Type: Array,
 		Item: &Schema{
-			Ref:  "foo.json#/components/schemas/RemoteSchema",
+			Ref:  Ref{Loc: "/foo.json", Ptr: "#/components/schemas/RemoteSchema"},
 			Type: Object,
 			Properties: []Property{
 				{
 					Name:   "relative",
-					Schema: &Schema{Ref: "#/components/schemas/Property", Type: Number},
+					Schema: &Schema{Ref: Ref{Loc: "/foo.json", Ptr: "#/components/schemas/Property"}, Type: Number},
 				},
 				{
 					Name:   "absolute",
-					Schema: &Schema{Ref: "#/components/schemas/Property", Type: Number},
+					Schema: &Schema{Ref: Ref{Loc: "/foo.json", Ptr: "#/components/schemas/Property"}, Type: Number},
 				},
 				{
 					Name:   "remote_absolute",
-					Schema: &Schema{Ref: "https://example.com/bar.json#/components/schemas/Property", Type: Boolean},
+					Schema: &Schema{Ref: Ref{Loc: "https://example.com/bar.json", Ptr: "#/components/schemas/Property"}, Type: Boolean},
 				},
 			},
 		},
@@ -177,7 +178,8 @@ func TestLimitDepth(t *testing.T) {
 		parser := NewParser(Settings{
 			Resolver: root,
 		})
-		_, err := parser.Resolve("#/components/schemas/Schema1", jsonpointer.NewResolveCtx(nil, tt.limit))
+		ctx := jsonpointer.NewResolveCtx(&url.URL{Path: "/limit.json"}, tt.limit)
+		_, err := parser.Resolve("#/components/schemas/Schema1", ctx)
 		tt.checker(t, err, "limit: %d", tt.limit)
 	}
 }
diff --git a/openapi/parser/errors_test.go b/openapi/parser/errors_test.go
index 23134950..16e32acd 100644
--- a/openapi/parser/errors_test.go
+++ b/openapi/parser/errors_test.go
@@ -42,8 +42,10 @@ func TestRemoteLocation(t *testing.T) {
 	_, err := Parse(root, Settings{
 		External: remote,
 		File:     location.NewFile("root.json", "root.json", nil),
+		RootURL:  testRootURL,
 	})
 	a.Error(err)
+	a.ErrorAs(err, new(*location.Error))
 	var (
 		iterErr = err
 		locErr  *LocationError
@@ -56,5 +58,5 @@ func TestRemoteLocation(t *testing.T) {
 	}
 	t.Log(locErr)
 	a.NotNil(locErr)
-	a.Equal("foo.json", locErr.File.Name)
+	a.Equal("/foo.json", locErr.File.Name)
 }
diff --git a/openapi/parser/parse_schema_test.go b/openapi/parser/parse_schema_test.go
index c93c439e..881b6d13 100644
--- a/openapi/parser/parse_schema_test.go
+++ b/openapi/parser/parse_schema_test.go
@@ -116,20 +116,25 @@ func TestParseDiscriminator(t *testing.T) {
 
 	a := require.New(t)
 
-	spec, err := Parse(root, Settings{})
+	ref := func(ptr string) refKey {
+		return refKey{Loc: testRootURL.String(), Ptr: ptr}
+	}
+	spec, err := Parse(root, Settings{
+		RootURL: testRootURL,
+	})
 	a.NoError(err)
 	{
 		s := spec.Components.Schemas["PetRef"]
 		m := s.Discriminator.Mapping
-		a.Equal(m["cat"].Ref, "#/components/schemas/Cat")
-		a.Equal(m["dog"].Ref, "#/components/schemas/Dog")
-		a.Equal(m["cow"].Ref, "#/components/schemas/Cow")
+		a.Equal(m["cat"].Ref, ref("#/components/schemas/Cat"))
+		a.Equal(m["dog"].Ref, ref("#/components/schemas/Dog"))
+		a.Equal(m["cow"].Ref, ref("#/components/schemas/Cow"))
 	}
 	{
 		s := spec.Components.Schemas["PetSchemaName"]
 		m := s.Discriminator.Mapping
-		a.Equal(m["cat"].Ref, "#/components/schemas/Cat")
-		a.Equal(m["dog"].Ref, "#/components/schemas/Dog")
-		a.Equal(m["cow"].Ref, "#/components/schemas/Cow")
+		a.Equal(m["cat"].Ref, ref("#/components/schemas/Cat"))
+		a.Equal(m["dog"].Ref, ref("#/components/schemas/Dog"))
+		a.Equal(m["cow"].Ref, ref("#/components/schemas/Cow"))
 	}
 }
diff --git a/openapi/parser/resolve_external_test.go b/openapi/parser/resolve_external_test.go
index 2bd9ed33..e28fb9a6 100644
--- a/openapi/parser/resolve_external_test.go
+++ b/openapi/parser/resolve_external_test.go
@@ -175,7 +175,7 @@ func TestExternalReference(t *testing.T) {
 			Components: &ogen.Components{
 				Schemas: map[string]*ogen.Schema{
 					"Schema": {
-						Ref: "root.json#",
+						Ref: "schema_file.json#",
 					},
 				},
 			},
@@ -200,27 +200,29 @@ func TestExternalReference(t *testing.T) {
 				},
 			},
 		},
-		"root.json": ogen.Schema{Type: "string"},
+		"schema_file.json": ogen.Schema{Type: "string"},
 	}
 
 	a := require.New(t)
 	spec, err := Parse(root, Settings{
 		External: remote,
+		File:     location.NewFile("root.json", "root.json", nil),
+		RootURL:  testRootURL,
 	})
 	a.NoError(err)
 
 	var (
 		schema = &jsonschema.Schema{
-			Ref:      "root.json#",
+			Ref:      refKey{Loc: "/schema_file.json", Ptr: "#"},
 			Type:     "string",
 			Examples: []jsonschema.Example{exampleValue},
 		}
 		localExample = &openapi.Example{
-			Ref:   "foo.json#/components/examples/RemoteExample",
+			Ref:   refKey{Loc: "/foo.json", Ptr: "#/components/examples/RemoteExample"},
 			Value: exampleValue,
 		}
 		param = &openapi.Parameter{
-			Ref:     "#/components/parameters/LocalParameter",
+			Ref:     refKey{Loc: "/foo.json", Ptr: "#/components/parameters/RemoteParameter"},
 			Name:    "parameter",
 			Schema:  schema,
 			In:      "query",
@@ -228,14 +230,14 @@ func TestExternalReference(t *testing.T) {
 			Explode: true,
 		}
 		requestBody = &openapi.RequestBody{
-			Ref:         "#/components/requestBodies/LocalRequestBody",
+			Ref:         refKey{Loc: "/foo.json", Ptr: "#/components/requestBodies/RemoteRequestBody"},
 			Description: "request description",
 			Content: map[string]*openapi.MediaType{
 				"application/json": {
 					Schema: schema,
 					Examples: map[string]*openapi.Example{
 						"ref": {
-							Ref:   "foo.json#/components/examples/RemoteExample",
+							Ref:   refKey{Loc: "/foo.json", Ptr: "#/components/examples/RemoteExample"},
 							Value: exampleValue,
 						},
 					},
@@ -244,11 +246,11 @@ func TestExternalReference(t *testing.T) {
 			},
 		}
 		response = &openapi.Response{
-			Ref:         "#/components/responses/LocalResponse",
+			Ref:         refKey{Loc: "/response.json", Ptr: "#"},
 			Description: "response description",
 			Headers: map[string]*openapi.Header{
 				"ResponseHeader": {
-					Ref:    "foo.json#/components/headers/RemoteHeader",
+					Ref:    refKey{Loc: "/foo.json", Ptr: "#/components/headers/RemoteHeader"},
 					Name:   "ResponseHeader",
 					In:     openapi.LocationHeader,
 					Style:  openapi.HeaderStyleSimple,
@@ -313,7 +315,7 @@ func TestExternalReference(t *testing.T) {
 			},
 			Examples: map[string]*openapi.Example{
 				"LocalExample": {
-					Ref:   "#/components/examples/LocalExample",
+					Ref:   refKey{Loc: "/foo.json", Ptr: "#/components/examples/RemoteExample"},
 					Value: exampleValue,
 				},
 			},
@@ -363,12 +365,14 @@ func TestDuplicateOperationID(t *testing.T) {
 	a := require.New(t)
 	_, err := Parse(root, Settings{
 		External: remote,
+		File:     location.NewFile("root.json", "root.json", nil),
+		RootURL:  testRootURL,
 	})
 	a.ErrorContains(err, "duplicate operationId: \"testGet\"")
 	// Ensure that the error contains the file name.
 	var locErr *location.Error
 	a.ErrorAs(err, &locErr)
-	a.Equal("pathItem.json", locErr.File.Name)
+	a.Equal("/pathItem.json", locErr.File.Name)
 }
 
 // Ensure that parser adds location information to the error, even if the error is occurred in the external file.
@@ -395,12 +399,14 @@ func TestExternalErrors(t *testing.T) {
 	a := require.New(t)
 	_, err := Parse(root, Settings{
 		External: remote,
+		File:     location.NewFile("root.json", "root.json", nil),
+		RootURL:  testRootURL,
 	})
 	a.ErrorContains(err, "parse status code")
 
 	var locErr *location.Error
 	a.ErrorAs(err, &locErr)
-	a.Equal("pathItem.json", locErr.File.Name)
+	a.Equal("/pathItem.json", locErr.File.Name)
 	a.True(locErr.PrettyPrint(io.Discard, false))
 }
 
diff --git a/openapi/parser/resolve_test.go b/openapi/parser/resolve_test.go
index 6b98cf4f..11248311 100644
--- a/openapi/parser/resolve_test.go
+++ b/openapi/parser/resolve_test.go
@@ -3,6 +3,7 @@ package parser
 import (
 	"encoding/json"
 	"fmt"
+	"net/url"
 	"testing"
 
 	yaml "github.com/go-faster/yamlx"
@@ -14,6 +15,10 @@ import (
 	"github.com/ogen-go/ogen/openapi"
 )
 
+var testRootURL = &url.URL{
+	Path: "/root.json",
+}
+
 func compareJSON(t require.TestingT, expected, got any) {
 	// Compare as JSON because we can't skip locators.
 	encode := func(s any) string {
@@ -110,7 +115,9 @@ func TestComplicatedReference(t *testing.T) {
 	a.NoError(raw.Encode(root))
 	root.Raw = &raw
 
-	spec, err := Parse(root, Settings{})
+	spec, err := Parse(root, Settings{
+		RootURL: testRootURL,
+	})
 	a.NoError(err)
 
 	var (
@@ -129,7 +136,7 @@ func TestComplicatedReference(t *testing.T) {
 			},
 			Parameters: []*openapi.Parameter{
 				{
-					Ref:  "#/paths/~1post/post/parameters/0",
+					Ref:  refKey{Loc: "/root.json", Ptr: "#/paths/~1post/post/parameters/0"},
 					Name: "param",
 					Schema: &jsonschema.Schema{
 						Type: "string",
@@ -140,7 +147,7 @@ func TestComplicatedReference(t *testing.T) {
 				},
 			},
 			RequestBody: &openapi.RequestBody{
-				Ref: "#/paths/~1post/post/requestBody",
+				Ref: refKey{Loc: "/root.json", Ptr: "#/paths/~1post/post/requestBody"},
 				Content: map[string]*openapi.MediaType{
 					"application/json": {
 						Schema: &jsonschema.Schema{
@@ -154,14 +161,14 @@ func TestComplicatedReference(t *testing.T) {
 			Security: []openapi.SecurityRequirement{},
 			Responses: map[string]*openapi.Response{
 				"200": {
-					Ref: "#/paths/~1post/post/responses/200",
+					Ref: refKey{Loc: "/root.json", Ptr: "#/paths/~1post/post/responses/200"},
 					Headers: map[string]*openapi.Header{
 						"ResponseHeader": responseHeader,
 					},
 					Content: map[string]*openapi.MediaType{
 						"application/json": {
 							Schema: &jsonschema.Schema{
-								Ref:  "#/paths/~1post/post/requestBody/content/application~1json/schema",
+								Ref:  refKey{Loc: "/root.json", Ptr: "#/paths/~1post/post/requestBody/content/application~1json/schema"},
 								Type: "string",
 							},
 							Examples: map[string]*openapi.Example{},
@@ -221,7 +228,7 @@ func TestComplicatedReference(t *testing.T) {
 					Content: map[string]*openapi.MediaType{
 						"application/json": {
 							Schema: &jsonschema.Schema{
-								Ref:  "#/paths/~1post/post/requestBody/content/application~1json/schema",
+								Ref:  refKey{Loc: "/root.json", Ptr: "#/paths/~1post/post/requestBody/content/application~1json/schema"},
 								Type: "string",
 							},
 							Examples: map[string]*openapi.Example{},