Repository: ogen-go/ogen
Pull request #392 (2022-06-10 07:50:30)

feat(parser): support remote component references


Patch:
diff --git a/conv/decode.go b/conv/decode.go
index cb3474fa..691696b4 100644
--- a/conv/decode.go
+++ b/conv/decode.go
@@ -74,56 +74,56 @@ func ToDuration(s string) (time.Duration, error) {
 }
 
 func ToInt32Array(a []string) ([]int32, error) {
-	arr := make([]int32, len(a))
-	for i := range a {
-		v, err := ToInt32(a[i])
+	var arr []int32
+	for _, s := range a {
+		v, err := ToInt32(s)
 		if err != nil {
 			return nil, err
 		}
 
-		arr[i] = v
+		arr = append(arr, v)
 	}
 
 	return arr, nil
 }
 
 func ToInt64Array(a []string) ([]int64, error) {
-	arr := make([]int64, len(a))
-	for i := range a {
-		v, err := ToInt64(a[i])
+	var arr []int64
+	for _, s := range a {
+		v, err := ToInt64(s)
 		if err != nil {
 			return nil, err
 		}
 
-		arr[i] = v
+		arr = append(arr, v)
 	}
 
 	return arr, nil
 }
 
 func ToFloat32Array(a []string) ([]float32, error) {
-	arr := make([]float32, len(a))
-	for i := range a {
-		v, err := ToFloat32(a[i])
+	var arr []float32
+	for _, s := range a {
+		v, err := ToFloat32(s)
 		if err != nil {
 			return nil, err
 		}
 
-		arr[i] = v
+		arr = append(arr, v)
 	}
 
 	return arr, nil
 }
 
 func ToFloat64Array(a []string) ([]float64, error) {
-	arr := make([]float64, len(a))
-	for i := range a {
-		v, err := ToFloat64(a[i])
+	var arr []float64
+	for _, s := range a {
+		v, err := ToFloat64(s)
 		if err != nil {
 			return nil, err
 		}
 
-		arr[i] = v
+		arr = append(arr, v)
 	}
 
 	return arr, nil
@@ -134,51 +134,51 @@ func ToStringArray(a []string) ([]string, error) {
 }
 
 func ToBytesArray(a []string) ([][]byte, error) {
-	arr := make([][]byte, len(a))
-	for i := range a {
-		arr[i] = []byte(a[i])
+	var arr [][]byte
+	for _, s := range a {
+		arr = append(arr, []byte(s))
 	}
 
 	return arr, nil
 }
 
 func ToTimeArray(a []string) ([]time.Time, error) {
-	arr := make([]time.Time, len(a))
-	for i := range a {
-		v, err := ToTime(a[i])
+	var arr []time.Time
+	for _, s := range a {
+		v, err := ToTime(s)
 		if err != nil {
 			return nil, err
 		}
 
-		arr[i] = v
+		arr = append(arr, v)
 	}
 
 	return arr, nil
 }
 
 func ToBoolArray(a []string) ([]bool, error) {
-	arr := make([]bool, len(a))
-	for i := range a {
-		v, err := ToBool(a[i])
+	var arr []bool
+	for _, s := range a {
+		v, err := ToBool(s)
 		if err != nil {
 			return nil, err
 		}
 
-		arr[i] = v
+		arr = append(arr, v)
 	}
 
 	return arr, nil
 }
 
 func ToUUIDArray(a []string) ([]uuid.UUID, error) {
-	arr := make([]uuid.UUID, len(a))
-	for i := range a {
-		v, err := ToUUID(a[i])
+	var arr []uuid.UUID
+	for _, s := range a {
+		v, err := ToUUID(s)
 		if err != nil {
 			return nil, err
 		}
 
-		arr[i] = v
+		arr = append(arr, v)
 	}
 
 	return arr, nil
diff --git a/examples/go.sum b/examples/go.sum
index 3a91102e..50608a58 100644
--- a/examples/go.sum
+++ b/examples/go.sum
@@ -45,13 +45,14 @@ go.uber.org/atomic v1.9.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=
 go.uber.org/multierr v1.8.0 h1:dg6GjLku4EH+249NNmoIciG9N/jURbDG+pFlTkhzIC8=
 go.uber.org/multierr v1.8.0/go.mod h1:7EAYxJLBy9rStEaz58O2t4Uvip6FSURkq8/ppBp95ak=
 go.uber.org/zap v1.21.0 h1:WefMeulhovoZ2sYXz7st6K0sLj7bBhpiFaud4r4zST8=
-golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4 h1:6zppjxzCulZykYSLyVDYbneBfbaBIQPYMevg0bEwv2s=
+golang.org/x/mod v0.6.0-dev.0.20220106191415-9b9b3d81d5e3 h1:kQgndtyPBW/JIYERgdxfwMYh3AVStj88WQTlNDi2a+o=
 golang.org/x/sync v0.0.0-20220601150217-0de741cfad7f h1:Ax0t5p6N38Ga0dThY21weqDEyz2oklo4IvDkpigvkD8=
 golang.org/x/sync v0.0.0-20220601150217-0de741cfad7f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
 golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a h1:dGzPydgVsqGcTRVwiLJ1jVbufYwmzD3LfVPLKsKg+0k=
 golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/tools v0.1.11 h1:loJ25fNOEhSXfHrpoGj91eCUThwdNX6u24rO1xnNteY=
+golang.org/x/tools v0.1.10 h1:QjFRCZxdOhBJ/UNgnBZLbNV13DlbnK0quyivTnXJM20=
 golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
+golang.org/x/xerrors v0.0.0-20220517211312-f3a8303e98df h1:5Pf6pFKu98ODmgnpvkJ3kFUOQGGLIzLIkbzUHp47618=
 gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
 gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo=
 gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
diff --git a/gen/external.go b/gen/external.go
index daa3db4e..98027fc5 100644
--- a/gen/external.go
+++ b/gen/external.go
@@ -32,6 +32,8 @@ func (r *RemoteOptions) setDefaults() {
 	}
 }
 
+var _ jsonschema.ExternalResolver = externalResolver{}
+
 type externalResolver struct {
 	client   *http.Client
 	readFile func(p string) ([]byte, error)
@@ -78,7 +80,7 @@ func (e externalResolver) httpGet(ctx context.Context, u *url.URL) ([]byte, erro
 	return data, nil
 }
 
-func (e externalResolver) Get(ctx context.Context, loc string) (jsonschema.ReferenceResolver, error) {
+func (e externalResolver) Get(ctx context.Context, loc string) ([]byte, error) {
 	u, err := url.Parse(loc)
 	if err != nil {
 		return nil, err
@@ -115,5 +117,5 @@ func (e externalResolver) Get(ctx context.Context, loc string) (jsonschema.Refer
 		data = converted
 	}
 
-	return jsonschema.NewRootResolver(data), nil
+	return data, nil
 }
diff --git a/go.mod b/go.mod
index c92f0d24..fe2544b6 100644
--- a/go.mod
+++ b/go.mod
@@ -15,7 +15,7 @@ require (
 	go.uber.org/multierr v1.8.0
 	go.uber.org/zap v1.21.0
 	golang.org/x/sync v0.0.0-20220601150217-0de741cfad7f
-	golang.org/x/tools v0.1.11
+	golang.org/x/tools v0.1.10
 )
 
 require (
@@ -29,8 +29,9 @@ require (
 	github.com/segmentio/asm v1.2.0 // indirect
 	github.com/valyala/bytebufferpool v1.0.0 // indirect
 	go.uber.org/atomic v1.9.0 // indirect
-	golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4 // indirect
+	golang.org/x/mod v0.6.0-dev.0.20220106191415-9b9b3d81d5e3 // indirect
 	golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a // indirect
+	golang.org/x/xerrors v0.0.0-20220517211312-f3a8303e98df // indirect
 	gopkg.in/yaml.v2 v2.4.0 // indirect
 	gopkg.in/yaml.v3 v3.0.1 // indirect
 )
diff --git a/go.sum b/go.sum
index 4262ba9d..62f4acc4 100644
--- a/go.sum
+++ b/go.sum
@@ -68,8 +68,8 @@ golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8U
 golang.org/x/crypto v0.0.0-20220214200702-86341886e292/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
 golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
 golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
-golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4 h1:6zppjxzCulZykYSLyVDYbneBfbaBIQPYMevg0bEwv2s=
-golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
+golang.org/x/mod v0.6.0-dev.0.20220106191415-9b9b3d81d5e3 h1:kQgndtyPBW/JIYERgdxfwMYh3AVStj88WQTlNDi2a+o=
+golang.org/x/mod v0.6.0-dev.0.20220106191415-9b9b3d81d5e3/go.mod h1:3p9vT2HGsQu2K1YbXdKPJLVgG5VJdoTa1poYQBtP1AY=
 golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
 golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
 golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
@@ -101,12 +101,14 @@ golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGm
 golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
 golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
 golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
-golang.org/x/tools v0.1.11 h1:loJ25fNOEhSXfHrpoGj91eCUThwdNX6u24rO1xnNteY=
-golang.org/x/tools v0.1.11/go.mod h1:SgwaegtQh8clINPpECJMqnxLv9I09HLqnW3RMqW0CA4=
+golang.org/x/tools v0.1.10 h1:QjFRCZxdOhBJ/UNgnBZLbNV13DlbnK0quyivTnXJM20=
+golang.org/x/tools v0.1.10/go.mod h1:Uh6Zz+xoGYZom868N8YTex3t7RhtHDBrE8Gzo9bV56E=
 golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
 golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
 golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
 golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
+golang.org/x/xerrors v0.0.0-20220517211312-f3a8303e98df h1:5Pf6pFKu98ODmgnpvkJ3kFUOQGGLIzLIkbzUHp47618=
+golang.org/x/xerrors v0.0.0-20220517211312-f3a8303e98df/go.mod h1:K8+ghG5WaK9qNqU5K3HdILfMLy1f3aNYFI/wnl100a8=
 gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
 gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
 gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo=
diff --git a/jsonschema/external.go b/jsonschema/external.go
index f0b874a4..a7aab772 100644
--- a/jsonschema/external.go
+++ b/jsonschema/external.go
@@ -8,12 +8,15 @@ import (
 
 // ExternalResolver resolves external links.
 type ExternalResolver interface {
-	Get(ctx context.Context, loc string) (ReferenceResolver, error)
+	Get(ctx context.Context, loc string) ([]byte, error)
 }
 
-// noExternal is ExternalResolver that always returns error.
-type noExternal struct{}
+var _ ExternalResolver = NoExternal{}
 
-func (n noExternal) Get(context.Context, string) (ReferenceResolver, error) {
+// NoExternal is ExternalResolver that always returns error.
+type NoExternal struct{}
+
+// Get implements ExternalResolver.
+func (n NoExternal) Get(context.Context, string) ([]byte, error) {
 	return nil, errors.New("external references are disabled")
 }
diff --git a/jsonschema/parser_settings.go b/jsonschema/parser_settings.go
index 735413e2..7ff05c55 100644
--- a/jsonschema/parser_settings.go
+++ b/jsonschema/parser_settings.go
@@ -30,7 +30,7 @@ func (nopResolver) ResolveReference(ref string) (*RawSchema, error) {
 
 func (s *Settings) setDefaults() {
 	if s.External == nil {
-		s.External = noExternal{}
+		s.External = NoExternal{}
 	}
 	if s.Resolver == nil {
 		s.Resolver = nopResolver{}
diff --git a/jsonschema/resolve.go b/jsonschema/resolve.go
index aa926240..4a5abf34 100644
--- a/jsonschema/resolve.go
+++ b/jsonschema/resolve.go
@@ -82,10 +82,12 @@ func (p *Parser) getResolver(ctx *resolveCtx) (ReferenceResolver, error) {
 		return r, nil
 	}
 
-	r, err := p.external.Get(context.TODO(), loc)
+	root, err := p.external.Get(context.TODO(), loc)
 	if err != nil {
 		return nil, errors.Wrapf(err, "external %q", loc)
 	}
+	r = NewRootResolver(root)
+	p.schemas[loc] = r
 
 	return r, nil
 }
diff --git a/jsonschema/resolver.go b/jsonschema/resolver.go
index 732f176e..f1676efe 100644
--- a/jsonschema/resolver.go
+++ b/jsonschema/resolver.go
@@ -20,7 +20,7 @@ func NewRootResolver(root []byte) *RootResolver {
 }
 
 // ResolveReference implements ReferenceResolver.
-func (r RootResolver) ResolveReference(ref string) (rawSchema *RawSchema, err error) {
+func (r *RootResolver) ResolveReference(ref string) (rawSchema *RawSchema, err error) {
 	ref = strings.TrimSpace(ref)
 
 	buf, err := jsonpointer.Resolve(ref, r.root)
diff --git a/openapi/parser/parse_components.go b/openapi/parser/parse_components.go
index d94a7bd4..1e920f33 100644
--- a/openapi/parser/parse_components.go
+++ b/openapi/parser/parse_components.go
@@ -2,6 +2,7 @@ package parser
 
 import (
 	"github.com/go-faster/errors"
+
 	"github.com/ogen-go/ogen"
 	"github.com/ogen-go/ogen/jsonschema"
 	"github.com/ogen-go/ogen/openapi"
@@ -17,7 +18,7 @@ func (p *parser) parseComponents(c *ogen.Components) (*openapi.Components, error
 
 	for name := range c.Parameters {
 		ref := "#/components/parameters/" + name
-		pp, err := p.resolveParameter(ref, resolveCtx{})
+		pp, err := p.resolveParameter(ref, newResolveCtx())
 		if err != nil {
 			return nil, errors.Wrapf(err, "parameters: %q", name)
 		}
@@ -37,7 +38,7 @@ func (p *parser) parseComponents(c *ogen.Components) (*openapi.Components, error
 
 	for name := range c.RequestBodies {
 		ref := "#/components/requestBodies/" + name
-		b, err := p.resolveRequestBody(ref, resolveCtx{})
+		b, err := p.resolveRequestBody(ref, newResolveCtx())
 		if err != nil {
 			return nil, errors.Wrapf(err, "requestBodies: %q", name)
 		}
@@ -47,7 +48,7 @@ func (p *parser) parseComponents(c *ogen.Components) (*openapi.Components, error
 
 	for name := range c.Responses {
 		ref := "#/components/responses/" + name
-		r, err := p.resolveResponse(ref, resolveCtx{})
+		r, err := p.resolveResponse(ref, newResolveCtx())
 		if err != nil {
 			return nil, errors.Wrapf(err, "responses: %q", name)
 		}
diff --git a/openapi/parser/parse_example.go b/openapi/parser/parse_example.go
index ad7c98e5..c625c2ec 100644
--- a/openapi/parser/parse_example.go
+++ b/openapi/parser/parse_example.go
@@ -2,11 +2,12 @@ package parser
 
 import (
 	"github.com/go-faster/errors"
+
 	"github.com/ogen-go/ogen"
 	"github.com/ogen-go/ogen/openapi"
 )
 
-func (p *parser) parseExample(e *ogen.Example, ctx resolveCtx) (*openapi.Example, error) {
+func (p *parser) parseExample(e *ogen.Example, ctx *resolveCtx) (*openapi.Example, error) {
 	if e == nil {
 		return nil, nil
 	}
diff --git a/openapi/parser/parse_header.go b/openapi/parser/parse_header.go
index f5876351..62dab34b 100644
--- a/openapi/parser/parse_header.go
+++ b/openapi/parser/parse_header.go
@@ -7,14 +7,14 @@ import (
 	"github.com/ogen-go/ogen/openapi"
 )
 
-func (p *parser) parseHeaders(headers map[string]*ogen.Header) (_ map[string]*openapi.Header, err error) {
+func (p *parser) parseHeaders(headers map[string]*ogen.Header, ctx *resolveCtx) (_ map[string]*openapi.Header, err error) {
 	if len(headers) == 0 {
 		return nil, nil
 	}
 
 	result := make(map[string]*openapi.Header, len(headers))
 	for name, m := range headers {
-		result[name], err = p.parseHeader(name, m, resolveCtx{})
+		result[name], err = p.parseHeader(name, m, ctx)
 		if err != nil {
 			return nil, errors.Wrap(err, name)
 		}
@@ -23,7 +23,7 @@ func (p *parser) parseHeaders(headers map[string]*ogen.Header) (_ map[string]*op
 	return result, nil
 }
 
-func (p *parser) parseHeader(name string, header *ogen.Header, ctx resolveCtx) (*openapi.Header, error) {
+func (p *parser) parseHeader(name string, header *ogen.Header, ctx *resolveCtx) (*openapi.Header, error) {
 	if header == nil {
 		return nil, errors.New("header object is empty or null")
 	}
@@ -50,12 +50,12 @@ func (p *parser) parseHeader(name string, header *ogen.Header, ctx resolveCtx) (
 		return nil, errors.New("header MUST contain either a schema property, or a content property")
 	}
 
-	schema, err := p.schemaParser.Parse(header.Schema.ToJSONSchema())
+	schema, err := p.parseSchema(header.Schema, ctx)
 	if err != nil {
 		return nil, errors.Wrap(err, "schema")
 	}
 
-	content, err := p.parseParameterContent(header.Content)
+	content, err := p.parseParameterContent(header.Content, ctx)
 	if err != nil {
 		return nil, errors.Wrap(err, "content")
 	}
diff --git a/openapi/parser/parse_mediatype.go b/openapi/parser/parse_mediatype.go
index 3922dae0..e59ff60f 100644
--- a/openapi/parser/parse_mediatype.go
+++ b/openapi/parser/parse_mediatype.go
@@ -8,14 +8,14 @@ import (
 	"github.com/ogen-go/ogen/openapi"
 )
 
-func (p *parser) parseContent(content map[string]ogen.Media) (_ map[string]*openapi.MediaType, err error) {
+func (p *parser) parseContent(content map[string]ogen.Media, ctx *resolveCtx) (_ map[string]*openapi.MediaType, err error) {
 	if len(content) == 0 {
 		return nil, nil
 	}
 
 	result := make(map[string]*openapi.MediaType, len(content))
 	for name, m := range content {
-		result[name], err = p.parseMediaType(m)
+		result[name], err = p.parseMediaType(m, ctx)
 		if err != nil {
 			return nil, errors.Wrap(err, name)
 		}
@@ -24,7 +24,7 @@ func (p *parser) parseContent(content map[string]ogen.Media) (_ map[string]*open
 	return result, nil
 }
 
-func (p *parser) parseParameterContent(content map[string]ogen.Media) (*openapi.ParameterContent, error) {
+func (p *parser) parseParameterContent(content map[string]ogen.Media, ctx *resolveCtx) (*openapi.ParameterContent, error) {
 	if content == nil {
 		return nil, nil
 	}
@@ -33,7 +33,7 @@ func (p *parser) parseParameterContent(content map[string]ogen.Media) (*openapi.
 	}
 
 	for name, m := range content {
-		media, err := p.parseMediaType(m)
+		media, err := p.parseMediaType(m, ctx)
 		if err != nil {
 			return nil, errors.Wrap(err, name)
 		}
@@ -45,8 +45,8 @@ func (p *parser) parseParameterContent(content map[string]ogen.Media) (*openapi.
 	panic("unreachable")
 }
 
-func (p *parser) parseMediaType(m ogen.Media) (*openapi.MediaType, error) {
-	s, err := p.schemaParser.Parse(m.Schema.ToJSONSchema())
+func (p *parser) parseMediaType(m ogen.Media, ctx *resolveCtx) (*openapi.MediaType, error) {
+	s, err := p.parseSchema(m.Schema, ctx)
 	if err != nil {
 		return nil, errors.Wrap(err, "schema")
 	}
@@ -71,7 +71,7 @@ func (p *parser) parseMediaType(m ogen.Media) (*openapi.MediaType, error) {
 				Explode:       inferParamExplode(openapi.LocationQuery, e.Explode),
 				AllowReserved: e.AllowReserved,
 			}
-			encoding.Headers, err = p.parseHeaders(e.Headers)
+			encoding.Headers, err = p.parseHeaders(e.Headers, ctx)
 			if err != nil {
 				return errors.Wrap(err, "headers")
 			}
@@ -101,7 +101,7 @@ func (p *parser) parseMediaType(m ogen.Media) (*openapi.MediaType, error) {
 
 	examples := make(map[string]*openapi.Example, len(m.Examples))
 	for name, ex := range m.Examples {
-		examples[name], err = p.parseExample(ex, resolveCtx{})
+		examples[name], err = p.parseExample(ex, ctx)
 		if err != nil {
 			return nil, errors.Wrapf(err, "examples: %q", name)
 		}
diff --git a/openapi/parser/parse_parameter.go b/openapi/parser/parse_parameter.go
index 5df1ac91..778c3134 100644
--- a/openapi/parser/parse_parameter.go
+++ b/openapi/parser/parse_parameter.go
@@ -27,7 +27,7 @@ func (p *parser) parseParams(params []*ogen.Parameter) ([]*openapi.Parameter, er
 			return nil, errors.Errorf("parameter %d is empty or null", idx)
 		}
 
-		param, err := p.parseParameter(spec, resolveCtx{})
+		param, err := p.parseParameter(spec, newResolveCtx())
 		if err != nil {
 			return nil, errors.Wrapf(err, "parse parameter %q", spec.Name)
 		}
@@ -47,7 +47,7 @@ func (p *parser) parseParams(params []*ogen.Parameter) ([]*openapi.Parameter, er
 	return result, nil
 }
 
-func (p *parser) parseParameter(param *ogen.Parameter, ctx resolveCtx) (*openapi.Parameter, error) {
+func (p *parser) parseParameter(param *ogen.Parameter, ctx *resolveCtx) (*openapi.Parameter, error) {
 	if param == nil {
 		return nil, errors.New("parameter object is empty or null")
 	}
@@ -89,12 +89,12 @@ func (p *parser) parseParameter(param *ogen.Parameter, ctx resolveCtx) (*openapi
 		return nil, errors.New("path parameters must be required")
 	}
 
-	schema, err := p.schemaParser.Parse(param.Schema.ToJSONSchema())
+	schema, err := p.parseSchema(param.Schema, ctx)
 	if err != nil {
 		return nil, errors.Wrap(err, "schema")
 	}
 
-	content, err := p.parseParameterContent(param.Content)
+	content, err := p.parseParameterContent(param.Content, ctx)
 	if err != nil {
 		return nil, errors.Wrap(err, "content")
 	}
@@ -234,5 +234,6 @@ func validateParamStyle(p *openapi.Parameter) error {
 		}
 	}
 
-	return errors.New("invalid schema:style:explode combination")
+	return errors.Errorf("invalid schema:style:explode combination: (%q:%q:%v)",
+		p.Schema.Type, p.Style, p.Explode)
 }
diff --git a/openapi/parser/parse_request_body.go b/openapi/parser/parse_request_body.go
index e68803da..beb2a1d2 100644
--- a/openapi/parser/parse_request_body.go
+++ b/openapi/parser/parse_request_body.go
@@ -7,7 +7,7 @@ import (
 	"github.com/ogen-go/ogen/openapi"
 )
 
-func (p *parser) parseRequestBody(body *ogen.RequestBody, ctx resolveCtx) (*openapi.RequestBody, error) {
+func (p *parser) parseRequestBody(body *ogen.RequestBody, ctx *resolveCtx) (*openapi.RequestBody, error) {
 	if body == nil {
 		return nil, errors.New("requestBody object is empty or null")
 	}
@@ -24,7 +24,7 @@ func (p *parser) parseRequestBody(body *ogen.RequestBody, ctx resolveCtx) (*open
 		return nil, errors.New("content must have at least one entry")
 	}
 
-	content, err := p.parseContent(body.Content)
+	content, err := p.parseContent(body.Content, ctx)
 	if err != nil {
 		return nil, errors.Wrap(err, "content")
 	}
diff --git a/openapi/parser/parse_response.go b/openapi/parser/parse_response.go
index 6c47d985..d6cfd2dc 100644
--- a/openapi/parser/parse_response.go
+++ b/openapi/parser/parse_response.go
@@ -9,12 +9,12 @@ import (
 	"github.com/ogen-go/ogen/openapi"
 )
 
-func (p *parser) parseStatus(status string, response *ogen.Response) (*openapi.Response, error) {
+func (p *parser) parseStatus(status string, response *ogen.Response, ctx *resolveCtx) (*openapi.Response, error) {
 	if err := validateStatusCode(status); err != nil {
 		return nil, err
 	}
 
-	resp, err := p.parseResponse(response, resolveCtx{})
+	resp, err := p.parseResponse(response, ctx)
 	if err != nil {
 		return nil, err
 	}
@@ -29,7 +29,7 @@ func (p *parser) parseResponses(responses ogen.Responses) (_ map[string]*openapi
 
 	result := make(map[string]*openapi.Response, len(responses))
 	for status, response := range responses {
-		result[status], err = p.parseStatus(status, response)
+		result[status], err = p.parseStatus(status, response, newResolveCtx())
 		if err != nil {
 			return nil, errors.Wrap(err, status)
 		}
@@ -38,7 +38,7 @@ func (p *parser) parseResponses(responses ogen.Responses) (_ map[string]*openapi
 	return result, nil
 }
 
-func (p *parser) parseResponse(resp *ogen.Response, ctx resolveCtx) (*openapi.Response, error) {
+func (p *parser) parseResponse(resp *ogen.Response, ctx *resolveCtx) (*openapi.Response, error) {
 	if resp == nil {
 		return nil, errors.New("response object is empty or null")
 	}
@@ -51,12 +51,12 @@ func (p *parser) parseResponse(resp *ogen.Response, ctx resolveCtx) (*openapi.Re
 		return resp, nil
 	}
 
-	content, err := p.parseContent(resp.Content)
+	content, err := p.parseContent(resp.Content, ctx)
 	if err != nil {
 		return nil, errors.Wrap(err, "content")
 	}
 
-	headers, err := p.parseHeaders(resp.Headers)
+	headers, err := p.parseHeaders(resp.Headers, ctx)
 	if err != nil {
 		return nil, errors.Wrap(err, "headers")
 	}
diff --git a/openapi/parser/parse_schema.go b/openapi/parser/parse_schema.go
new file mode 100644
index 00000000..4f987bfe
--- /dev/null
+++ b/openapi/parser/parse_schema.go
@@ -0,0 +1,29 @@
+package parser
+
+import (
+	"net/url"
+	"strings"
+
+	"github.com/go-faster/errors"
+
+	"github.com/ogen-go/ogen"
+	"github.com/ogen-go/ogen/jsonschema"
+)
+
+func (p *parser) parseSchema(schema *ogen.Schema, ctx *resolveCtx) (*jsonschema.Schema, 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)
+}
diff --git a/openapi/parser/parse_security.go b/openapi/parser/parse_security.go
index 8a7cf161..b8f531ac 100644
--- a/openapi/parser/parse_security.go
+++ b/openapi/parser/parse_security.go
@@ -7,7 +7,7 @@ import (
 	"github.com/ogen-go/ogen/openapi"
 )
 
-func (p *parser) parseSecuritySchema(s *ogen.SecuritySchema, ctx resolveCtx) (*ogen.SecuritySchema, error) {
+func (p *parser) parseSecuritySchema(s *ogen.SecuritySchema, ctx *resolveCtx) (*ogen.SecuritySchema, error) {
 	if s == nil {
 		return nil, errors.New("securitySchema object is empty or null")
 	}
@@ -55,7 +55,7 @@ func (p *parser) parseSecurityRequirements(requirements ogen.SecurityRequirement
 				return nil, errors.Errorf("unknown security schema %q", requirementName)
 			}
 
-			spec, err := p.parseSecuritySchema(v, resolveCtx{})
+			spec, err := p.parseSecuritySchema(v, newResolveCtx())
 			if err != nil {
 				return nil, errors.Wrapf(err, "resolve %q", requirementName)
 			}
diff --git a/openapi/parser/parser.go b/openapi/parser/parser.go
index 90f07728..8a90e192 100644
--- a/openapi/parser/parser.go
+++ b/openapi/parser/parser.go
@@ -23,6 +23,9 @@ type parser struct {
 		securitySchemes map[string]*ogen.SecuritySchema
 	}
 
+	external jsonschema.ExternalResolver
+	schemas  map[string][]byte
+
 	schemaParser *jsonschema.Parser
 }
 
@@ -47,12 +50,13 @@ func Parse(spec *ogen.Spec, s Settings) (*openapi.API, error) {
 			examples:        map[string]*openapi.Example{},
 			securitySchemes: map[string]*ogen.SecuritySchema{},
 		},
+		external: s.External,
+		schemas: map[string][]byte{
+			"": spec.Raw,
+		},
 		schemaParser: jsonschema.NewParser(jsonschema.Settings{
-			External: s.External,
-			Resolver: componentsResolver{
-				components: spec.Components.Schemas,
-				root:       jsonschema.NewRootResolver(spec.Raw),
-			},
+			External:   s.External,
+			Resolver:   jsonschema.NewRootResolver(spec.Raw),
 			InferTypes: s.InferTypes,
 		}),
 	}
@@ -138,7 +142,7 @@ func (p *parser) parseOp(path, httpMethod string, spec ogen.Operation, itemParam
 	}
 
 	if spec.RequestBody != nil {
-		op.RequestBody, err = p.parseRequestBody(spec.RequestBody, resolveCtx{})
+		op.RequestBody, err = p.parseRequestBody(spec.RequestBody, newResolveCtx())
 		if err != nil {
 			return nil, errors.Wrap(err, "requestBody")
 		}
diff --git a/openapi/parser/ref.go b/openapi/parser/ref.go
index 8ae92cf5..d7add5d8 100644
--- a/openapi/parser/ref.go
+++ b/openapi/parser/ref.go
@@ -1,35 +1,144 @@
 package parser
 
 import (
+	"context"
+	"encoding/json"
+	"net/url"
 	"strings"
 
 	"github.com/go-faster/errors"
 
 	"github.com/ogen-go/ogen"
+	"github.com/ogen-go/ogen/jsonpointer"
 	"github.com/ogen-go/ogen/openapi"
 )
 
-type resolveCtx map[string]struct{}
+type refKey struct {
+	loc string
+	ref string
+}
 
-func (p *parser) resolveRequestBody(ref string, ctx resolveCtx) (*openapi.RequestBody, error) {
-	const prefix = "#/components/requestBodies/"
-	if !strings.HasPrefix(ref, prefix) {
-		return nil, errors.New("invalid requestBody reference")
+func (r *refKey) fromURL(u *url.URL) {
+	{
+		// Make copy.
+		u2 := *u
+		u2.Fragment = ""
+		r.loc = u2.String()
 	}
+	r.ref = "#" + u.Fragment
+}
 
-	if r, ok := p.refs.requestBodies[ref]; ok {
+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{}
+}
+
+func newResolveCtx() *resolveCtx {
+	return &resolveCtx{
+		refs: map[refKey]struct{}{},
+	}
+}
+
+func (r *resolveCtx) add(ref string) (key refKey, _ error) {
+	u, err := url.Parse(ref)
+	if err != nil {
+		return refKey{}, err
+	}
+	key.fromURL(u)
+
+	if _, ok := r.refs[key]; ok {
+		return key, errors.New("infinite recursion")
+	}
+	r.refs[key] = struct{}{}
+	if key.loc != "" {
+		r.locstack = append(r.locstack, key.loc)
+	}
+	return key, nil
+}
+
+func (r *resolveCtx) delete(key refKey) {
+	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) getSchema(ctx *resolveCtx) ([]byte, error) {
+	loc := ctx.lastLoc()
+
+	r, ok := p.schemas[loc]
+	if ok {
 		return r, nil
 	}
 
-	if _, ok := ctx[ref]; ok {
-		return nil, errors.New("infinite recursion")
+	r, err := p.external.Get(context.TODO(), loc)
+	if err != nil {
+		return nil, errors.Wrapf(err, "external %q", loc)
+	}
+	p.schemas[loc] = r
+
+	return r, nil
+}
+
+func (p *parser) resolve(key refKey, ctx *resolveCtx, to interface{}) error {
+	schema, err := p.getSchema(ctx)
+	if err != nil {
+		return err
+	}
+
+	data, err := jsonpointer.Resolve(key.ref, schema)
+	if err != nil {
+		return err
+	}
+
+	return json.Unmarshal(data, to)
+}
+
+func (p *parser) resolveRequestBody(ref string, ctx *resolveCtx) (*openapi.RequestBody, error) {
+	const prefix = "#/components/requestBodies/"
+
+	if r, ok := p.refs.requestBodies[ref]; ok {
+		return r, nil
 	}
-	ctx[ref] = struct{}{}
 
-	name := strings.TrimPrefix(ref, prefix)
-	component, found := p.spec.Components.RequestBodies[name]
-	if !found {
-		return nil, errors.New("component by reference not found")
+	key, err := ctx.add(ref)
+	if err != nil {
+		return nil, err
+	}
+	defer func() {
+		ctx.delete(key)
+	}()
+
+	var component *ogen.RequestBody
+	if key.loc == "" && ctx.lastLoc() == "" {
+		name := strings.TrimPrefix(ref, prefix)
+		c, found := p.spec.Components.RequestBodies[name]
+		if !found {
+			return nil, errors.New("component by reference not found")
+		}
+		component = c
+	} else {
+		if err := p.resolve(key, ctx, &component); err != nil {
+			return nil, err
+		}
 	}
 
 	r, err := p.parseRequestBody(component, ctx)
@@ -42,25 +151,33 @@ func (p *parser) resolveRequestBody(ref string, ctx resolveCtx) (*openapi.Reques
 	return r, nil
 }
 
-func (p *parser) resolveResponse(ref string, ctx resolveCtx) (*openapi.Response, error) {
+func (p *parser) resolveResponse(ref string, ctx *resolveCtx) (*openapi.Response, error) {
 	const prefix = "#/components/responses/"
-	if !strings.HasPrefix(ref, prefix) {
-		return nil, errors.New("invalid response reference")
-	}
 
 	if r, ok := p.refs.responses[ref]; ok {
 		return r, nil
 	}
 
-	if _, ok := ctx[ref]; ok {
-		return nil, errors.New("infinite recursion")
+	key, err := ctx.add(ref)
+	if err != nil {
+		return nil, err
 	}
-	ctx[ref] = struct{}{}
-
-	name := strings.TrimPrefix(ref, prefix)
-	component, found := p.spec.Components.Responses[name]
-	if !found {
-		return nil, errors.New("component by reference not found")
+	defer func() {
+		ctx.delete(key)
+	}()
+
+	var component *ogen.Response
+	if key.loc == "" && ctx.lastLoc() == "" {
+		name := strings.TrimPrefix(ref, prefix)
+		c, found := p.spec.Components.Responses[name]
+		if !found {
+			return nil, errors.New("component by reference not found")
+		}
+		component = c
+	} else {
+		if err := p.resolve(key, ctx, &component); err != nil {
+			return nil, err
+		}
 	}
 
 	r, err := p.parseResponse(component, ctx)
@@ -73,25 +190,33 @@ 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 *resolveCtx) (*openapi.Parameter, error) {
 	const prefix = "#/components/parameters/"
-	if !strings.HasPrefix(ref, prefix) {
-		return nil, errors.New("invalid parameter reference")
-	}
 
 	if param, ok := p.refs.parameters[ref]; ok {
 		return param, nil
 	}
 
-	if _, ok := ctx[ref]; ok {
-		return nil, errors.New("infinite recursion")
+	key, err := ctx.add(ref)
+	if err != nil {
+		return nil, err
 	}
-	ctx[ref] = struct{}{}
-
-	name := strings.TrimPrefix(ref, prefix)
-	component, found := p.spec.Components.Parameters[name]
-	if !found {
-		return nil, errors.New("component by reference not found")
+	defer func() {
+		ctx.delete(key)
+	}()
+
+	var component *ogen.Parameter
+	if key.loc == "" && ctx.lastLoc() == "" {
+		name := strings.TrimPrefix(ref, prefix)
+		c, found := p.spec.Components.Parameters[name]
+		if !found {
+			return nil, errors.New("component by reference not found")
+		}
+		component = c
+	} else {
+		if err := p.resolve(key, ctx, &component); err != nil {
+			return nil, err
+		}
 	}
 
 	param, err := p.parseParameter(component, ctx)
@@ -104,56 +229,72 @@ func (p *parser) resolveParameter(ref string, ctx resolveCtx) (*openapi.Paramete
 	return param, nil
 }
 
-func (p *parser) resolveHeader(headerName, ref string, ctx resolveCtx) (*openapi.Header, error) {
+func (p *parser) resolveHeader(headerName, ref string, ctx *resolveCtx) (*openapi.Header, error) {
 	const prefix = "#/components/headers/"
-	if !strings.HasPrefix(ref, prefix) {
-		return nil, errors.New("invalid header reference")
-	}
 
-	if param, ok := p.refs.headers[ref]; ok {
-		return param, nil
+	if header, ok := p.refs.headers[ref]; ok {
+		return header, nil
 	}
 
-	if _, ok := ctx[ref]; ok {
-		return nil, errors.New("infinite recursion")
+	key, err := ctx.add(ref)
+	if err != nil {
+		return nil, err
 	}
-	ctx[ref] = struct{}{}
-
-	name := strings.TrimPrefix(ref, prefix)
-	component, found := p.spec.Components.Headers[name]
-	if !found {
-		return nil, errors.New("component by reference not found")
+	defer func() {
+		ctx.delete(key)
+	}()
+
+	var component *ogen.Header
+	if key.loc == "" && ctx.lastLoc() == "" {
+		name := strings.TrimPrefix(ref, prefix)
+		c, found := p.spec.Components.Headers[name]
+		if !found {
+			return nil, errors.New("component by reference not found")
+		}
+		component = c
+	} else {
+		if err := p.resolve(key, ctx, &component); err != nil {
+			return nil, err
+		}
 	}
 
-	param, err := p.parseHeader(headerName, component, ctx)
+	header, err := p.parseHeader(headerName, component, ctx)
 	if err != nil {
 		return nil, err
 	}
 
-	param.Ref = ref
-	p.refs.headers[ref] = param
-	return param, nil
+	header.Ref = ref
+	p.refs.headers[ref] = header
+	return header, nil
 }
 
-func (p *parser) resolveExample(ref string, ctx resolveCtx) (*openapi.Example, error) {
+func (p *parser) resolveExample(ref string, ctx *resolveCtx) (*openapi.Example, error) {
 	const prefix = "#/components/examples/"
-	if !strings.HasPrefix(ref, prefix) {
-		return nil, errors.New("invalid example reference")
-	}
 
-	if param, ok := p.refs.examples[ref]; ok {
-		return param, nil
+	if ex, ok := p.refs.examples[ref]; ok {
+		return ex, nil
 	}
 
-	if _, ok := ctx[ref]; ok {
-		return nil, errors.New("infinite recursion")
+	key, err := ctx.add(ref)
+	if err != nil {
+		return nil, err
 	}
-	ctx[ref] = struct{}{}
-
-	name := strings.TrimPrefix(ref, prefix)
-	component, found := p.spec.Components.Examples[name]
-	if !found {
-		return nil, errors.New("component by reference not found")
+	defer func() {
+		ctx.delete(key)
+	}()
+
+	var component *ogen.Example
+	if key.loc == "" && ctx.lastLoc() == "" {
+		name := strings.TrimPrefix(ref, prefix)
+		c, found := p.spec.Components.Examples[name]
+		if !found {
+			return nil, errors.New("component by reference not found")
+		}
+		component = c
+	} else {
+		if err := p.resolve(key, ctx, &component); err != nil {
+			return nil, err
+		}
 	}
 
 	ex, err := p.parseExample(component, ctx)
@@ -166,25 +307,33 @@ func (p *parser) resolveExample(ref string, ctx resolveCtx) (*openapi.Example, e
 	return ex, nil
 }
 
-func (p *parser) resolveSecuritySchema(ref string, ctx resolveCtx) (*ogen.SecuritySchema, error) {
+func (p *parser) resolveSecuritySchema(ref string, ctx *resolveCtx) (*ogen.SecuritySchema, error) {
 	const prefix = "#/components/securitySchemes/"
-	if !strings.HasPrefix(ref, prefix) {
-		return nil, errors.New("invalid securitySchema reference")
-	}
 
-	if param, ok := p.refs.securitySchemes[ref]; ok {
-		return param, nil
+	if r, ok := p.refs.securitySchemes[ref]; ok {
+		return r, nil
 	}
 
-	if _, ok := ctx[ref]; ok {
-		return nil, errors.New("infinite recursion")
+	key, err := ctx.add(ref)
+	if err != nil {
+		return nil, err
 	}
-	ctx[ref] = struct{}{}
-
-	name := strings.TrimPrefix(ref, prefix)
-	component, found := p.spec.Components.SecuritySchemes[name]
-	if !found {
-		return nil, errors.New("component by reference not found")
+	defer func() {
+		ctx.delete(key)
+	}()
+
+	var component *ogen.SecuritySchema
+	if key.loc == "" && ctx.lastLoc() == "" {
+		name := strings.TrimPrefix(ref, prefix)
+		c, found := p.spec.Components.SecuritySchemes[name]
+		if !found {
+			return nil, errors.New("component by reference not found")
+		}
+		component = c
+	} else {
+		if err := p.resolve(key, ctx, &component); err != nil {
+			return nil, err
+		}
 	}
 
 	p.refs.securitySchemes[ref] = component
diff --git a/openapi/parser/settings.go b/openapi/parser/settings.go
index 18377eae..fd887523 100644
--- a/openapi/parser/settings.go
+++ b/openapi/parser/settings.go
@@ -19,3 +19,9 @@ type Settings struct {
 	// In that case schemaParser will handle that schema as "array" schema, because it has "items" field.
 	InferTypes bool
 }
+
+func (s *Settings) setDefaults() {
+	if s.External == nil {
+		s.External = jsonschema.NoExternal{}
+	}
+}


Test patch:
diff --git a/jsonschema/resolve_test.go b/jsonschema/resolve_test.go
index fb0e9ee0..1274ab3f 100644
--- a/jsonschema/resolve_test.go
+++ b/jsonschema/resolve_test.go
@@ -5,17 +5,35 @@ import (
 	"testing"
 
 	"github.com/go-faster/errors"
+	"github.com/go-faster/jx"
 	"github.com/stretchr/testify/require"
+
+	"github.com/ogen-go/ogen/json"
 )
 
 type external map[string]components
 
-func (e external) Get(_ context.Context, loc string) (ReferenceResolver, error) {
+func (e external) Get(_ context.Context, loc string) ([]byte, error) {
 	r, ok := e[loc]
 	if !ok {
 		return nil, errors.Errorf("unexpected location %q", loc)
 	}
-	return r, nil
+
+	data, err := json.Marshal(r)
+	if err != nil {
+		return nil, err
+	}
+
+	enc := jx.GetEncoder()
+	enc.Obj(func(e *jx.Encoder) {
+		enc.FieldStart("components")
+		enc.Obj(func(e *jx.Encoder) {
+			enc.FieldStart("schemas")
+			enc.Raw(data)
+		})
+	})
+
+	return enc.Bytes(), nil
 }
 
 func TestExternalReference(t *testing.T) {
diff --git a/openapi/parser/ref_test.go b/openapi/parser/ref_test.go
new file mode 100644
index 00000000..d827491a
--- /dev/null
+++ b/openapi/parser/ref_test.go
@@ -0,0 +1,266 @@
+package parser
+
+import (
+	"context"
+	"encoding/json"
+	"testing"
+
+	"github.com/go-faster/errors"
+	"github.com/stretchr/testify/require"
+
+	"github.com/ogen-go/ogen"
+	"github.com/ogen-go/ogen/jsonschema"
+	"github.com/ogen-go/ogen/openapi"
+)
+
+type external map[string]interface{}
+
+func (e external) Get(_ context.Context, loc string) ([]byte, error) {
+	r, ok := e[loc]
+	if !ok {
+		return nil, errors.Errorf("unexpected location %q", loc)
+	}
+	return json.Marshal(r)
+}
+
+func TestExternalReference(t *testing.T) {
+	exampleValue := json.RawMessage(`"value"`)
+
+	root := &ogen.Spec{
+		Paths: map[string]*ogen.PathItem{
+			"/get": {
+				Get: &ogen.Operation{
+					OperationID: "testGet",
+					Description: "operation description",
+					Parameters: []*ogen.Parameter{
+						{Ref: "#/components/parameters/LocalParameter"},
+					},
+					RequestBody: &ogen.RequestBody{
+						Ref: "#/components/requestBodies/LocalRequestBody",
+					},
+					Responses: map[string]*ogen.Response{
+						"200": {
+							Ref: "#/components/responses/LocalResponse",
+						},
+					},
+				},
+			},
+		},
+		Components: &ogen.Components{
+			Responses: map[string]*ogen.Response{
+				"LocalResponse": {
+					Ref: "foo.json#/components/responses/RemoteResponse",
+				},
+			},
+			Parameters: map[string]*ogen.Parameter{
+				"LocalParameter": {
+					Ref: "foo.json#/components/parameters/RemoteParameter",
+				},
+			},
+			Headers: map[string]*ogen.Header{
+				"LocalHeader": {
+					Ref: "foo.json#/components/headers/RemoteHeader",
+				},
+			},
+			Examples: map[string]*ogen.Example{
+				"LocalExample": {
+					Ref: "foo.json#/components/examples/RemoteExample",
+				},
+			},
+			RequestBodies: map[string]*ogen.RequestBody{
+				"LocalRequestBody": {
+					Ref: "foo.json#/components/requestBodies/RemoteRequestBody",
+				},
+			},
+			SecuritySchemes: map[string]*ogen.SecuritySchema{
+				"LocalSecuritySchema": {
+					Ref: "foo.json#/components/securitySchemes/RemoteSecuritySchema",
+				},
+			},
+		},
+	}
+	remote := external{
+		"foo.json": &ogen.Spec{
+			Components: &ogen.Components{
+				Schemas: map[string]*ogen.Schema{
+					"Schema": {
+						Ref: "bar.json#/components/schemas/Schema",
+					},
+				},
+				Responses: map[string]*ogen.Response{
+					"RemoteResponse": {
+						Ref: "response.json#",
+					},
+				},
+				Parameters: map[string]*ogen.Parameter{
+					"RemoteParameter": {
+						Name:  "parameter",
+						In:    "query",
+						Style: "form",
+						Schema: &ogen.Schema{
+							Ref: "#/components/schemas/Schema",
+						},
+					},
+				},
+				Headers: map[string]*ogen.Header{
+					"RemoteHeader": {
+						Style: "simple",
+						Schema: &ogen.Schema{
+							Ref: "bar.json#/components/schemas/Schema",
+						},
+					},
+				},
+				Examples: map[string]*ogen.Example{
+					"RemoteExample": {
+						Value: exampleValue,
+					},
+				},
+				RequestBodies: map[string]*ogen.RequestBody{
+					"RemoteRequestBody": {
+						Description: "request description",
+						Content: map[string]ogen.Media{
+							"application/json": {
+								Schema: &ogen.Schema{
+									Ref: "foo.json#/components/schemas/Schema",
+								},
+								Examples: map[string]*ogen.Example{
+									"ref": {
+										Ref: "#/components/examples/RemoteExample",
+									},
+								},
+							},
+						},
+					},
+				},
+				SecuritySchemes: nil,
+			},
+		},
+		"bar.json": &ogen.Spec{
+			Components: &ogen.Components{
+				Schemas: map[string]*ogen.Schema{
+					"Schema": {
+						Ref: "root.json#",
+					},
+				},
+			},
+		},
+		"response.json": ogen.Response{
+			Description: "response description",
+			Headers: map[string]*ogen.Header{
+				"ResponseHeader": {
+					Ref: "foo.json#/components/headers/RemoteHeader",
+				},
+			},
+			Content: map[string]ogen.Media{
+				"application/json": {
+					Schema: &ogen.Schema{
+						Type: "string",
+					},
+					Examples: map[string]*ogen.Example{
+						"ref": {
+							Ref: "foo.json#/components/examples/RemoteExample",
+						},
+					},
+				},
+			},
+		},
+		"root.json": ogen.Schema{Type: "string"},
+	}
+
+	a := require.New(t)
+	spec, err := Parse(root, Settings{
+		External: remote,
+	})
+	a.NoError(err)
+
+	var (
+		schema = &jsonschema.Schema{
+			Ref:      "root.json#",
+			Type:     "string",
+			Examples: []json.RawMessage{exampleValue},
+		}
+		param = &openapi.Parameter{
+			Ref:     "#/components/parameters/LocalParameter",
+			Name:    "parameter",
+			Schema:  schema,
+			In:      "query",
+			Style:   "form",
+			Explode: true,
+		}
+		requestBody = &openapi.RequestBody{
+			Ref:         "#/components/requestBodies/LocalRequestBody",
+			Description: "request description",
+			Content: map[string]*openapi.MediaType{
+				"application/json": {
+					Schema: schema,
+					Examples: map[string]*openapi.Example{
+						"ref": {
+							Ref:   "#/components/examples/RemoteExample",
+							Value: exampleValue,
+						},
+					},
+					Encoding: map[string]*openapi.Encoding{},
+				},
+			},
+		}
+		response = &openapi.Response{
+			Ref:         "#/components/responses/LocalResponse",
+			Description: "response description",
+			Headers: map[string]*openapi.Header{
+				"ResponseHeader": {
+					Ref:    "foo.json#/components/headers/RemoteHeader",
+					Name:   "ResponseHeader",
+					In:     openapi.LocationHeader,
+					Style:  openapi.HeaderStyleSimple,
+					Schema: schema,
+				},
+			},
+			Content: map[string]*openapi.MediaType{
+				"application/json": {
+					Schema: &jsonschema.Schema{
+						Type:     "string",
+						Examples: []json.RawMessage{exampleValue},
+					},
+					Examples: map[string]*openapi.Example{
+						"ref": {
+							Ref:   "foo.json#/components/examples/RemoteExample",
+							Value: exampleValue,
+						},
+					},
+					Encoding: map[string]*openapi.Encoding{},
+				},
+			},
+		}
+	)
+
+	a.Equal(&openapi.API{
+		Operations: []*openapi.Operation{
+			{
+				OperationID: "testGet",
+				Description: "operation description",
+				HTTPMethod:  "get",
+				Path: openapi.Path{
+					{Raw: "/get"},
+				},
+				Parameters:  []*openapi.Parameter{param},
+				RequestBody: requestBody,
+				Security:    []openapi.SecurityRequirements{},
+				Responses: map[string]*openapi.Response{
+					"200": response,
+				},
+			},
+		},
+		Components: &openapi.Components{
+			Parameters: map[string]*openapi.Parameter{
+				"LocalParameter": param,
+			},
+			Schemas: map[string]*jsonschema.Schema{},
+			RequestBodies: map[string]*openapi.RequestBody{
+				"LocalRequestBody": requestBody,
+			},
+			Responses: map[string]*openapi.Response{
+				"LocalResponse": response,
+			},
+		},
+	}, spec)
+}