Repository: ogen-go/ogen
Pull request #548 (2022-09-01 06:17:00)

fix(jsonschema): resolve references against base


Patch:
diff --git a/cmd/ogen/main.go b/cmd/ogen/main.go
index 1375e98b..544c2bdb 100644
--- a/cmd/ogen/main.go
+++ b/cmd/ogen/main.go
@@ -189,7 +189,7 @@ func run() error {
 		_ = logger.Sync()
 	}()
 
-	_, fileName := filepath.Split(specPath)
+	specDir, fileName := filepath.Split(specPath)
 	opts := gen.Options{
 		NoClient:             *noClient,
 		NoServer:             *noServer,
@@ -198,6 +198,11 @@ func run() error {
 		SkipUnimplemented:    *skipUnimplemented,
 		InferSchemaType:      *inferTypes,
 		AllowRemote:          *allowRemote,
+		Remote: gen.RemoteOptions{
+			ReadFile: func(p string) ([]byte, error) {
+				return os.ReadFile(filepath.Join(specDir, p))
+			},
+		},
 		Filters: gen.Filters{
 			PathRegex: filterPath,
 			Methods:   filterMethods,
diff --git a/internal/jsonpointer/resolve_ctx.go b/internal/jsonpointer/resolve_ctx.go
new file mode 100644
index 00000000..b77c0ad2
--- /dev/null
+++ b/internal/jsonpointer/resolve_ctx.go
@@ -0,0 +1,108 @@
+package jsonpointer
+
+import (
+	"net/url"
+
+	"github.com/go-faster/errors"
+)
+
+// RefKey is JSON reference key.
+type RefKey struct {
+	Loc string
+	Ref string
+}
+
+// FromURL sets RefKey from URL.
+func (r *RefKey) FromURL(u *url.URL) {
+	{
+		// Make copy.
+		u2 := *u
+		u2.Fragment = ""
+		r.Loc = u2.String()
+	}
+	r.Ref = "#" + u.Fragment
+}
+
+// ResolveCtx is JSON pointer resolve context.
+type ResolveCtx struct {
+	// Location stack. Used for context-depending resolving.
+	//
+	// For resolve trace like
+	//
+	// 	"#/components/schemas/Schema" ->
+	// 	"https://example.com/schema#Schema" ->
+	//	"#/definitions/SchemaProperty"
+	//
+	// "#/definitions/SchemaProperty" should be resolved against "https://example.com/schema".
+	locstack []string
+	// Store references to detect infinite recursive references.
+	refs       map[RefKey]struct{}
+	depthLimit int
+}
+
+// DefaultDepthLimit is default depth limit for ResolveCtx.
+const DefaultDepthLimit = 1000
+
+// DefaultCtx creates new ResolveCtx with default depth limit.
+func DefaultCtx() *ResolveCtx {
+	return NewResolveCtx(DefaultDepthLimit)
+}
+
+// NewResolveCtx creates new ResolveCtx.
+func NewResolveCtx(depthLimit int) *ResolveCtx {
+	return &ResolveCtx{
+		locstack:   nil,
+		refs:       map[RefKey]struct{}{},
+		depthLimit: depthLimit,
+	}
+}
+
+// Add adds reference to context and returns key.
+func (r *ResolveCtx) Add(ref string) (key RefKey, _ error) {
+	parser := url.Parse
+	if loc := r.LastLoc(); loc != "" {
+		base, err := url.Parse(loc)
+		if err != nil {
+			return key, err
+		}
+		parser = base.Parse
+	}
+
+	u, err := parser(ref)
+	if err != nil {
+		return RefKey{}, err
+	}
+	key.FromURL(u)
+
+	if r.depthLimit <= 0 {
+		return RefKey{}, errors.New("depth limit exceeded")
+	}
+	if _, ok := r.refs[key]; ok {
+		return key, errors.New("infinite recursion")
+	}
+	r.refs[key] = struct{}{}
+	r.depthLimit--
+
+	if key.Loc != "" {
+		r.locstack = append(r.locstack, key.Loc)
+	}
+	return key, nil
+}
+
+// Delete removes reference from context.
+func (r *ResolveCtx) Delete(key RefKey) {
+	r.depthLimit++
+	delete(r.refs, key)
+	if key.Loc != "" && len(r.locstack) > 0 {
+		r.locstack = r.locstack[:len(r.locstack)-1]
+	}
+}
+
+// LastLoc returns last location from stack.
+func (r *ResolveCtx) LastLoc() string {
+	s := r.locstack
+	if len(s) == 0 {
+		return ""
+	}
+	return s[len(s)-1]
+}
diff --git a/jsonschema/parser.go b/jsonschema/parser.go
index 7c12036f..dadcd6d5 100644
--- a/jsonschema/parser.go
+++ b/jsonschema/parser.go
@@ -9,6 +9,7 @@ import (
 	"github.com/go-faster/errors"
 	"github.com/go-faster/jx"
 
+	"github.com/ogen-go/ogen/internal/jsonpointer"
 	"github.com/ogen-go/ogen/internal/location"
 	ogenjson "github.com/ogen-go/ogen/json"
 )
@@ -17,10 +18,9 @@ import (
 type Parser struct {
 	external ExternalResolver
 	schemas  map[string]ReferenceResolver
-	refcache map[refKey]*Schema
+	refcache map[string]*Schema
 
-	depthLimit int
-	filename   string // optional, used for error messages
+	filename string // optional, used for error messages
 
 	inferTypes bool
 }
@@ -33,8 +33,7 @@ func NewParser(s Settings) *Parser {
 		schemas: map[string]ReferenceResolver{
 			"": s.Resolver,
 		},
-		refcache:   map[refKey]*Schema{},
-		depthLimit: s.DepthLimit,
+		refcache:   map[string]*Schema{},
 		filename:   s.Filename,
 		inferTypes: s.InferTypes,
 	}
@@ -42,18 +41,23 @@ func NewParser(s Settings) *Parser {
 
 // Parse parses given RawSchema and returns parsed Schema.
 func (p *Parser) Parse(schema *RawSchema) (*Schema, error) {
-	return p.parse(schema, newResolveCtx(p.depthLimit))
+	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) {
+	return p.parse(schema, ctx)
 }
 
 // Resolve resolves Schema by given ref.
-func (p *Parser) Resolve(ref string) (*Schema, error) {
-	return p.resolve(ref, newResolveCtx(p.depthLimit))
+func (p *Parser) Resolve(ref string, ctx *jsonpointer.ResolveCtx) (*Schema, error) {
+	return p.resolve(ref, ctx)
 }
 
-func (p *Parser) parse(schema *RawSchema, ctx *resolveCtx) (_ *Schema, rerr error) {
+func (p *Parser) parse(schema *RawSchema, ctx *jsonpointer.ResolveCtx) (_ *Schema, rerr error) {
 	if schema != nil {
 		defer func() {
-			rerr = p.wrapLocation(ctx.lastLoc(), schema.Locator, rerr)
+			rerr = p.wrapLocation(ctx.LastLoc(), schema.Locator, rerr)
 		}()
 	}
 	return p.parse1(schema, ctx, func(s *Schema) *Schema {
@@ -61,7 +65,7 @@ func (p *Parser) parse(schema *RawSchema, ctx *resolveCtx) (_ *Schema, rerr erro
 	})
 }
 
-func (p *Parser) parse1(schema *RawSchema, ctx *resolveCtx, hook func(*Schema) *Schema) (*Schema, error) {
+func (p *Parser) parse1(schema *RawSchema, ctx *jsonpointer.ResolveCtx, hook func(*Schema) *Schema) (*Schema, error) {
 	s, err := p.parseSchema(schema, ctx, hook)
 	if err != nil {
 		return nil, errors.Wrap(err, "parse schema")
@@ -79,7 +83,7 @@ func (p *Parser) parse1(schema *RawSchema, ctx *resolveCtx, hook func(*Schema) *
 					if ok, _ := ogenjson.Equal(a, b); ok {
 						loc := loc.Index(i)
 						err := errors.Errorf("duplicate enum value: %q", a)
-						return nil, p.wrapLocation(ctx.lastLoc(), loc, err)
+						return nil, p.wrapLocation(ctx.LastLoc(), loc, err)
 					}
 				}
 			}
@@ -87,7 +91,7 @@ func (p *Parser) parse1(schema *RawSchema, ctx *resolveCtx, hook func(*Schema) *
 			values, err := parseEnumValues(s, enum)
 			if err != nil {
 				err := errors.Wrap(err, "parse enum values")
-				return nil, p.wrapLocation(ctx.lastLoc(), loc, err)
+				return nil, p.wrapLocation(ctx.LastLoc(), loc, err)
 			}
 			s.Enum = values
 			handleNullableEnum(s)
@@ -108,7 +112,7 @@ func (p *Parser) parse1(schema *RawSchema, ctx *resolveCtx, hook func(*Schema) *
 				return nil
 			}(); err != nil {
 				err := errors.Wrap(err, "parse default")
-				return nil, p.wrapField("default", ctx.lastLoc(), schema.Locator, err)
+				return nil, p.wrapField("default", ctx.LastLoc(), schema.Locator, err)
 			}
 		}
 		if a, ok := schema.XAnnotations["x-ogen-name"]; ok {
@@ -123,12 +127,12 @@ func (p *Parser) parse1(schema *RawSchema, ctx *resolveCtx, hook func(*Schema) *
 	return s, nil
 }
 
-func (p *Parser) parseSchema(schema *RawSchema, ctx *resolveCtx, hook func(*Schema) *Schema) (*Schema, error) {
+func (p *Parser) parseSchema(schema *RawSchema, ctx *jsonpointer.ResolveCtx, hook func(*Schema) *Schema) (*Schema, error) {
 	if schema == nil {
 		return nil, nil
 	}
 	wrapField := func(field string, err error) error {
-		return p.wrapField(field, ctx.lastLoc(), schema.Locator, err)
+		return p.wrapField(field, ctx.LastLoc(), schema.Locator, err)
 	}
 
 	validateMinMax := func(prop string, min, max *uint64) (rerr error) {
@@ -307,12 +311,12 @@ func (p *Parser) parseSchema(schema *RawSchema, ctx *resolveCtx, hook func(*Sche
 				if err != nil {
 					loc := ppLoc.Key(pattern)
 					err := errors.Wrapf(err, "compile pattern %q", pattern)
-					return nil, p.wrapLocation(ctx.lastLoc(), loc, err)
+					return nil, p.wrapLocation(ctx.LastLoc(), loc, err)
 				}
 				sch, err := p.parse(prop.Schema, ctx)
 				if err != nil {
 					err := errors.Wrapf(err, "pattern schema %q", pattern)
-					return nil, p.wrapField(pattern, ctx.lastLoc(), ppLoc, err)
+					return nil, p.wrapField(pattern, ctx.LastLoc(), ppLoc, err)
 				}
 				patterns[idx] = PatternProperty{
 					Pattern: r,
@@ -327,7 +331,7 @@ func (p *Parser) parseSchema(schema *RawSchema, ctx *resolveCtx, hook func(*Sche
 			prop, err := p.parse(propSpec.Schema, ctx)
 			if err != nil {
 				err := errors.Wrapf(err, "property %q", propSpec.Name)
-				return nil, p.wrapField(propSpec.Name, ctx.lastLoc(), propsLoc, err)
+				return nil, p.wrapField(propSpec.Name, ctx.LastLoc(), propsLoc, err)
 			}
 
 			var description string
@@ -444,12 +448,12 @@ func (p *Parser) parseSchema(schema *RawSchema, ctx *resolveCtx, hook func(*Sche
 	}
 }
 
-func (p *Parser) parseMany(schemas []*RawSchema, loc location.Locator, ctx *resolveCtx) ([]*Schema, error) {
+func (p *Parser) parseMany(schemas []*RawSchema, loc location.Locator, ctx *jsonpointer.ResolveCtx) ([]*Schema, error) {
 	result := make([]*Schema, 0, len(schemas))
 	for i, schema := range schemas {
 		s, err := p.parse(schema, ctx)
 		if err != nil {
-			return nil, p.wrapLocation(ctx.lastLoc(), loc.Index(i), err)
+			return nil, p.wrapLocation(ctx.LastLoc(), loc.Index(i), err)
 		}
 		result = append(result, s)
 	}
diff --git a/jsonschema/parser_settings.go b/jsonschema/parser_settings.go
index 74ea7c1f..4676a37a 100644
--- a/jsonschema/parser_settings.go
+++ b/jsonschema/parser_settings.go
@@ -15,9 +15,6 @@ type Settings struct {
 	// Used for error messages.
 	Filename string
 
-	// DepthLimit limits the number of nested references. Default is 1000.
-	DepthLimit int
-
 	// Enables type inference.
 	//
 	// For example:
@@ -45,7 +42,4 @@ func (s *Settings) setDefaults() {
 	if s.Resolver == nil {
 		s.Resolver = nopResolver{}
 	}
-	if s.DepthLimit == 0 {
-		s.DepthLimit = 1000
-	}
 }
diff --git a/jsonschema/resolve.go b/jsonschema/resolve.go
index 3dca0c5e..74f5db34 100644
--- a/jsonschema/resolve.go
+++ b/jsonschema/resolve.go
@@ -2,10 +2,11 @@ package jsonschema
 
 import (
 	"context"
-	"net/url"
 
 	"github.com/go-faster/errors"
 	yaml "github.com/go-faster/yamlx"
+
+	"github.com/ogen-go/ogen/internal/jsonpointer"
 )
 
 // ReferenceResolver resolves JSON schema references.
@@ -13,78 +14,8 @@ type ReferenceResolver interface {
 	ResolveReference(ref string) (*RawSchema, error)
 }
 
-type refKey struct {
-	loc string
-	ref string
-}
-
-func (r *refKey) fromURL(u *url.URL) {
-	{
-		// Make copy.
-		u2 := *u
-		u2.Fragment = ""
-		r.loc = u2.String()
-	}
-	r.ref = "#" + u.Fragment
-}
-
-type resolveCtx struct {
-	// Location stack. Used for context-depending resolving.
-	//
-	// For resolve trace like
-	//
-	// 	"#/components/schemas/Schema" ->
-	// 	"https://example.com/schema#Schema" ->
-	//	"#/definitions/SchemaProperty"
-	//
-	// "#/definitions/SchemaProperty" should be resolved against "https://example.com/schema".
-	locstack []string
-	// Store references to detect infinite recursive references.
-	refs       map[refKey]struct{}
-	depthLimit int
-}
-
-func newResolveCtx(depthLimit int) *resolveCtx {
-	return &resolveCtx{
-		refs:       map[refKey]struct{}{},
-		depthLimit: depthLimit,
-	}
-}
-
-func (r *resolveCtx) add(key refKey) error {
-	if r.depthLimit <= 0 {
-		return errors.New("reference depth limit exceeded")
-	}
-	if _, ok := r.refs[key]; ok {
-		return errors.New("infinite recursion")
-	}
-	r.refs[key] = struct{}{}
-	r.depthLimit--
-
-	if key.loc != "" {
-		r.locstack = append(r.locstack, key.loc)
-	}
-	return nil
-}
-
-func (r *resolveCtx) delete(key refKey) {
-	r.depthLimit++
-	delete(r.refs, key)
-	if key.loc != "" && len(r.locstack) > 0 {
-		r.locstack = r.locstack[:len(r.locstack)-1]
-	}
-}
-
-func (r *resolveCtx) lastLoc() string {
-	s := r.locstack
-	if len(s) == 0 {
-		return ""
-	}
-	return s[len(s)-1]
-}
-
-func (p *Parser) getResolver(ctx *resolveCtx) (ReferenceResolver, error) {
-	loc := ctx.lastLoc()
+func (p *Parser) getResolver(ctx *jsonpointer.ResolveCtx) (ReferenceResolver, error) {
+	loc := ctx.LastLoc()
 
 	r, ok := p.schemas[loc]
 	if ok {
@@ -113,24 +44,18 @@ func (p *Parser) getResolver(ctx *resolveCtx) (ReferenceResolver, error) {
 	return r, nil
 }
 
-func (p *Parser) resolve(ref string, ctx *resolveCtx) (*Schema, error) {
-	u, err := url.Parse(ref)
-	if err != nil {
-		return nil, err
-	}
-	var key refKey
-	key.fromURL(u)
-
-	if s, ok := p.refcache[key]; ok {
+func (p *Parser) resolve(ref string, ctx *jsonpointer.ResolveCtx) (*Schema, error) {
+	if s, ok := p.refcache[ref]; ok {
 		return s, nil
 	}
 
-	if err := ctx.add(key); err != nil {
+	key, err := ctx.Add(ref)
+	if err != nil {
 		return nil, err
 	}
 	defer func() {
 		// Drop the resolved ref to prevent false-positive infinite recursion detection.
-		ctx.delete(key)
+		ctx.Delete(key)
 	}()
 
 	resolver, err := p.getResolver(ctx)
@@ -138,14 +63,14 @@ func (p *Parser) resolve(ref string, ctx *resolveCtx) (*Schema, error) {
 		return nil, err
 	}
 
-	raw, err := resolver.ResolveReference(key.ref)
+	raw, err := resolver.ResolveReference(key.Ref)
 	if err != nil {
 		return nil, errors.Wrap(err, "find schema")
 	}
 
 	return p.parse1(raw, ctx, func(s *Schema) *Schema {
 		s.Ref = ref
-		p.refcache[key] = s
+		p.refcache[ref] = s
 		return p.extendInfo(raw, s)
 	})
 }
diff --git a/openapi/parser/parse_components.go b/openapi/parser/parse_components.go
index 249cdea9..783514cf 100644
--- a/openapi/parser/parse_components.go
+++ b/openapi/parser/parse_components.go
@@ -4,6 +4,7 @@ import (
 	"github.com/go-faster/errors"
 
 	"github.com/ogen-go/ogen"
+	"github.com/ogen-go/ogen/internal/jsonpointer"
 	"github.com/ogen-go/ogen/jsonschema"
 	"github.com/ogen-go/ogen/openapi"
 )
@@ -37,7 +38,7 @@ func (p *parser) parseComponents(c *ogen.Components) (_ *openapi.Components, rer
 
 	for name := range c.Schemas {
 		ref := "#/components/schemas/" + name
-		s, err := p.schemaParser.Resolve(ref)
+		s, err := p.schemaParser.Resolve(ref, jsonpointer.NewResolveCtx(p.depthLimit))
 		if err != nil {
 			return nil, wrapErr("schemas", name, err)
 		}
@@ -47,7 +48,7 @@ func (p *parser) parseComponents(c *ogen.Components) (_ *openapi.Components, rer
 
 	for name := range c.Responses {
 		ref := "#/components/responses/" + name
-		r, err := p.resolveResponse(ref, newResolveCtx(p.depthLimit))
+		r, err := p.resolveResponse(ref, jsonpointer.NewResolveCtx(p.depthLimit))
 		if err != nil {
 			return nil, wrapErr("responses", name, err)
 		}
@@ -57,7 +58,7 @@ func (p *parser) parseComponents(c *ogen.Components) (_ *openapi.Components, rer
 
 	for name := range c.Parameters {
 		ref := "#/components/parameters/" + name
-		pp, err := p.resolveParameter(ref, newResolveCtx(p.depthLimit))
+		pp, err := p.resolveParameter(ref, jsonpointer.NewResolveCtx(p.depthLimit))
 		if err != nil {
 			return nil, wrapErr("parameters", name, err)
 		}
@@ -67,7 +68,7 @@ func (p *parser) parseComponents(c *ogen.Components) (_ *openapi.Components, rer
 
 	for name := range c.Examples {
 		ref := "#/components/examples/" + name
-		ex, err := p.resolveExample(ref, newResolveCtx(p.depthLimit))
+		ex, err := p.resolveExample(ref, jsonpointer.NewResolveCtx(p.depthLimit))
 		if err != nil {
 			return nil, wrapErr("examples", name, err)
 		}
@@ -77,7 +78,7 @@ func (p *parser) parseComponents(c *ogen.Components) (_ *openapi.Components, rer
 
 	for name := range c.RequestBodies {
 		ref := "#/components/requestBodies/" + name
-		b, err := p.resolveRequestBody(ref, newResolveCtx(p.depthLimit))
+		b, err := p.resolveRequestBody(ref, jsonpointer.NewResolveCtx(p.depthLimit))
 		if err != nil {
 			return nil, wrapErr("requestBodies", name, err)
 		}
diff --git a/openapi/parser/parse_example.go b/openapi/parser/parse_example.go
index 5b96bc98..e7b95de8 100644
--- a/openapi/parser/parse_example.go
+++ b/openapi/parser/parse_example.go
@@ -2,20 +2,21 @@ package parser
 
 import (
 	"github.com/ogen-go/ogen"
+	"github.com/ogen-go/ogen/internal/jsonpointer"
 	"github.com/ogen-go/ogen/openapi"
 )
 
-func (p *parser) parseExample(e *ogen.Example, ctx *resolveCtx) (_ *openapi.Example, rerr error) {
+func (p *parser) parseExample(e *ogen.Example, ctx *jsonpointer.ResolveCtx) (_ *openapi.Example, rerr error) {
 	if e == nil {
 		return nil, nil
 	}
 	defer func() {
-		rerr = p.wrapLocation(ctx.lastLoc(), e.Locator, rerr)
+		rerr = p.wrapLocation(ctx.LastLoc(), e.Locator, rerr)
 	}()
 	if ref := e.Ref; ref != "" {
 		resolved, err := p.resolveExample(ref, ctx)
 		if err != nil {
-			return nil, p.wrapRef(ctx.lastLoc(), e.Locator, err)
+			return nil, p.wrapRef(ctx.LastLoc(), e.Locator, err)
 		}
 		return resolved, nil
 	}
diff --git a/openapi/parser/parse_header.go b/openapi/parser/parse_header.go
index f883b244..cb2864a6 100644
--- a/openapi/parser/parse_header.go
+++ b/openapi/parser/parse_header.go
@@ -4,10 +4,11 @@ import (
 	"github.com/go-faster/errors"
 
 	"github.com/ogen-go/ogen"
+	"github.com/ogen-go/ogen/internal/jsonpointer"
 	"github.com/ogen-go/ogen/openapi"
 )
 
-func (p *parser) parseHeaders(headers map[string]*ogen.Header, ctx *resolveCtx) (_ map[string]*openapi.Header, err error) {
+func (p *parser) parseHeaders(headers map[string]*ogen.Header, ctx *jsonpointer.ResolveCtx) (_ map[string]*openapi.Header, err error) {
 	if len(headers) == 0 {
 		return nil, nil
 	}
@@ -23,17 +24,17 @@ func (p *parser) parseHeaders(headers map[string]*ogen.Header, ctx *resolveCtx)
 	return result, nil
 }
 
-func (p *parser) parseHeader(name string, header *ogen.Header, ctx *resolveCtx) (_ *openapi.Header, rerr error) {
+func (p *parser) parseHeader(name string, header *ogen.Header, ctx *jsonpointer.ResolveCtx) (_ *openapi.Header, rerr error) {
 	if header == nil {
 		return nil, errors.New("header object is empty or null")
 	}
 	defer func() {
-		rerr = p.wrapLocation(ctx.lastLoc(), header.Locator, rerr)
+		rerr = p.wrapLocation(ctx.LastLoc(), header.Locator, rerr)
 	}()
 	if ref := header.Ref; ref != "" {
 		resolved, err := p.resolveHeader(name, ref, ctx)
 		if err != nil {
-			return nil, p.wrapRef(ctx.lastLoc(), header.Locator, err)
+			return nil, p.wrapRef(ctx.LastLoc(), header.Locator, err)
 		}
 		return resolved, nil
 	}
@@ -43,7 +44,7 @@ func (p *parser) parseHeader(name string, header *ogen.Header, ctx *resolveCtx)
 			return nil
 		}
 		err := errors.Errorf(`%q MUST NOT be specified, got %q`, name, got)
-		return p.wrapField(name, ctx.lastLoc(), header.Locator, err)
+		return p.wrapField(name, ctx.LastLoc(), header.Locator, err)
 	}
 	if err := mustNotSpecified("in", header.In); err != nil {
 		return nil, err
@@ -53,20 +54,20 @@ func (p *parser) parseHeader(name string, header *ogen.Header, ctx *resolveCtx)
 	}
 	locatedIn := openapi.LocationHeader
 
-	if err := p.validateParameter(name, locatedIn, header, ctx.lastLoc()); err != nil {
+	if err := p.validateParameter(name, locatedIn, header, ctx.LastLoc()); err != nil {
 		return nil, err
 	}
 
 	schema, err := p.parseSchema(header.Schema, ctx)
 	if err != nil {
 		err := errors.Wrap(err, "schema")
-		return nil, p.wrapField("schema", ctx.lastLoc(), header.Locator, err)
+		return nil, p.wrapField("schema", ctx.LastLoc(), header.Locator, err)
 	}
 
 	content, err := p.parseParameterContent(header.Content, header.Locator.Field("content"), ctx)
 	if err != nil {
 		err := errors.Wrap(err, "content")
-		return nil, p.wrapField("content", ctx.lastLoc(), header.Locator, err)
+		return nil, p.wrapField("content", ctx.LastLoc(), header.Locator, err)
 	}
 
 	op := &openapi.Header{
@@ -84,7 +85,7 @@ func (p *parser) parseHeader(name string, header *ogen.Header, ctx *resolveCtx)
 
 	// TODO: Validate content?
 	if header.Content == nil {
-		if err := p.validateParamStyle(op, ctx.lastLoc()); err != nil {
+		if err := p.validateParamStyle(op, ctx.LastLoc()); err != nil {
 			return nil, err
 		}
 	}
diff --git a/openapi/parser/parse_mediatype.go b/openapi/parser/parse_mediatype.go
index 2bee6441..2479f2bc 100644
--- a/openapi/parser/parse_mediatype.go
+++ b/openapi/parser/parse_mediatype.go
@@ -7,6 +7,7 @@ import (
 	"github.com/go-faster/errors"
 
 	"github.com/ogen-go/ogen"
+	"github.com/ogen-go/ogen/internal/jsonpointer"
 	"github.com/ogen-go/ogen/internal/location"
 	"github.com/ogen-go/ogen/jsonschema"
 	"github.com/ogen-go/ogen/openapi"
@@ -15,7 +16,7 @@ import (
 func (p *parser) parseContent(
 	content map[string]ogen.Media,
 	locator location.Locator,
-	ctx *resolveCtx,
+	ctx *jsonpointer.ResolveCtx,
 ) (_ map[string]*openapi.MediaType, err error) {
 	if len(content) == 0 {
 		return nil, nil
@@ -26,7 +27,7 @@ func (p *parser) parseContent(
 		ct, _, err := mime.ParseMediaType(name)
 		if err != nil {
 			err := errors.Wrapf(err, "content type %q", name)
-			return nil, p.wrapLocation(ctx.lastLoc(), locator.Key(name), err)
+			return nil, p.wrapLocation(ctx.LastLoc(), locator.Key(name), err)
 		}
 
 		result[name], err = p.parseMediaType(ct, m, ctx)
@@ -41,7 +42,7 @@ func (p *parser) parseContent(
 func (p *parser) parseParameterContent(
 	content map[string]ogen.Media,
 	locator location.Locator,
-	ctx *resolveCtx,
+	ctx *jsonpointer.ResolveCtx,
 ) (*openapi.ParameterContent, error) {
 	if content == nil {
 		return nil, nil
@@ -53,14 +54,14 @@ func (p *parser) parseParameterContent(
 		}
 
 		err := errors.New(`"content" map MUST only contain one entry`)
-		return nil, p.wrapLocation(ctx.lastLoc(), locator, err)
+		return nil, p.wrapLocation(ctx.LastLoc(), locator, err)
 	}
 
 	for name, m := range content {
 		ct, _, err := mime.ParseMediaType(name)
 		if err != nil {
 			err := errors.Wrapf(err, "content type %q", name)
-			return nil, p.wrapLocation(ctx.lastLoc(), locator.Key(name), err)
+			return nil, p.wrapLocation(ctx.LastLoc(), locator.Key(name), err)
 		}
 
 		media, err := p.parseMediaType(ct, m, ctx)
@@ -76,9 +77,9 @@ func (p *parser) parseParameterContent(
 	panic("unreachable")
 }
 
-func (p *parser) parseMediaType(ct string, m ogen.Media, ctx *resolveCtx) (_ *openapi.MediaType, rerr error) {
+func (p *parser) parseMediaType(ct string, m ogen.Media, ctx *jsonpointer.ResolveCtx) (_ *openapi.MediaType, rerr error) {
 	defer func() {
-		rerr = p.wrapLocation(ctx.lastLoc(), m.Locator, rerr)
+		rerr = p.wrapLocation(ctx.LastLoc(), m.Locator, rerr)
 	}()
 
 	s, err := p.parseSchema(m.Schema, ctx)
@@ -100,7 +101,7 @@ func (p *parser) parseMediaType(ct string, m ogen.Media, ctx *resolveCtx) (_ *op
 
 			parseEncoding := func(name string, prop jsonschema.Property, e ogen.Encoding) (rerr error) {
 				defer func() {
-					rerr = p.wrapLocation(ctx.lastLoc(), e.Locator, rerr)
+					rerr = p.wrapLocation(ctx.LastLoc(), e.Locator, rerr)
 				}()
 
 				encoding := &openapi.Encoding{
@@ -113,7 +114,7 @@ func (p *parser) parseMediaType(ct string, m ogen.Media, ctx *resolveCtx) (_ *op
 				}
 				encoding.Headers, err = p.parseHeaders(e.Headers, ctx)
 				if err != nil {
-					return p.wrapField("headers", ctx.lastLoc(), e.Locator, err)
+					return p.wrapField("headers", ctx.LastLoc(), e.Locator, err)
 				}
 				encodings[name] = encoding
 
@@ -126,7 +127,7 @@ func (p *parser) parseMediaType(ct string, m ogen.Media, ctx *resolveCtx) (_ *op
 					Required:      prop.Required,
 					AllowReserved: encoding.AllowReserved,
 					Locator:       encoding.Locator,
-				}, ctx.lastLoc()); err != nil {
+				}, ctx.LastLoc()); err != nil {
 					return errors.Wrap(err, "param style")
 				}
 
@@ -139,7 +140,7 @@ func (p *parser) parseMediaType(ct string, m ogen.Media, ctx *resolveCtx) (_ *op
 				if !ok {
 					loc := encodingLoc.Key(name)
 					err := errors.Errorf("unknown property %q", name)
-					return nil, p.wrapLocation(ctx.lastLoc(), loc, err)
+					return nil, p.wrapLocation(ctx.LastLoc(), loc, err)
 				}
 
 				if err := parseEncoding(name, prop, e); err != nil {
diff --git a/openapi/parser/parse_parameter.go b/openapi/parser/parse_parameter.go
index b8d830ec..dff19c1a 100644
--- a/openapi/parser/parse_parameter.go
+++ b/openapi/parser/parse_parameter.go
@@ -7,6 +7,7 @@ import (
 	"golang.org/x/net/http/httpguts"
 
 	"github.com/ogen-go/ogen"
+	"github.com/ogen-go/ogen/internal/jsonpointer"
 	"github.com/ogen-go/ogen/internal/location"
 	"github.com/ogen-go/ogen/jsonschema"
 	"github.com/ogen-go/ogen/openapi"
@@ -37,7 +38,7 @@ func mergeParams(opParams, itemParams []*openapi.Parameter) []*openapi.Parameter
 func (p *parser) parseParams(
 	params []*ogen.Parameter,
 	locator location.Locator,
-	ctx *resolveCtx,
+	ctx *jsonpointer.ResolveCtx,
 ) ([]*openapi.Parameter, error) {
 	// Unique parameter is defined by a combination of a name and location.
 	type pnameLoc struct {
@@ -54,7 +55,7 @@ func (p *parser) parseParams(
 		if spec == nil {
 			loc := locator.Index(idx)
 			err := errors.Errorf("parameter %d is empty or null", idx)
-			return nil, p.wrapLocation(ctx.lastLoc(), loc, err)
+			return nil, p.wrapLocation(ctx.LastLoc(), loc, err)
 		}
 
 		param, err := p.parseParameter(spec, ctx)
@@ -68,7 +69,7 @@ func (p *parser) parseParams(
 		}
 		if _, ok := unique[ploc]; ok {
 			err := errors.Errorf("duplicate parameter: %q in %q", param.Name, param.In)
-			return nil, p.wrapLocation(ctx.lastLoc(), spec.Locator, err)
+			return nil, p.wrapLocation(ctx.LastLoc(), spec.Locator, err)
 		}
 
 		unique[ploc] = struct{}{}
@@ -112,17 +113,17 @@ func (p *parser) validateParameter(
 	return nil
 }
 
-func (p *parser) parseParameter(param *ogen.Parameter, ctx *resolveCtx) (_ *openapi.Parameter, rerr error) {
+func (p *parser) parseParameter(param *ogen.Parameter, ctx *jsonpointer.ResolveCtx) (_ *openapi.Parameter, rerr error) {
 	if param == nil {
 		return nil, errors.New("parameter object is empty or null")
 	}
 	defer func() {
-		rerr = p.wrapLocation(ctx.lastLoc(), param.Locator, rerr)
+		rerr = p.wrapLocation(ctx.LastLoc(), param.Locator, rerr)
 	}()
 	if ref := param.Ref; ref != "" {
 		parsed, err := p.resolveParameter(ref, ctx)
 		if err != nil {
-			return nil, p.wrapRef(ctx.lastLoc(), param.Locator, err)
+			return nil, p.wrapRef(ctx.LastLoc(), param.Locator, err)
 		}
 		return parsed, nil
 	}
@@ -137,23 +138,23 @@ func (p *parser) parseParameter(param *ogen.Parameter, ctx *resolveCtx) (_ *open
 	locatedIn, exists := types[strings.ToLower(param.In)]
 	if !exists {
 		err := errors.Errorf("unknown parameter location %q", param.In)
-		return nil, p.wrapField("in", ctx.lastLoc(), param.Locator, err)
+		return nil, p.wrapField("in", ctx.LastLoc(), param.Locator, err)
 	}
 
-	if err := p.validateParameter(param.Name, locatedIn, param, ctx.lastLoc()); err != nil {
+	if err := p.validateParameter(param.Name, locatedIn, param, ctx.LastLoc()); err != nil {
 		return nil, err
 	}
 
 	schema, err := p.parseSchema(param.Schema, ctx)
 	if err != nil {
 		err := errors.Wrap(err, "schema")
-		return nil, p.wrapField("schema", ctx.lastLoc(), param.Locator, err)
+		return nil, p.wrapField("schema", ctx.LastLoc(), param.Locator, err)
 	}
 
 	content, err := p.parseParameterContent(param.Content, param.Locator.Field("content"), ctx)
 	if err != nil {
 		err := errors.Wrap(err, "content")
-		return nil, p.wrapField("content", ctx.lastLoc(), param.Locator, err)
+		return nil, p.wrapField("content", ctx.LastLoc(), param.Locator, err)
 	}
 
 	op := &openapi.Parameter{
@@ -172,7 +173,7 @@ func (p *parser) parseParameter(param *ogen.Parameter, ctx *resolveCtx) (_ *open
 
 	// TODO: Validate content?
 	if param.Content == nil {
-		if err := p.validateParamStyle(op, ctx.lastLoc()); err != nil {
+		if err := p.validateParamStyle(op, ctx.LastLoc()); err != nil {
 			return nil, err
 		}
 	}
diff --git a/openapi/parser/parse_path_item.go b/openapi/parser/parse_path_item.go
index f1b916ab..c8767096 100644
--- a/openapi/parser/parse_path_item.go
+++ b/openapi/parser/parse_path_item.go
@@ -4,6 +4,7 @@ import (
 	"github.com/go-faster/errors"
 
 	"github.com/ogen-go/ogen"
+	"github.com/ogen-go/ogen/internal/jsonpointer"
 	"github.com/ogen-go/ogen/internal/location"
 	"github.com/ogen-go/ogen/openapi"
 )
@@ -14,19 +15,19 @@ func (p *parser) parsePathItem(
 	path string,
 	item *ogen.PathItem,
 	operationIDs map[string]struct{},
-	ctx *resolveCtx,
+	ctx *jsonpointer.ResolveCtx,
 ) (_ pathItem, rerr error) {
 	if item == nil {
 		return nil, errors.New("pathItem object is empty or null")
 	}
 	defer func() {
-		rerr = p.wrapLocation(ctx.lastLoc(), item.Locator, rerr)
+		rerr = p.wrapLocation(ctx.LastLoc(), item.Locator, rerr)
 	}()
 
 	if ref := item.Ref; ref != "" {
 		ops, err := p.resolvePathItem(path, ref, operationIDs, ctx)
 		if err != nil {
-			return nil, p.wrapRef(ctx.lastLoc(), item.Locator, err)
+			return nil, p.wrapRef(ctx.LastLoc(), item.Locator, err)
 		}
 		return ops, nil
 	}
@@ -66,10 +67,10 @@ func (p *parser) parseOp(
 	path, httpMethod string,
 	spec ogen.Operation,
 	itemParams []*openapi.Parameter,
-	ctx *resolveCtx,
+	ctx *jsonpointer.ResolveCtx,
 ) (_ *openapi.Operation, err error) {
 	defer func() {
-		err = p.wrapLocation(ctx.lastLoc(), spec.Locator, err)
+		err = p.wrapLocation(ctx.LastLoc(), spec.Locator, err)
 	}()
 
 	op := &openapi.Operation{
@@ -106,7 +107,7 @@ func (p *parser) parseOp(
 		op.Responses, err = p.parseResponses(spec.Responses, locator, ctx)
 		if err != nil {
 			err := errors.Wrap(err, "responses")
-			return nil, p.wrapLocation(ctx.lastLoc(), locator, err)
+			return nil, p.wrapLocation(ctx.LastLoc(), locator, err)
 		}
 	}
 
@@ -114,7 +115,7 @@ func (p *parser) parseOp(
 		op.Security, err = p.parseSecurityRequirements(spec, locator, ctx)
 		if err != nil {
 			err := errors.Wrap(err, "security")
-			return p.wrapLocation(ctx.lastLoc(), locator, err)
+			return p.wrapLocation(ctx.LastLoc(), locator, err)
 		}
 		return nil
 	}
diff --git a/openapi/parser/parse_request_body.go b/openapi/parser/parse_request_body.go
index 5e973271..d0a90a58 100644
--- a/openapi/parser/parse_request_body.go
+++ b/openapi/parser/parse_request_body.go
@@ -4,20 +4,21 @@ import (
 	"github.com/go-faster/errors"
 
 	"github.com/ogen-go/ogen"
+	"github.com/ogen-go/ogen/internal/jsonpointer"
 	"github.com/ogen-go/ogen/openapi"
 )
 
-func (p *parser) parseRequestBody(body *ogen.RequestBody, ctx *resolveCtx) (_ *openapi.RequestBody, rerr error) {
+func (p *parser) parseRequestBody(body *ogen.RequestBody, ctx *jsonpointer.ResolveCtx) (_ *openapi.RequestBody, rerr error) {
 	if body == nil {
 		return nil, errors.New("requestBody object is empty or null")
 	}
 	defer func() {
-		rerr = p.wrapLocation(ctx.lastLoc(), body.Locator, rerr)
+		rerr = p.wrapLocation(ctx.LastLoc(), body.Locator, rerr)
 	}()
 	if ref := body.Ref; ref != "" {
 		resolved, err := p.resolveRequestBody(ref, ctx)
 		if err != nil {
-			return nil, p.wrapRef(ctx.lastLoc(), body.Locator, err)
+			return nil, p.wrapRef(ctx.LastLoc(), body.Locator, err)
 		}
 		return resolved, nil
 	}
@@ -36,7 +37,7 @@ func (p *parser) parseRequestBody(body *ogen.RequestBody, ctx *resolveCtx) (_ *o
 		return content, nil
 	}()
 	if err != nil {
-		return nil, p.wrapField("content", ctx.lastLoc(), body.Locator, err)
+		return nil, p.wrapField("content", ctx.LastLoc(), body.Locator, err)
 	}
 
 	return &openapi.RequestBody{
diff --git a/openapi/parser/parse_response.go b/openapi/parser/parse_response.go
index 06aa26d0..d40596b7 100644
--- a/openapi/parser/parse_response.go
+++ b/openapi/parser/parse_response.go
@@ -6,6 +6,7 @@ import (
 	"github.com/go-faster/errors"
 
 	"github.com/ogen-go/ogen"
+	"github.com/ogen-go/ogen/internal/jsonpointer"
 	"github.com/ogen-go/ogen/internal/location"
 	"github.com/ogen-go/ogen/openapi"
 )
@@ -13,7 +14,7 @@ import (
 func (p *parser) parseResponses(
 	responses ogen.Responses,
 	locator location.Locator,
-	ctx *resolveCtx,
+	ctx *jsonpointer.ResolveCtx,
 ) (_ map[string]*openapi.Response, err error) {
 	if len(responses) == 0 {
 		return nil, errors.New("no responses")
@@ -22,13 +23,13 @@ func (p *parser) parseResponses(
 	result := make(map[string]*openapi.Response, len(responses))
 	for status, response := range responses {
 		if err := validateStatusCode(status); err != nil {
-			return nil, p.wrapLocation(ctx.lastLoc(), locator.Key(status), err)
+			return nil, p.wrapLocation(ctx.LastLoc(), locator.Key(status), err)
 		}
 
 		resp, err := p.parseResponse(response, ctx)
 		if err != nil {
 			err := errors.Wrap(err, status)
-			return nil, p.wrapLocation(ctx.lastLoc(), locator.Field(status), err)
+			return nil, p.wrapLocation(ctx.LastLoc(), locator.Field(status), err)
 		}
 
 		result[status] = resp
@@ -37,17 +38,17 @@ func (p *parser) parseResponses(
 	return result, nil
 }
 
-func (p *parser) parseResponse(resp *ogen.Response, ctx *resolveCtx) (_ *openapi.Response, rerr error) {
+func (p *parser) parseResponse(resp *ogen.Response, ctx *jsonpointer.ResolveCtx) (_ *openapi.Response, rerr error) {
 	if resp == nil {
 		return nil, errors.New("response object is empty or null")
 	}
 	defer func() {
-		rerr = p.wrapLocation(ctx.lastLoc(), resp.Locator, rerr)
+		rerr = p.wrapLocation(ctx.LastLoc(), resp.Locator, rerr)
 	}()
 	if ref := resp.Ref; ref != "" {
 		resolved, err := p.resolveResponse(ref, ctx)
 		if err != nil {
-			return nil, p.wrapRef(ctx.lastLoc(), resp.Locator, err)
+			return nil, p.wrapRef(ctx.LastLoc(), resp.Locator, err)
 		}
 		return resolved, nil
 	}
@@ -55,13 +56,13 @@ func (p *parser) parseResponse(resp *ogen.Response, ctx *resolveCtx) (_ *openapi
 	content, err := p.parseContent(resp.Content, resp.Locator.Field("content"), ctx)
 	if err != nil {
 		err := errors.Wrap(err, "content")
-		return nil, p.wrapField("content", ctx.lastLoc(), resp.Locator, err)
+		return nil, p.wrapField("content", ctx.LastLoc(), resp.Locator, err)
 	}
 
 	headers, err := p.parseHeaders(resp.Headers, ctx)
 	if err != nil {
 		err := errors.Wrap(err, "headers")
-		return nil, p.wrapField("headers", ctx.lastLoc(), resp.Locator, err)
+		return nil, p.wrapField("headers", ctx.LastLoc(), resp.Locator, err)
 	}
 
 	return &openapi.Response{
diff --git a/openapi/parser/parse_schema.go b/openapi/parser/parse_schema.go
index d7cb8e11..3bb07fdc 100644
--- a/openapi/parser/parse_schema.go
+++ b/openapi/parser/parse_schema.go
@@ -1,31 +1,11 @@
 package parser
 
 import (
-	"net/url"
-	"strings"
-
-	"github.com/go-faster/errors"
-
 	"github.com/ogen-go/ogen"
+	"github.com/ogen-go/ogen/internal/jsonpointer"
 	"github.com/ogen-go/ogen/jsonschema"
 )
 
-func (p *parser) parseSchema(schema *ogen.Schema, ctx *resolveCtx) (*jsonschema.Schema, error) {
-	// jsonschema.Parser will add location to the error.
-
-	s := schema.ToJSONSchema()
-	if loc := ctx.lastLoc(); s != nil && s.Ref != "" && loc != "" {
-		base, err := url.Parse(loc)
-		if err != nil {
-			return nil, errors.Wrap(err, "parse base")
-		}
-
-		ref, err := url.Parse(s.Ref)
-		if err != nil {
-			return nil, errors.Wrap(err, "parse ref")
-		}
-
-		s.Ref = strings.TrimPrefix(base.ResolveReference(ref).String(), "/")
-	}
-	return p.schemaParser.Parse(s)
+func (p *parser) parseSchema(schema *ogen.Schema, ctx *jsonpointer.ResolveCtx) (*jsonschema.Schema, error) {
+	return p.schemaParser.ParseWithContext(schema.ToJSONSchema(), ctx)
 }
diff --git a/openapi/parser/parse_security.go b/openapi/parser/parse_security.go
index bc664d32..e4e771d4 100644
--- a/openapi/parser/parse_security.go
+++ b/openapi/parser/parse_security.go
@@ -7,25 +7,26 @@ import (
 	"github.com/go-faster/errors"
 
 	"github.com/ogen-go/ogen"
+	"github.com/ogen-go/ogen/internal/jsonpointer"
 	"github.com/ogen-go/ogen/internal/location"
 	"github.com/ogen-go/ogen/openapi"
 )
 
 func (p *parser) parseSecurityScheme(
 	scheme *ogen.SecurityScheme,
-	ctx *resolveCtx,
+	ctx *jsonpointer.ResolveCtx,
 ) (_ *ogen.SecurityScheme, rerr error) {
 	if scheme == nil {
 		return nil, errors.New("securityScheme is empty or null")
 	}
 	defer func() {
-		rerr = p.wrapLocation(ctx.lastLoc(), scheme.Locator, rerr)
+		rerr = p.wrapLocation(ctx.LastLoc(), scheme.Locator, rerr)
 	}()
 
 	if ref := scheme.Ref; ref != "" {
 		resolved, err := p.resolveSecurityScheme(ref, ctx)
 		if err != nil {
-			return nil, p.wrapRef(ctx.lastLoc(), scheme.Locator, err)
+			return nil, p.wrapRef(ctx.LastLoc(), scheme.Locator, err)
 		}
 		return resolved, nil
 	}
@@ -37,11 +38,11 @@ func (p *parser) parseSecurityScheme(
 			case "query", "header", "cookie":
 			default:
 				err := errors.Errorf(`invalid "in": %q`, scheme.In)
-				return p.wrapField("in", ctx.lastLoc(), scheme.Locator, err)
+				return p.wrapField("in", ctx.LastLoc(), scheme.Locator, err)
 			}
 			if scheme.Name == "" {
 				err := errors.New(`"name" is required and MUST be a non-empty string`)
-				return p.wrapField("name", ctx.lastLoc(), scheme.Locator, err)
+				return p.wrapField("name", ctx.LastLoc(), scheme.Locator, err)
 			}
 			return nil
 		case "http":
@@ -63,23 +64,23 @@ func (p *parser) parseSecurityScheme(
 				"vapid":
 			default:
 				err := errors.Errorf(`invalid "scheme": %q`, scheme.Scheme)
-				return p.wrapField("scheme", ctx.lastLoc(), scheme.Locator, err)
+				return p.wrapField("scheme", ctx.LastLoc(), scheme.Locator, err)
 			}
 			return nil
 		case "mutualTLS":
 			return nil
 		case "oauth2":
-			err := p.validateOAuthFlows(scheme.Flows, ctx.lastLoc())
-			return p.wrapField("flows", ctx.lastLoc(), scheme.Locator, err)
+			err := p.validateOAuthFlows(scheme.Flows, ctx.LastLoc())
+			return p.wrapField("flows", ctx.LastLoc(), scheme.Locator, err)
 		case "openIdConnect":
 			if _, err := url.ParseRequestURI(scheme.OpenIDConnectURL); err != nil {
 				err := errors.Wrap(err, `"openIdConnectUrl" MUST be in the form of a URL`)
-				return p.wrapField("openIdConnectUrl", ctx.lastLoc(), scheme.Locator, err)
+				return p.wrapField("openIdConnectUrl", ctx.LastLoc(), scheme.Locator, err)
 			}
 			return nil
 		default:
 			err := errors.Errorf("unknown security scheme type %q", scheme.Type)
-			return p.wrapField("type", ctx.lastLoc(), scheme.Locator, err)
+			return p.wrapField("type", ctx.LastLoc(), scheme.Locator, err)
 		}
 	}(); err != nil {
 		return nil, errors.Wrap(err, scheme.Type)
@@ -180,7 +181,7 @@ func cloneOAuthFlows(flows ogen.OAuthFlows) (r openapi.OAuthFlows) {
 func (p *parser) parseSecurityRequirements(
 	requirements ogen.SecurityRequirements,
 	locator location.Locator,
-	ctx *resolveCtx,
+	ctx *jsonpointer.ResolveCtx,
 ) ([]openapi.SecurityRequirement, error) {
 	result := make([]openapi.SecurityRequirement, 0, len(requirements))
 	securitySchemesLoc := p.rootLoc.Field("components").Field("securitySchemes")
@@ -192,14 +193,14 @@ func (p *parser) parseSecurityRequirements(
 			v, ok := p.refs.securitySchemes[name]
 			if !ok {
 				err := errors.Errorf("unknown security schema %q", name)
-				return nil, p.wrapLocation(ctx.lastLoc(), locator.Key(name), err)
+				return nil, p.wrapLocation(ctx.LastLoc(), locator.Key(name), err)
 			}
 
 			spec, err := p.parseSecurityScheme(v, ctx)
 			if err != nil {
 				loc := securitySchemesLoc.Field(name)
 				err := errors.Wrapf(err, "parse security scheme %q", name)
-				return nil, p.wrapLocation(ctx.lastLoc(), loc, err)
+				return nil, p.wrapLocation(ctx.LastLoc(), loc, err)
 			}
 
 			if len(scopes) > 0 {
@@ -209,7 +210,7 @@ func (p *parser) parseSecurityRequirements(
 					// Point to the first scope in the list.
 					locator := locator.Field(name).Index(0)
 					err := errors.Errorf(`list of scopes MUST be empty for "type" %q`, spec.Type)
-					return nil, p.wrapLocation(ctx.lastLoc(), locator, err)
+					return nil, p.wrapLocation(ctx.LastLoc(), locator, err)
 				}
 			}
 
diff --git a/openapi/parser/parser.go b/openapi/parser/parser.go
index daca902f..d44dbce4 100644
--- a/openapi/parser/parser.go
+++ b/openapi/parser/parser.go
@@ -8,6 +8,7 @@ import (
 	yaml "github.com/go-faster/yamlx"
 
 	"github.com/ogen-go/ogen"
+	"github.com/ogen-go/ogen/internal/jsonpointer"
 	"github.com/ogen-go/ogen/internal/location"
 	"github.com/ogen-go/ogen/jsonschema"
 	"github.com/ogen-go/ogen/openapi"
@@ -80,7 +81,6 @@ func Parse(spec *ogen.Spec, s Settings) (*openapi.API, error) {
 				root:       jsonschema.NewRootResolver(spec.Raw),
 			},
 			Filename:   s.Filename,
-			DepthLimit: s.DepthLimit,
 			InferTypes: s.InferTypes,
 		}),
 	}
@@ -148,7 +148,7 @@ func (p *parser) parsePathItems() error {
 			return p.wrapLocation("", pathsLoc.Key(path), err)
 		}
 
-		ops, err := p.parsePathItem(path, item, operationIDs, newResolveCtx(p.depthLimit))
+		ops, err := p.parsePathItem(path, item, operationIDs, jsonpointer.NewResolveCtx(p.depthLimit))
 		if err != nil {
 			err := errors.Wrapf(err, "path %q", path)
 			return p.wrapLocation("", pathsLoc.Field(path), err)
diff --git a/openapi/parser/resolve.go b/openapi/parser/resolve.go
index a8fb2a7a..f3b580a5 100644
--- a/openapi/parser/resolve.go
+++ b/openapi/parser/resolve.go
@@ -12,8 +12,8 @@ import (
 	"github.com/ogen-go/ogen/openapi"
 )
 
-func (p *parser) getSchema(ctx *resolveCtx) (*yaml.Node, error) {
-	loc := ctx.lastLoc()
+func (p *parser) getSchema(ctx *jsonpointer.ResolveCtx) (*yaml.Node, error) {
+	loc := ctx.LastLoc()
 
 	r, ok := p.schemas[loc]
 	if ok {
@@ -48,31 +48,31 @@ func resolvePointer(root *yaml.Node, ptr string, to any) error {
 	return n.Decode(to)
 }
 
-func (p *parser) resolve(key refKey, ctx *resolveCtx, to any) error {
+func (p *parser) resolve(key jsonpointer.RefKey, ctx *jsonpointer.ResolveCtx, to any) error {
 	schema, err := p.getSchema(ctx)
 	if err != nil {
 		return err
 	}
-	return resolvePointer(schema, key.ref, to)
+	return resolvePointer(schema, key.Ref, to)
 }
 
-func (p *parser) resolveRequestBody(ref string, ctx *resolveCtx) (*openapi.RequestBody, error) {
+func (p *parser) resolveRequestBody(ref string, ctx *jsonpointer.ResolveCtx) (*openapi.RequestBody, error) {
 	const prefix = "#/components/requestBodies/"
 
 	if r, ok := p.refs.requestBodies[ref]; ok {
 		return r, nil
 	}
 
-	key, err := ctx.add(ref)
+	key, err := ctx.Add(ref)
 	if err != nil {
 		return nil, err
 	}
 	defer func() {
-		ctx.delete(key)
+		ctx.Delete(key)
 	}()
 
 	var component *ogen.RequestBody
-	if key.loc == "" && ctx.lastLoc() == "" {
+	if key.Loc == "" && ctx.LastLoc() == "" {
 		name := strings.TrimPrefix(ref, prefix)
 		c, found := p.spec.Components.RequestBodies[name]
 		if found {
@@ -98,23 +98,23 @@ func (p *parser) resolveRequestBody(ref string, ctx *resolveCtx) (*openapi.Reque
 	return r, nil
 }
 
-func (p *parser) resolveResponse(ref string, ctx *resolveCtx) (*openapi.Response, error) {
+func (p *parser) resolveResponse(ref string, ctx *jsonpointer.ResolveCtx) (*openapi.Response, error) {
 	const prefix = "#/components/responses/"
 
 	if r, ok := p.refs.responses[ref]; ok {
 		return r, nil
 	}
 
-	key, err := ctx.add(ref)
+	key, err := ctx.Add(ref)
 	if err != nil {
 		return nil, err
 	}
 	defer func() {
-		ctx.delete(key)
+		ctx.Delete(key)
 	}()
 
 	var component *ogen.Response
-	if key.loc == "" && ctx.lastLoc() == "" {
+	if key.Loc == "" && ctx.LastLoc() == "" {
 		name := strings.TrimPrefix(ref, prefix)
 		c, found := p.spec.Components.Responses[name]
 		if found {
@@ -140,23 +140,23 @@ func (p *parser) resolveResponse(ref string, ctx *resolveCtx) (*openapi.Response
 	return r, nil
 }
 
-func (p *parser) resolveParameter(ref string, ctx *resolveCtx) (*openapi.Parameter, error) {
+func (p *parser) resolveParameter(ref string, ctx *jsonpointer.ResolveCtx) (*openapi.Parameter, error) {
 	const prefix = "#/components/parameters/"
 
 	if param, ok := p.refs.parameters[ref]; ok {
 		return param, nil
 	}
 
-	key, err := ctx.add(ref)
+	key, err := ctx.Add(ref)
 	if err != nil {
 		return nil, err
 	}
 	defer func() {
-		ctx.delete(key)
+		ctx.Delete(key)
 	}()
 
 	var component *ogen.Parameter
-	if key.loc == "" && ctx.lastLoc() == "" {
+	if key.Loc == "" && ctx.LastLoc() == "" {
 		name := strings.TrimPrefix(ref, prefix)
 		c, found := p.spec.Components.Parameters[name]
 		if found {
@@ -182,23 +182,23 @@ func (p *parser) resolveParameter(ref string, ctx *resolveCtx) (*openapi.Paramet
 	return param, nil
 }
 
-func (p *parser) resolveHeader(headerName, ref string, ctx *resolveCtx) (*openapi.Header, error) {
+func (p *parser) resolveHeader(headerName, ref string, ctx *jsonpointer.ResolveCtx) (*openapi.Header, error) {
 	const prefix = "#/components/headers/"
 
 	if header, ok := p.refs.headers[ref]; ok {
 		return header, nil
 	}
 
-	key, err := ctx.add(ref)
+	key, err := ctx.Add(ref)
 	if err != nil {
 		return nil, err
 	}
 	defer func() {
-		ctx.delete(key)
+		ctx.Delete(key)
 	}()
 
 	var component *ogen.Header
-	if key.loc == "" && ctx.lastLoc() == "" {
+	if key.Loc == "" && ctx.LastLoc() == "" {
 		name := strings.TrimPrefix(ref, prefix)
 		c, found := p.spec.Components.Headers[name]
 		if found {
@@ -224,23 +224,23 @@ func (p *parser) resolveHeader(headerName, ref string, ctx *resolveCtx) (*openap
 	return header, nil
 }
 
-func (p *parser) resolveExample(ref string, ctx *resolveCtx) (*openapi.Example, error) {
+func (p *parser) resolveExample(ref string, ctx *jsonpointer.ResolveCtx) (*openapi.Example, error) {
 	const prefix = "#/components/examples/"
 
 	if ex, ok := p.refs.examples[ref]; ok {
 		return ex, nil
 	}
 
-	key, err := ctx.add(ref)
+	key, err := ctx.Add(ref)
 	if err != nil {
 		return nil, err
 	}
 	defer func() {
-		ctx.delete(key)
+		ctx.Delete(key)
 	}()
 
 	var component *ogen.Example
-	if key.loc == "" && ctx.lastLoc() == "" {
+	if key.Loc == "" && ctx.LastLoc() == "" {
 		name := strings.TrimPrefix(ref, prefix)
 		c, found := p.spec.Components.Examples[name]
 		if found {
@@ -268,23 +268,23 @@ func (p *parser) resolveExample(ref string, ctx *resolveCtx) (*openapi.Example,
 	return ex, nil
 }
 
-func (p *parser) resolveSecurityScheme(ref string, ctx *resolveCtx) (*ogen.SecurityScheme, error) {
+func (p *parser) resolveSecurityScheme(ref string, ctx *jsonpointer.ResolveCtx) (*ogen.SecurityScheme, error) {
 	const prefix = "#/components/securitySchemes/"
 
 	if r, ok := p.refs.securitySchemes[ref]; ok {
 		return r, nil
 	}
 
-	key, err := ctx.add(ref)
+	key, err := ctx.Add(ref)
 	if err != nil {
 		return nil, err
 	}
 	defer func() {
-		ctx.delete(key)
+		ctx.Delete(key)
 	}()
 
 	var component *ogen.SecurityScheme
-	if key.loc == "" && ctx.lastLoc() == "" {
+	if key.Loc == "" && ctx.LastLoc() == "" {
 		name := strings.TrimPrefix(ref, prefix)
 		c, found := p.spec.Components.SecuritySchemes[name]
 		if found {
@@ -312,7 +312,7 @@ func (p *parser) resolveSecurityScheme(ref string, ctx *resolveCtx) (*ogen.Secur
 func (p *parser) resolvePathItem(
 	itemPath, ref string,
 	operationIDs map[string]struct{},
-	ctx *resolveCtx,
+	ctx *jsonpointer.ResolveCtx,
 ) (pathItem, error) {
 	const prefix = "#/components/pathItems/"
 
@@ -320,16 +320,16 @@ func (p *parser) resolvePathItem(
 		return r, nil
 	}
 
-	key, err := ctx.add(ref)
+	key, err := ctx.Add(ref)
 	if err != nil {
 		return nil, err
 	}
 	defer func() {
-		ctx.delete(key)
+		ctx.Delete(key)
 	}()
 
 	var component *ogen.PathItem
-	if key.loc == "" && ctx.lastLoc() == "" {
+	if key.Loc == "" && ctx.LastLoc() == "" {
 		name := strings.TrimPrefix(ref, prefix)
 		c, found := p.spec.Components.PathItems[name]
 		if found {
diff --git a/openapi/parser/resolve_ctx.go b/openapi/parser/resolve_ctx.go
deleted file mode 100644
index e1a092f4..00000000
--- a/openapi/parser/resolve_ctx.go
+++ /dev/null
@@ -1,84 +0,0 @@
-package parser
-
-import (
-	"net/url"
-
-	"github.com/go-faster/errors"
-)
-
-type refKey struct {
-	loc string
-	ref string
-}
-
-func (r *refKey) fromURL(u *url.URL) {
-	{
-		// Make copy.
-		u2 := *u
-		u2.Fragment = ""
-		r.loc = u2.String()
-	}
-	r.ref = "#" + u.Fragment
-}
-
-type resolveCtx struct {
-	// Location stack. Used for context-depending resolving.
-	//
-	// For resolve trace like
-	//
-	// 	"#/components/schemas/Schema" ->
-	// 	"https://example.com/schema#Schema" ->
-	//	"#/definitions/SchemaProperty"
-	//
-	// "#/definitions/SchemaProperty" should be resolved against "https://example.com/schema".
-	locstack []string
-	// Store references to detect infinite recursive references.
-	refs       map[refKey]struct{}
-	depthLimit int
-}
-
-func newResolveCtx(depthLimit int) *resolveCtx {
-	return &resolveCtx{
-		locstack:   nil,
-		refs:       map[refKey]struct{}{},
-		depthLimit: depthLimit,
-	}
-}
-
-func (r *resolveCtx) add(ref string) (key refKey, _ error) {
-	u, err := url.Parse(ref)
-	if err != nil {
-		return refKey{}, err
-	}
-	key.fromURL(u)
-
-	if r.depthLimit <= 0 {
-		return refKey{}, errors.New("depth limit exceeded")
-	}
-	if _, ok := r.refs[key]; ok {
-		return key, errors.New("infinite recursion")
-	}
-	r.refs[key] = struct{}{}
-	r.depthLimit--
-
-	if key.loc != "" {
-		r.locstack = append(r.locstack, key.loc)
-	}
-	return key, nil
-}
-
-func (r *resolveCtx) delete(key refKey) {
-	r.depthLimit++
-	delete(r.refs, key)
-	if key.loc != "" && len(r.locstack) > 0 {
-		r.locstack = r.locstack[:len(r.locstack)-1]
-	}
-}
-
-func (r *resolveCtx) lastLoc() string {
-	s := r.locstack
-	if len(s) == 0 {
-		return ""
-	}
-	return s[len(s)-1]
-}
diff --git a/openapi/parser/settings.go b/openapi/parser/settings.go
index 4f82502b..2babe186 100644
--- a/openapi/parser/settings.go
+++ b/openapi/parser/settings.go
@@ -1,6 +1,9 @@
 package parser
 
-import "github.com/ogen-go/ogen/jsonschema"
+import (
+	"github.com/ogen-go/ogen/internal/jsonpointer"
+	"github.com/ogen-go/ogen/jsonschema"
+)
 
 // Settings is parser settings.
 type Settings struct {
@@ -34,6 +37,6 @@ func (s *Settings) setDefaults() {
 		s.External = jsonschema.NoExternal{}
 	}
 	if s.DepthLimit == 0 {
-		s.DepthLimit = 1000
+		s.DepthLimit = jsonpointer.DefaultDepthLimit
 	}
 }


Test patch:
diff --git a/jsonschema/parser_test.go b/jsonschema/parser_test.go
index 81d5dfa7..c4c513ec 100644
--- a/jsonschema/parser_test.go
+++ b/jsonschema/parser_test.go
@@ -116,8 +116,8 @@ func TestSchemaRecursive(t *testing.T) {
 		},
 	}
 
-	expectRefcache := map[refKey]*Schema{
-		{ref: "#/components/schemas/Pet"}: {
+	expectRefcache := map[string]*Schema{
+		"#/components/schemas/Pet": {
 			Type: Object,
 			Ref:  "#/components/schemas/Pet",
 			Properties: []Property{
@@ -306,8 +306,8 @@ func TestSchemaReferencedArray(t *testing.T) {
 		Item: &Schema{Type: String},
 	}
 
-	expectRefcache := map[refKey]*Schema{
-		{ref: "#/components/schemas/Pets"}: pets,
+	expectRefcache := map[string]*Schema{
+		"#/components/schemas/Pets": pets,
 	}
 
 	expect := &Schema{
diff --git a/jsonschema/resolve_test.go b/jsonschema/resolve_test.go
index 33b9e7e3..db54a97b 100644
--- a/jsonschema/resolve_test.go
+++ b/jsonschema/resolve_test.go
@@ -3,18 +3,22 @@ package jsonschema
 import (
 	"context"
 	"encoding/json"
+	"strings"
 	"testing"
 
 	"github.com/go-faster/errors"
 	"github.com/go-faster/jx"
 	"github.com/stretchr/testify/require"
 
+	"github.com/ogen-go/ogen/internal/jsonpointer"
 	"github.com/ogen-go/ogen/internal/location"
 )
 
 type external map[string]components
 
 func (e external) Get(_ context.Context, loc string) ([]byte, error) {
+	loc = strings.TrimPrefix(loc, "/")
+
 	r, ok := e[loc]
 	if !ok {
 		return nil, errors.Errorf("unexpected location %q", loc)
@@ -66,12 +70,6 @@ func TestExternalReference(t *testing.T) {
 							Ref: "https://example.com/bar.json#/components/schemas/Property",
 						},
 					},
-					{
-						Name: "remote_recursive",
-						Schema: &RawSchema{
-							Ref: "https://example.com/bar.json#/components/schemas/Recursive",
-						},
-					},
 				},
 			},
 			"Property": {
@@ -88,9 +86,6 @@ func TestExternalReference(t *testing.T) {
 			"Property": {
 				Type: "boolean",
 			},
-			"Recursive": {
-				Ref: "foo.json#/components/schemas/Property",
-			},
 		},
 	}
 
@@ -125,10 +120,6 @@ func TestExternalReference(t *testing.T) {
 					Name:   "remote_absolute",
 					Schema: &Schema{Ref: "https://example.com/bar.json#/components/schemas/Property", Type: Boolean},
 				},
-				{
-					Name:   "remote_recursive",
-					Schema: &Schema{Ref: "foo.json#/components/schemas/Property", Type: Number},
-				},
 			},
 		},
 	}
@@ -181,15 +172,13 @@ func TestLimitDepth(t *testing.T) {
 		{2, require.Error},
 		{3, require.Error},
 		{4, require.NoError},
-		{0, require.NoError}, // Default
 	}
 
 	for _, tt := range tests {
 		parser := NewParser(Settings{
-			Resolver:   root,
-			DepthLimit: tt.limit,
+			Resolver: root,
 		})
-		_, err := parser.Resolve("#/components/schemas/Schema1")
+		_, err := parser.Resolve("#/components/schemas/Schema1", jsonpointer.NewResolveCtx(tt.limit))
 		tt.checker(t, err, "limit: %d", tt.limit)
 	}
 }
diff --git a/openapi/parser/_testdata/negative/parameters/invalid_parameter_schema.json b/openapi/parser/_testdata/negative/parameters/invalid_parameter_schema.json
index ff3a2173..ca9a0fba 100644
--- a/openapi/parser/_testdata/negative/parameters/invalid_parameter_schema.json
+++ b/openapi/parser/_testdata/negative/parameters/invalid_parameter_schema.json
@@ -12,7 +12,7 @@
             "name": "foo",
             "in": "query",
             "schema": {
-                "type": "foobar"
+              "type": "foobar"
             }
           }
         ],
diff --git a/openapi/parser/resolve_external_test.go b/openapi/parser/resolve_external_test.go
index 9c534d8a..abbd3dca 100644
--- a/openapi/parser/resolve_external_test.go
+++ b/openapi/parser/resolve_external_test.go
@@ -3,6 +3,7 @@ package parser
 import (
 	"context"
 	"encoding/json"
+	"strings"
 	"testing"
 
 	"github.com/go-faster/errors"
@@ -16,6 +17,7 @@ import (
 type external map[string]any
 
 func (e external) Get(_ context.Context, loc string) ([]byte, error) {
+	loc = strings.TrimPrefix(loc, "/")
 	r, ok := e[loc]
 	if !ok {
 		return nil, errors.Errorf("unexpected location %q", loc)