Repository: ogen-go/ogen
Pull request #637 (2022-10-20 04:49:51)

fix: print pretty error for external references


Patch:
diff --git a/cmd/ogen/main.go b/cmd/ogen/main.go
index 7a877b2f..dd4d90a2 100644
--- a/cmd/ogen/main.go
+++ b/cmd/ogen/main.go
@@ -92,7 +92,7 @@ func generate(data []byte, packageName, targetDir string, clean bool, opts gen.O
 	return nil
 }
 
-func handleGenerateError(w io.Writer, color bool, specPath string, data []byte, err error) (r bool) {
+func handleGenerateError(w io.Writer, color bool, err error) (r bool) {
 	defer func() {
 		// Add trailing newline to the error message if error is handled.
 		if r {
@@ -100,7 +100,7 @@ func handleGenerateError(w io.Writer, color bool, specPath string, data []byte,
 		}
 	}()
 
-	if location.PrintPrettyError(w, color, specPath, data, err) {
+	if location.PrintPrettyError(w, color, err) {
 		return true
 	}
 
@@ -248,6 +248,11 @@ func run() error {
 	}
 
 	specDir, fileName := filepath.Split(specPath)
+	data, err := os.ReadFile(specPath)
+	if err != nil {
+		return err
+	}
+
 	opts := gen.Options{
 		NoClient:             *noClient,
 		NoServer:             *noServer,
@@ -267,7 +272,7 @@ func run() error {
 		},
 		IgnoreNotImplemented: strings.Split(*debugIgnoreNotImplemented, ","),
 		ContentTypeAliases:   ctAliases,
-		Filename:             fileName,
+		File:                 location.NewFile(fileName, specPath, data),
 		Logger:               logger,
 	}
 	if expr := *skipTestsRegex; expr != "" {
@@ -286,13 +291,8 @@ func run() error {
 		}
 	}
 
-	data, err := os.ReadFile(specPath)
-	if err != nil {
-		return err
-	}
-
 	if err := generate(data, *packageName, *targetDir, *clean, opts); err != nil {
-		if handleGenerateError(os.Stderr, logOptions.Color, fileName, data, err) {
+		if handleGenerateError(os.Stderr, logOptions.Color, err) {
 			return errors.New("generation failed")
 		}
 		return errors.Wrap(err, "generate")
diff --git a/examples/go.mod b/examples/go.mod
index 005db3a1..740790c7 100644
--- a/examples/go.mod
+++ b/examples/go.mod
@@ -8,9 +8,9 @@ require (
 	github.com/google/uuid v1.3.0
 	github.com/ogen-go/ogen v0.0.0
 	github.com/stretchr/testify v1.8.0
-	go.opentelemetry.io/otel v1.11.1
-	go.opentelemetry.io/otel/metric v0.33.0
-	go.opentelemetry.io/otel/trace v1.11.1
+	go.opentelemetry.io/otel v1.11.0
+	go.opentelemetry.io/otel/metric v0.32.3
+	go.opentelemetry.io/otel/trace v1.11.0
 	go.uber.org/multierr v1.8.0
 	go.uber.org/zap v1.23.0
 	golang.org/x/exp v0.0.0-20221006183845-316c7553db56
diff --git a/examples/go.sum b/examples/go.sum
index f0913412..c73dd92a 100644
--- a/examples/go.sum
+++ b/examples/go.sum
@@ -17,7 +17,7 @@ github.com/go-logr/logr v1.2.3 h1:2DntVwHkVopvECVRSlL5PSo9eG+cAkDCuckLubN+rq0=
 github.com/go-logr/logr v1.2.3/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
 github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
 github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
-github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
+github.com/google/go-cmp v0.5.8 h1:e6P7q2lk1O+qJJb4BtCQXlK8vWEO8V1ZeuEdJNOqZyg=
 github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I=
 github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
 github.com/kr/pretty v0.2.1 h1:Fmg33tUaq4/8ym9TJN1x7sLJnHVwhP33CNkpYV/7rwI=
@@ -39,12 +39,12 @@ github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/
 github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
 github.com/stretchr/testify v1.8.0 h1:pSgiaMZlXftHpm5L7V1+rVB+AZJydKsMxsQBIJw4PKk=
 github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
-go.opentelemetry.io/otel v1.11.1 h1:4WLLAmcfkmDk2ukNXJyq3/kiz/3UzCaYq6PskJsaou4=
-go.opentelemetry.io/otel v1.11.1/go.mod h1:1nNhXBbWSD0nsL38H6btgnFN2k4i0sNLHNNMZMSbUGE=
-go.opentelemetry.io/otel/metric v0.33.0 h1:xQAyl7uGEYvrLAiV/09iTJlp1pZnQ9Wl793qbVvED1E=
-go.opentelemetry.io/otel/metric v0.33.0/go.mod h1:QlTYc+EnYNq/M2mNk1qDDMRLpqCOj2f/r5c7Fd5FYaI=
-go.opentelemetry.io/otel/trace v1.11.1 h1:ofxdnzsNrGBYXbP7t7zpUK281+go5rF7dvdIZXF8gdQ=
-go.opentelemetry.io/otel/trace v1.11.1/go.mod h1:f/Q9G7vzk5u91PhbmKbg1Qn0rzH1LJ4vbPHFGkTPtOk=
+go.opentelemetry.io/otel v1.11.0 h1:kfToEGMDq6TrVrJ9Vht84Y8y9enykSZzDDZglV0kIEk=
+go.opentelemetry.io/otel v1.11.0/go.mod h1:H2KtuEphyMvlhZ+F7tg9GRhAOe60moNx61Ex+WmiKkk=
+go.opentelemetry.io/otel/metric v0.32.3 h1:dMpnJYk2KULXr0j8ph6N7+IcuiIQXlPXD4kix9t7L9c=
+go.opentelemetry.io/otel/metric v0.32.3/go.mod h1:pgiGmKohxHyTPHGOff+vrtIH39/R9fiO/WoenUQ3kcc=
+go.opentelemetry.io/otel/trace v1.11.0 h1:20U/Vj42SX+mASlXLmSGBg6jpI1jQtv682lZtTAOVFI=
+go.opentelemetry.io/otel/trace v1.11.0/go.mod h1:nyYjis9jy0gytE9LXGU+/m1sHTKbRY0fX0hulNNDP1U=
 go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=
 go.uber.org/atomic v1.9.0 h1:ECmE8Bn/WFTYwEW/bpKD3M8VtR/zQVbavAoalC1PYyE=
 go.uber.org/atomic v1.9.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=
diff --git a/gen/errors.go b/gen/errors.go
index 8a20b058..b5382be9 100644
--- a/gen/errors.go
+++ b/gen/errors.go
@@ -58,7 +58,7 @@ func (e *ErrUnsupportedContentTypes) Error() string {
 	return fmt.Sprintf("unsupported content types: [%s]", strings.Join(e.ContentTypes, ", "))
 }
 
-func (g *Generator) trySkip(err error, msg string, l locatable) error {
+func (g *Generator) trySkip(err error, msg string, l pos) error {
 	if err == nil {
 		return nil
 	}
@@ -71,7 +71,7 @@ func (g *Generator) trySkip(err error, msg string, l locatable) error {
 		reason = uErr.Error()
 	}
 	g.log.Info(msg,
-		g.zapLocation(l),
+		g.zapPosition(l),
 		zap.String("reason_error", reason),
 	)
 	return nil
diff --git a/gen/gen_contents.go b/gen/gen_contents.go
index 1cd3e8a0..a090dc17 100644
--- a/gen/gen_contents.go
+++ b/gen/gen_contents.go
@@ -374,7 +374,7 @@ func (g *Generator) generateContents(
 				}
 
 				g.log.Info(`Content type is unsupported, set "format" to "binary" to use io.Reader`,
-					g.zapLocation(media),
+					g.zapPosition(media),
 					zap.String("contentType", contentType),
 				)
 				unsupported = append(unsupported, contentType)
diff --git a/gen/gen_headers.go b/gen/gen_headers.go
index 80308a63..105c7454 100644
--- a/gen/gen_headers.go
+++ b/gen/gen_headers.go
@@ -22,7 +22,7 @@ func (g *Generator) generateHeaders(
 		if http.CanonicalHeaderKey(hname) == "Content-Type" {
 			g.log.Warn(
 				"Content-Type is described separately and will be ignored in this section.",
-				g.zapLocation(header),
+				g.zapPosition(header),
 			)
 			continue
 		}
diff --git a/gen/gen_schema.go b/gen/gen_schema.go
index 3801137f..9dc28c9f 100644
--- a/gen/gen_schema.go
+++ b/gen/gen_schema.go
@@ -38,7 +38,7 @@ func saveSchemaTypes(ctx *genctx, gen *schemaGen) error {
 }
 
 func (g *Generator) generateSchema(ctx *genctx, name string, schema *jsonschema.Schema, optional bool) (*ir.Type, error) {
-	gen := newSchemaGen(g.opt.Filename, ctx.lookupRef)
+	gen := newSchemaGen(g.opt.File.Name, ctx.lookupRef)
 	gen.log = g.log.Named("schemagen")
 	gen.fail = g.fail
 
diff --git a/gen/generator.go b/gen/generator.go
index f50970e2..b2d75ad6 100644
--- a/gen/generator.go
+++ b/gen/generator.go
@@ -41,7 +41,7 @@ func NewGenerator(spec *ogen.Spec, opts Options) (*Generator, error) {
 	}
 	api, err := parser.Parse(spec, parser.Settings{
 		External:   external,
-		Filename:   opts.Filename,
+		File:       opts.File,
 		InferTypes: opts.InferSchemaType,
 	})
 	if err != nil {
@@ -108,7 +108,7 @@ func (g *Generator) makeOps(ops []*openapi.Operation) error {
 
 	for _, spec := range ops {
 		routePath := spec.Path.String()
-		log := g.log.With(g.zapLocation(spec))
+		log := g.log.With(g.zapPosition(spec))
 
 		if !g.opt.Filters.accept(spec) {
 			log.Info("Skipping filtered operation")
@@ -167,7 +167,7 @@ func (g *Generator) makeWebhooks(webhooks []openapi.Webhook) error {
 			Name: w.Name,
 		}
 		for _, spec := range w.Operations {
-			log := g.log.With(g.zapLocation(spec))
+			log := g.log.With(g.zapPosition(spec))
 
 			if !g.opt.Filters.accept(spec) {
 				log.Info("Skipping filtered operation")
diff --git a/gen/options.go b/gen/options.go
index 8493be82..401bb670 100644
--- a/gen/options.go
+++ b/gen/options.go
@@ -8,6 +8,7 @@ import (
 	"go.uber.org/zap"
 
 	"github.com/ogen-go/ogen/gen/ir"
+	"github.com/ogen-go/ogen/internal/location"
 	"github.com/ogen-go/ogen/internal/xslices"
 	"github.com/ogen-go/ogen/openapi"
 )
@@ -45,10 +46,10 @@ type Options struct {
 	// ContentTypeAliases contains content type aliases.
 	ContentTypeAliases ContentTypeAliases
 
-	// Filename is a name of the spec file.
+	// File is the file that is being parsed.
 	//
 	// Used for error messages.
-	Filename string
+	File location.File
 	// Logger to use.
 	Logger *zap.Logger
 }
diff --git a/gen/schema_gen.go b/gen/schema_gen.go
index 22c18367..43134eda 100644
--- a/gen/schema_gen.go
+++ b/gen/schema_gen.go
@@ -295,7 +295,7 @@ func (g *schemaGen) generate2(name string, schema *jsonschema.Schema) (ret *ir.T
 		return g.regtype(name, t), nil
 	case jsonschema.Empty:
 		g.log.Info("Type is not defined, using any",
-			g.zapLocation(schema),
+			g.zapPosition(schema),
 			zap.String("name", name),
 		)
 		return g.regtype(name, ir.Any(schema)), nil
diff --git a/gen/utils.go b/gen/utils.go
index 06091ef9..113111f6 100644
--- a/gen/utils.go
+++ b/gen/utils.go
@@ -75,25 +75,25 @@ func statusText(code int) string {
 	return fmt.Sprintf("Code%d", code)
 }
 
-type locatable interface {
-	Location() (location.Location, bool)
+type pos interface {
+	Position() (location.Position, bool)
 }
 
-func zapLocation(filename string, l locatable) zap.Field {
+func zapPosition(filename string, l pos) zap.Field {
 	if l == nil {
 		return zap.Skip()
 	}
-	loc, ok := l.Location()
+	loc, ok := l.Position()
 	if !ok {
 		return zap.Skip()
 	}
 	return zap.String("at", loc.WithFilename(filename))
 }
 
-func (g *Generator) zapLocation(l locatable) zap.Field {
-	return zapLocation(g.opt.Filename, l)
+func (g *Generator) zapPosition(l pos) zap.Field {
+	return zapPosition(g.opt.File.Name, l)
 }
 
-func (g *schemaGen) zapLocation(l locatable) zap.Field {
-	return zapLocation(g.filename, l)
+func (g *schemaGen) zapPosition(l pos) zap.Field {
+	return zapPosition(g.filename, l)
 }
diff --git a/go.mod b/go.mod
index b35d37b5..b4c704c2 100644
--- a/go.mod
+++ b/go.mod
@@ -11,9 +11,9 @@ require (
 	github.com/google/uuid v1.3.0
 	github.com/stretchr/testify v1.8.0
 	github.com/valyala/fasthttp v1.40.0
-	go.opentelemetry.io/otel v1.11.1
+	go.opentelemetry.io/otel v1.11.0
 	go.opentelemetry.io/otel/metric v0.32.3
-	go.opentelemetry.io/otel/trace v1.11.1
+	go.opentelemetry.io/otel/trace v1.11.0
 	go.uber.org/multierr v1.8.0
 	go.uber.org/zap v1.23.0
 	golang.org/x/exp v0.0.0-20221006183845-316c7553db56
diff --git a/go.sum b/go.sum
index 57c44b1e..6dd665bd 100644
--- a/go.sum
+++ b/go.sum
@@ -20,7 +20,7 @@ github.com/go-logr/logr v1.2.3 h1:2DntVwHkVopvECVRSlL5PSo9eG+cAkDCuckLubN+rq0=
 github.com/go-logr/logr v1.2.3/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
 github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
 github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
-github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
+github.com/google/go-cmp v0.5.8 h1:e6P7q2lk1O+qJJb4BtCQXlK8vWEO8V1ZeuEdJNOqZyg=
 github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I=
 github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
 github.com/klauspost/compress v1.15.0/go.mod h1:/3/Vjq9QcHkK5uEr5lBEmyoZ1iFhe47etQ6QUkpK6sk=
@@ -50,12 +50,12 @@ github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyC
 github.com/valyala/fasthttp v1.40.0 h1:CRq/00MfruPGFLTQKY8b+8SfdK60TxNztjRMnH0t1Yc=
 github.com/valyala/fasthttp v1.40.0/go.mod h1:t/G+3rLek+CyY9bnIE+YlMRddxVAAGjhxndDB4i4C0I=
 github.com/valyala/tcplisten v1.0.0/go.mod h1:T0xQ8SeCZGxckz9qRXTfG43PvQ/mcWh7FwZEA7Ioqkc=
-go.opentelemetry.io/otel v1.11.1 h1:4WLLAmcfkmDk2ukNXJyq3/kiz/3UzCaYq6PskJsaou4=
-go.opentelemetry.io/otel v1.11.1/go.mod h1:1nNhXBbWSD0nsL38H6btgnFN2k4i0sNLHNNMZMSbUGE=
+go.opentelemetry.io/otel v1.11.0 h1:kfToEGMDq6TrVrJ9Vht84Y8y9enykSZzDDZglV0kIEk=
+go.opentelemetry.io/otel v1.11.0/go.mod h1:H2KtuEphyMvlhZ+F7tg9GRhAOe60moNx61Ex+WmiKkk=
 go.opentelemetry.io/otel/metric v0.32.3 h1:dMpnJYk2KULXr0j8ph6N7+IcuiIQXlPXD4kix9t7L9c=
 go.opentelemetry.io/otel/metric v0.32.3/go.mod h1:pgiGmKohxHyTPHGOff+vrtIH39/R9fiO/WoenUQ3kcc=
-go.opentelemetry.io/otel/trace v1.11.1 h1:ofxdnzsNrGBYXbP7t7zpUK281+go5rF7dvdIZXF8gdQ=
-go.opentelemetry.io/otel/trace v1.11.1/go.mod h1:f/Q9G7vzk5u91PhbmKbg1Qn0rzH1LJ4vbPHFGkTPtOk=
+go.opentelemetry.io/otel/trace v1.11.0 h1:20U/Vj42SX+mASlXLmSGBg6jpI1jQtv682lZtTAOVFI=
+go.opentelemetry.io/otel/trace v1.11.0/go.mod h1:nyYjis9jy0gytE9LXGU+/m1sHTKbRY0fX0hulNNDP1U=
 go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=
 go.uber.org/atomic v1.9.0 h1:ECmE8Bn/WFTYwEW/bpKD3M8VtR/zQVbavAoalC1PYyE=
 go.uber.org/atomic v1.9.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=
diff --git a/internal/jsonpointer/resolve_ctx.go b/internal/jsonpointer/resolve_ctx.go
index 7d771f81..04b8184d 100644
--- a/internal/jsonpointer/resolve_ctx.go
+++ b/internal/jsonpointer/resolve_ctx.go
@@ -5,6 +5,8 @@ import (
 	"strings"
 
 	"github.com/go-faster/errors"
+
+	"github.com/ogen-go/ogen/internal/location"
 )
 
 // RefKey is JSON reference key.
@@ -36,6 +38,7 @@ type ResolveCtx struct {
 	//
 	// "#/definitions/SchemaProperty" should be resolved against "https://example.com/schema".
 	locstack []string
+	files    []location.File
 	// Store references to detect infinite recursive references.
 	refs       map[RefKey]struct{}
 	depthLimit int
@@ -85,7 +88,7 @@ func (r *ResolveCtx) Key(ref string) (key RefKey, _ error) {
 }
 
 // AddKey adds reference key to context.
-func (r *ResolveCtx) AddKey(key RefKey) error {
+func (r *ResolveCtx) AddKey(key RefKey, file location.File) error {
 	if r.depthLimit <= 0 {
 		return errors.New("depth limit exceeded")
 	}
@@ -97,6 +100,7 @@ func (r *ResolveCtx) AddKey(key RefKey) error {
 
 	if key.Loc != "" {
 		r.locstack = append(r.locstack, key.Loc)
+		r.files = append(r.files, file)
 	}
 	return nil
 }
@@ -118,3 +122,12 @@ func (r *ResolveCtx) LastLoc() string {
 	}
 	return s[len(s)-1]
 }
+
+// File returns last file from stack.
+func (r *ResolveCtx) File() (f location.File) {
+	s := r.files
+	if len(s) == 0 {
+		return f
+	}
+	return s[len(s)-1]
+}
diff --git a/internal/location/error.go b/internal/location/error.go
index 3ea5233c..eae529e9 100644
--- a/internal/location/error.go
+++ b/internal/location/error.go
@@ -2,10 +2,22 @@ package location
 
 import (
 	"fmt"
+	"io"
 
 	"github.com/go-faster/errors"
+	yaml "github.com/go-faster/yamlx"
+	"go.uber.org/multierr"
 )
 
+func firstNonEmpty(strs ...string) string {
+	for _, s := range strs {
+		if s != "" {
+			return s
+		}
+	}
+	return ""
+}
+
 var _ interface {
 	errors.Wrapper
 	errors.Formatter
@@ -15,8 +27,8 @@ var _ interface {
 
 // Error is a wrapper for an error that has a location.
 type Error struct {
-	File string
-	Loc  Location
+	File File
+	Pos  Position
 	Err  error
 }
 
@@ -26,16 +38,16 @@ func (e *Error) Unwrap() error {
 }
 
 func (e *Error) fileName() string {
-	filename := e.File
-	if filename == "" || e.Loc.Line == 0 {
+	filename := firstNonEmpty(e.File.Name, e.File.Source)
+	if filename == "" || e.Pos.Line == 0 {
 		return ""
 	}
 	return filename + ":"
 }
 
 // FormatError implements errors.Formatter.
-func (e *Error) FormatError(p errors.Printer) (next error) {
-	p.Printf("at %s%s", e.fileName(), e.Loc)
+func (e *Error) FormatError(p errors.Printer) error {
+	p.Printf("at %s%s", e.fileName(), e.Pos)
 	return e.Err
 }
 
@@ -46,5 +58,86 @@ func (e *Error) Format(s fmt.State, verb rune) {
 
 // Error implements error.
 func (e *Error) Error() string {
-	return fmt.Sprintf("at %s%s: %s", e.fileName(), e.Loc, e.Err)
+	return fmt.Sprintf("at %s%s: %s", e.fileName(), e.Pos, e.Err)
+}
+
+// PrettyPrint prints the error in a pretty way and returns true if it was printed successfully.
+func (e *Error) PrettyPrint(w io.Writer, color bool) bool {
+	// TODO(tdakkota): make it configurable?
+	const (
+		printLimit   = 5
+		contextLines = 5
+	)
+	var (
+		err      = e.Err
+		filename = firstNonEmpty(e.File.Name, e.File.Source)
+		lines    = e.File.Lines
+
+		write = func(msg string, loc Position, context int) {
+			opts := PrintListingOptions{
+				Filename: filename,
+				Context:  context,
+			}
+			if !color {
+				opts = opts.WithoutColor()
+			}
+			_ = lines.PrintListing(w, msg, loc, opts)
+		}
+	)
+
+	if e, ok := errors.Into[*yaml.SyntaxError](err); ok {
+		loc := Position{
+			Line: e.Line,
+		}
+		write(e.Msg, loc, contextLines)
+		return true
+	}
+
+	if e, ok := errors.Into[*yaml.TypeError](err); ok {
+		printed := 0
+		for _, e := range multierr.Errors(e.Group) {
+			if printed >= printLimit {
+				break
+			}
+			if e, ok := errors.Into[*yaml.UnmarshalError](e); ok && e.Node != nil {
+				loc := Position{
+					Line:   e.Node.Line,
+					Column: e.Node.Column,
+					Node:   e.Node,
+				}
+				write(e.Err.Error(), loc, contextLines)
+				printed++
+			}
+		}
+		// Consider the error as handled if it is printed at least once.
+		return printed > 0
+	}
+
+	var (
+		iterErr = e.Err
+		locErr  = e
+	)
+	for {
+		e, ok := errors.Into[*Error](iterErr)
+		if !ok || e.Pos.Line == 0 {
+			break
+		}
+		locErr = e
+		iterErr = e.Err
+	}
+	if locErr.Pos.Line != 0 {
+		write(locErr.Err.Error(), locErr.Pos, contextLines)
+		return true
+	}
+
+	return false
+}
+
+// PrintPrettyError prints the error in a pretty way and returns true if it was printed successfully.
+func PrintPrettyError(w io.Writer, color bool, err error) bool {
+	v, ok := errors.Into[*Error](err)
+	if !ok {
+		return false
+	}
+	return v.PrettyPrint(w, color)
 }
diff --git a/internal/location/file.go b/internal/location/file.go
new file mode 100644
index 00000000..bbdd8d07
--- /dev/null
+++ b/internal/location/file.go
@@ -0,0 +1,32 @@
+package location
+
+// File is a source file.
+type File struct {
+	// Name is the file name.
+	Name string
+	// Source is the path or URL of the file.
+	Source string
+	// Lines stores newline offsets.
+	Lines Lines
+}
+
+// IsZero returns true if file has zero value.
+func (f File) IsZero() bool {
+	s := struct {
+		Name   string
+		Source string
+		Lines  Lines
+	}(f)
+	// File is not useful if lines is empty.
+	return (s.Name == "" && s.Source == "") || s.Lines.IsZero()
+}
+
+// NewFile creates a new File.
+func NewFile(name, source string, data []byte) File {
+	f := File{
+		Name:   name,
+		Source: source,
+	}
+	f.Lines.Collect(data)
+	return f
+}
diff --git a/internal/location/lines.go b/internal/location/lines.go
index d99e5238..3e31ac9c 100644
--- a/internal/location/lines.go
+++ b/internal/location/lines.go
@@ -2,6 +2,8 @@ package location
 
 import (
 	"bytes"
+
+	"golang.org/x/exp/slices"
 )
 
 // Lines is a sorted slice of newline offsets.
@@ -13,6 +15,15 @@ type Lines struct {
 	lines []int
 }
 
+// IsZero returns true if lines has zero value.
+func (l Lines) IsZero() bool {
+	s := struct {
+		data  []byte
+		lines []int
+	}(l)
+	return s.data == nil && s.lines == nil
+}
+
 // Line returns offset range of the line.
 //
 // NOTE: the line number is 1-based. Returns (-1, -1) if the line is invalid.
@@ -40,7 +51,7 @@ func (l Lines) Line(n int) (start, end int) {
 
 // Collect fills the given slice with the offset of newlines.
 func (l *Lines) Collect(data []byte) {
-	l.data = data
+	l.data = slices.Clone(data)
 	l.lines = l.lines[:0]
 
 	var (
diff --git a/internal/location/location.go b/internal/location/position.go
similarity index 52%
rename from internal/location/location.go
rename to internal/location/position.go
index 14ff750a..c7848ae0 100644
--- a/internal/location/location.go
+++ b/internal/location/position.go
@@ -1,4 +1,3 @@
-// Package location is a helper package for working with locations.
 package location
 
 import (
@@ -8,37 +7,37 @@ import (
 	yaml "github.com/go-faster/yamlx"
 )
 
-// Location is a JSON value location.
-type Location struct {
+// Position is a JSON value position.
+type Position struct {
 	Line, Column int
 	Node         *yaml.Node
 }
 
-// FromNode sets the location of the value from the given node.
-func (l *Location) FromNode(node *yaml.Node) {
-	*l = Location{
+// FromNode sets the position of the value from the given node.
+func (p *Position) FromNode(node *yaml.Node) {
+	*p = Position{
 		Line:   node.Line,
 		Column: node.Column,
 		Node:   node,
 	}
 }
 
-// Key tries to find the child node using given key and returns its location.
-// If such node is not found or parent node is not a mapping, Key returns location of the parent node.
+// Key tries to find the child node using given key and returns its position.
+// If such node is not found or parent node is not a mapping, Key returns position of the parent node.
 //
-// NOTE: child location will point to the key node, not to the value node.
-// Use Field if you want location of the value.
-func (l Location) Key(key string) (loc Location) {
-	n := l.Node
+// NOTE: child position will point to the key node, not to the value node.
+// Use Field if you want position of the value.
+func (p Position) Key(key string) (loc Position) {
+	n := p.Node
 	if n != nil && n.Kind == yaml.DocumentNode {
 		if len(n.Content) < 1 {
-			return l
+			return p
 		}
 		n = n.Content[0]
 	}
 
 	if n == nil || n.Kind != yaml.MappingNode || len(n.Content) < 2 {
-		return l
+		return p
 	}
 
 	children := n.Content
@@ -49,25 +48,25 @@ func (l Location) Key(key string) (loc Location) {
 			return loc
 		}
 	}
-	return l
+	return p
 }
 
-// Field tries to find the child node using given key and returns its location.
-// If such node is not found or parent node is not a mapping, Field returns location of the parent node.
+// Field tries to find the child node using given key and returns its position.
+// If such node is not found or parent node is not a mapping, Field returns position of the parent node.
 //
-// NOTE: child location will point to the value node, not to the key node.
-// Use Key if you want location of the key.
-func (l Location) Field(key string) (loc Location) {
-	n := l.Node
+// NOTE: child position will point to the value node, not to the key node.
+// Use Key if you want position of the key.
+func (p Position) Field(key string) (loc Position) {
+	n := p.Node
 	if n != nil && n.Kind == yaml.DocumentNode {
 		if len(n.Content) < 1 {
-			return l
+			return p
 		}
 		n = n.Content[0]
 	}
 
 	if n == nil || n.Kind != yaml.MappingNode || len(n.Content) < 2 {
-		return l
+		return p
 	}
 
 	children := n.Content
@@ -78,35 +77,35 @@ func (l Location) Field(key string) (loc Location) {
 			return loc
 		}
 	}
-	return l
+	return p
 }
 
-// Index tries to find the child node using given index and returns its location.
-// If such node is not found or parent node is not a sequence, Field returns location of the parent node.
-func (l Location) Index(idx int) (loc Location) {
-	n := l.Node
+// Index tries to find the child node using given index and returns its position.
+// If such node is not found or parent node is not a sequence, Field returns position of the parent node.
+func (p Position) Index(idx int) (loc Position) {
+	n := p.Node
 	if n != nil && n.Kind == yaml.DocumentNode {
 		if len(n.Content) < 1 {
-			return l
+			return p
 		}
 		n = n.Content[0]
 	}
 
 	if n == nil || n.Kind != yaml.SequenceNode {
-		return l
+		return p
 	}
 
 	children := n.Content
 	if idx < 0 || idx >= len(children) {
-		return l
+		return p
 	}
 	loc.FromNode(children[idx])
 	return loc
 }
 
 // String implements fmt.Stringer.
-func (l Location) String() string {
-	line, column, n := l.Line, l.Column, l.Node
+func (p Position) String() string {
+	line, column, n := p.Line, p.Column, p.Node
 	if n != nil {
 		line, column = n.Line, n.Column
 	}
@@ -116,81 +115,76 @@ func (l Location) String() string {
 	return fmt.Sprintf("%d:%d", line, column)
 }
 
-// WithFilename prints the location with the given filename.
+// WithFilename prints the position with the given filename.
 //
-// If filename is empty, the location is printed as is.
-func (l Location) WithFilename(filename string) string {
+// If filename is empty, the position is printed as is.
+func (p Position) WithFilename(filename string) string {
 	if filename != "" {
 		filename += ":"
 	}
-	return filename + l.String()
+	return filename + p.String()
 }
 
-// Locatable is an interface for JSON value location store.
+// Locatable is an interface for JSON value position store.
 type Locatable interface {
-	// SetLocation sets the location of the value.
-	SetLocation(Location)
+	// SetPosition sets the position of the value.
+	SetPosition(Position)
 
-	// Location returns the location of the value if it is set.
-	Location() (Location, bool)
+	// Position returns the position of the value if it is set.
+	Position() (Position, bool)
 }
 
-// Locator stores the Location of a JSON value.
+// Locator stores the Position of a JSON value.
 type Locator struct {
-	location Location
+	position Position
 	set      bool
 }
 
-// SetLocation sets the location of the value.
-func (l *Locator) SetLocation(loc Location) {
-	l.location = loc
+// SetPosition sets the position of the value.
+func (l *Locator) SetPosition(loc Position) {
+	l.position = loc
 	l.set = true
 }
 
-// Location returns the location of the value if it is set.
-func (l Locator) Location() (Location, bool) {
-	return l.location, l.set
+// Position returns the position of the value if it is set.
+func (l Locator) Position() (Position, bool) {
+	return l.position, l.set
 }
 
-// Key tries to find the child node using given key and returns its location.
+// Key tries to find the child node using given key and returns its position.
 //
-// See Key method of Location.
+// See Key method of Position.
 func (l Locator) Key(key string) (loc Locator) {
 	if l.set {
-		loc.SetLocation(l.location.Key(key))
+		loc.SetPosition(l.position.Key(key))
 	}
 	return
 }
 
-// Field tries to find the child node using given key and returns its location.
+// Field tries to find the child node using given key and returns its position.
 //
-// See Field method of Location.
+// See Field method of Position.
 func (l Locator) Field(key string) (loc Locator) {
 	if l.set {
-		loc.SetLocation(l.location.Field(key))
+		loc.SetPosition(l.position.Field(key))
 	}
 	return
 }
 
-// Index tries to find the child node using given index and returns its location.
+// Index tries to find the child node using given index and returns its position.
 //
-// See Index method of Location.
+// See Index method of Position.
 func (l Locator) Index(idx int) (loc Locator) {
 	if l.set {
-		loc.SetLocation(l.location.Index(idx))
+		loc.SetPosition(l.position.Index(idx))
 	}
 	return
 }
 
-// MarshalYAML implements yaml.Marshaler.
-func (l *Locator) MarshalYAML(n *yaml.Node) error {
-	return nil
-}
-
 // UnmarshalYAML implements yaml.Unmarshaler.
 func (l *Locator) UnmarshalYAML(n *yaml.Node) error {
-	var loc Location
+	var loc Position
 	loc.FromNode(n)
-	l.SetLocation(loc)
+	l.SetPosition(loc)
 	return nil
 }
diff --git a/internal/location/pretty_error.go b/internal/location/pretty_error.go
deleted file mode 100644
index 4c609fad..00000000
--- a/internal/location/pretty_error.go
+++ /dev/null
@@ -1,82 +0,0 @@
-package location
-
-import (
-	"io"
-
-	"github.com/go-faster/errors"
-	yaml "github.com/go-faster/yamlx"
-	"go.uber.org/multierr"
-)
-
-// PrintPrettyError prints the error in a pretty way and returns true if it was printed successfully.
-func PrintPrettyError(w io.Writer, color bool, filename string, data []byte, err error) bool {
-	// TODO(tdakkota): make it configurable?
-	const (
-		printLimit   = 5
-		contextLines = 5
-	)
-
-	var lines Lines
-	lines.Collect(data)
-
-	write := func(msg string, loc Location, context int) {
-		opts := PrintListingOptions{
-			Filename: filename,
-			Context:  context,
-		}
-		if !color {
-			opts = opts.WithoutColor()
-		}
-		_ = lines.PrintListing(w, msg, loc, opts)
-	}
-
-	if e, ok := errors.Into[*yaml.SyntaxError](err); ok {
-		loc := Location{
-			Line: e.Line,
-		}
-		write(e.Msg, loc, contextLines)
-		return true
-	}
-
-	if e, ok := errors.Into[*yaml.TypeError](err); ok {
-		printed := 0
-		for _, e := range multierr.Errors(e.Group) {
-			if printed >= printLimit {
-				break
-			}
-			if e, ok := errors.Into[*yaml.UnmarshalError](e); ok && e.Node != nil {
-				loc := Location{
-					Line:   e.Node.Line,
-					Column: e.Node.Column,
-					Node:   e.Node,
-				}
-				write(e.Err.Error(), loc, contextLines)
-				printed++
-			}
-		}
-		// Consider the error as handled if it is printed at least once.
-		return printed > 0
-	}
-
-	if e, ok := errors.Into[*Error](err); ok {
-		var (
-			iterErr = e.Err
-			locErr  = e
-		)
-		for {
-			e, ok := errors.Into[*Error](iterErr)
-			if !ok {
-				break
-			}
-			locErr = e
-			iterErr = e.Err
-		}
-		// TODO(tdakkota): handle different files.
-		if locErr.File == filename {
-			write(locErr.Err.Error(), locErr.Loc, contextLines)
-			return true
-		}
-	}
-
-	return false
-}
diff --git a/internal/location/print.go b/internal/location/print.go
index 663d826d..ac9321e2 100644
--- a/internal/location/print.go
+++ b/internal/location/print.go
@@ -89,7 +89,7 @@ const BugLine = `Cannot render line properly, please fill a bug report`
 // PrintListing prints given message with line number and file listing to the writer.
 //
 // The context parameter defines the number of lines to print before and after.
-func (l Lines) PrintListing(w io.Writer, msg string, loc Location, opts PrintListingOptions) error {
+func (l Lines) PrintListing(w io.Writer, msg string, loc Position, opts PrintListingOptions) error {
 	opts.setDefaults()
 
 	// Line starts from 1, but index starts from 0.
diff --git a/jsonschema/errors.go b/jsonschema/errors.go
index 22423af5..ac81db85 100644
--- a/jsonschema/errors.go
+++ b/jsonschema/errors.go
@@ -9,30 +9,30 @@ import (
 // LocationError is a wrapper for an error that has a location.
 type LocationError = location.Error
 
-func (p *Parser) wrapField(field, filename string, l location.Locator, err error) error {
+func (p *Parser) wrapField(field string, file location.File, l location.Locator, err error) error {
 	if err == nil || p == nil {
 		return err
 	}
-	return p.wrapLocation(filename, l.Field(field), err)
+	return p.wrapLocation(file, l.Field(field), err)
 }
 
-func (p *Parser) wrapLocation(filename string, l location.Locator, err error) error {
+func (p *Parser) wrapLocation(file location.File, l location.Locator, err error) error {
 	var locErr *LocationError
 	// Do not wrap error if it is nil or is already a LocationError.
 	if err == nil || p == nil || errors.As(err, &locErr) {
 		return err
 	}
 
-	loc, ok := l.Location()
+	pos, ok := l.Position()
 	if !ok {
 		return err
 	}
-	if filename == "" {
-		filename = p.filename
+	if file.IsZero() {
+		file = p.file
 	}
 	return &LocationError{
-		File: filename,
-		Loc:  loc,
+		File: file,
+		Pos:  pos,
 		Err:  err,
 	}
 }
diff --git a/jsonschema/parser.go b/jsonschema/parser.go
index 3fe1fc1a..adc8f009 100644
--- a/jsonschema/parser.go
+++ b/jsonschema/parser.go
@@ -17,10 +17,10 @@ import (
 // Parser parses JSON schemas.
 type Parser struct {
 	external ExternalResolver
-	schemas  map[string]ReferenceResolver
+	schemas  map[string]resolver
 	refcache map[jsonpointer.RefKey]*Schema
 
-	filename string // optional, used for error messages
+	file location.File // optional, used for error messages
 
 	inferTypes bool
 }
@@ -30,11 +30,14 @@ func NewParser(s Settings) *Parser {
 	s.setDefaults()
 	return &Parser{
 		external: s.External,
-		schemas: map[string]ReferenceResolver{
-			"": s.Resolver,
+		schemas: map[string]resolver{
+			"": {
+				ReferenceResolver: s.Resolver,
+				file:              s.File,
+			},
 		},
 		refcache:   map[jsonpointer.RefKey]*Schema{},
-		filename:   s.Filename,
+		file:       s.File,
 		inferTypes: s.InferTypes,
 	}
 }
@@ -57,7 +60,7 @@ func (p *Parser) Resolve(ref string, ctx *jsonpointer.ResolveCtx) (*Schema, erro
 func (p *Parser) parse(schema *RawSchema, ctx *jsonpointer.ResolveCtx) (_ *Schema, rerr error) {
 	if schema != nil {
 		defer func() {
-			rerr = p.wrapLocation(ctx.LastLoc(), schema.Common.Locator, rerr)
+			rerr = p.wrapLocation(ctx.File(), schema.Common.Locator, rerr)
 		}()
 	}
 	return p.parse1(schema, ctx, func(s *Schema) *Schema {
@@ -83,7 +86,7 @@ func (p *Parser) parse1(schema *RawSchema, ctx *jsonpointer.ResolveCtx, hook fun
 					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.File(), loc, err)
 					}
 				}
 			}
@@ -91,7 +94,7 @@ func (p *Parser) parse1(schema *RawSchema, ctx *jsonpointer.ResolveCtx, hook fun
 			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.File(), loc, err)
 			}
 			s.Enum = values
 			handleNullableEnum(s)
@@ -112,7 +115,7 @@ func (p *Parser) parse1(schema *RawSchema, ctx *jsonpointer.ResolveCtx, hook fun
 				return nil
 			}(); err != nil {
 				err := errors.Wrap(err, "parse default")
-				return nil, p.wrapField("default", ctx.LastLoc(), schema.Common.Locator, err)
+				return nil, p.wrapField("default", ctx.File(), schema.Common.Locator, err)
 			}
 		}
 		if a, ok := schema.Common.Extensions["x-ogen-name"]; ok {
@@ -130,7 +133,7 @@ func (p *Parser) parseSchema(schema *RawSchema, ctx *jsonpointer.ResolveCtx, hoo
 		return nil, nil
 	}
 	wrapField := func(field string, err error) error {
-		return p.wrapField(field, ctx.LastLoc(), schema.Common.Locator, err)
+		return p.wrapField(field, ctx.File(), schema.Common.Locator, err)
 	}
 
 	validateMinMax := func(prop string, min, max *uint64) (rerr error) {
@@ -304,12 +307,12 @@ func (p *Parser) parseSchema(schema *RawSchema, ctx *jsonpointer.ResolveCtx, hoo
 				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.File(), 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.File(), ppLoc, err)
 				}
 				patterns[idx] = PatternProperty{
 					Pattern: r,
@@ -324,7 +327,7 @@ func (p *Parser) parseSchema(schema *RawSchema, ctx *jsonpointer.ResolveCtx, hoo
 			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.File(), propsLoc, err)
 			}
 
 			var description string
@@ -446,7 +449,7 @@ func (p *Parser) parseMany(schemas []*RawSchema, loc location.Locator, ctx *json
 	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.File(), loc.Index(i), err)
 		}
 		result = append(result, s)
 	}
diff --git a/jsonschema/parser_settings.go b/jsonschema/parser_settings.go
index 4676a37a..299305ef 100644
--- a/jsonschema/parser_settings.go
+++ b/jsonschema/parser_settings.go
@@ -1,6 +1,10 @@
 package jsonschema
 
-import "errors"
+import (
+	"errors"
+
+	"github.com/ogen-go/ogen/internal/location"
+)
 
 // Settings is parser settings.
 type Settings struct {
@@ -10,10 +14,10 @@ type Settings struct {
 	// Resolver is a root resolver.
 	Resolver ReferenceResolver
 
-	// Filename is a name of the file being parsed.
+	// File is the file that is being parsed.
 	//
 	// Used for error messages.
-	Filename string
+	File location.File
 
 	// Enables type inference.
 	//
diff --git a/jsonschema/resolve.go b/jsonschema/resolve.go
index cf766852..3fab9b1f 100644
--- a/jsonschema/resolve.go
+++ b/jsonschema/resolve.go
@@ -7,6 +7,7 @@ import (
 	yaml "github.com/go-faster/yamlx"
 
 	"github.com/ogen-go/ogen/internal/jsonpointer"
+	"github.com/ogen-go/ogen/internal/location"
 )
 
 // ReferenceResolver resolves JSON schema references.
@@ -14,31 +15,41 @@ type ReferenceResolver interface {
 	ResolveReference(ref string) (*RawSchema, error)
 }
 
-func (p *Parser) getResolver(ctx *jsonpointer.ResolveCtx) (ReferenceResolver, error) {
-	loc := ctx.LastLoc()
+type resolver struct {
+	ReferenceResolver
+	file location.File
+}
 
+func (p *Parser) getResolver(loc string) (r resolver, rerr error) {
 	r, ok := p.schemas[loc]
 	if ok {
 		return r, nil
 	}
 
-	var node yaml.Node
-	if err := func() error {
-		raw, err := p.external.Get(context.TODO(), loc)
-		if err != nil {
-			return errors.Wrap(err, "get")
-		}
+	raw, err := p.external.Get(context.TODO(), loc)
+	if err != nil {
+		return r, errors.Wrap(err, "get")
+	}
 
-		if err := yaml.Unmarshal(raw, &node); err != nil {
-			return errors.Wrap(err, "unmarshal")
+	file := location.NewFile(loc, loc, raw)
+	defer func() {
+		if rerr != nil {
+			rerr = &location.Error{
+				File: file,
+				Err:  rerr,
+			}
 		}
+	}()
 
-		return nil
-	}(); err != nil {
-		return nil, errors.Wrapf(err, "external %q", loc)
+	var node yaml.Node
+	if err := yaml.Unmarshal(raw, &node); err != nil {
+		return r, errors.Wrap(err, "unmarshal")
 	}
 
-	r = NewRootResolver(&node)
+	r = resolver{
+		ReferenceResolver: NewRootResolver(&node),
+		file:              location.NewFile(loc, loc, raw),
+	}
 	p.schemas[loc] = r
 
 	return r, nil
@@ -54,7 +65,12 @@ func (p *Parser) resolve(ref string, ctx *jsonpointer.ResolveCtx) (*Schema, erro
 		return s, nil
 	}
 
-	if err := ctx.AddKey(key); err != nil {
+	r, err := p.getResolver(key.Loc)
+	if err != nil {
+		return nil, err
+	}
+
+	if err := ctx.AddKey(key, r.file); err != nil {
 		return nil, err
 	}
 	defer func() {
@@ -62,12 +78,7 @@ func (p *Parser) resolve(ref string, ctx *jsonpointer.ResolveCtx) (*Schema, erro
 		ctx.Delete(key)
 	}()
 
-	resolver, err := p.getResolver(ctx)
-	if err != nil {
-		return nil, err
-	}
-
-	raw, err := resolver.ResolveReference(key.Ref)
+	raw, err := r.ResolveReference(key.Ref)
 	if err != nil {
 		return nil, err
 	}
diff --git a/openapi/parser/errors.go b/openapi/parser/errors.go
index 41ce545b..52633a77 100644
--- a/openapi/parser/errors.go
+++ b/openapi/parser/errors.go
@@ -9,37 +9,37 @@ import (
 // LocationError is a wrapper for an error that has a location.
 type LocationError = location.Error
 
-func (p *parser) wrapRef(filename string, l location.Locator, err error) error {
+func (p *parser) wrapRef(file location.File, l location.Locator, err error) error {
 	if err == nil || p == nil {
 		return err
 	}
-	return p.wrapLocation(filename, l.Field("$ref"), err)
+	return p.wrapLocation(file, l.Field("$ref"), err)
 }
 
-func (p *parser) wrapField(field, filename string, l location.Locator, err error) error {
+func (p *parser) wrapField(field string, file location.File, l location.Locator, err error) error {
 	if err == nil || p == nil {
 		return err
 	}
-	return p.wrapLocation(filename, l.Field(field), err)
+	return p.wrapLocation(file, l.Field(field), err)
 }
 
-func (p *parser) wrapLocation(filename string, l location.Locator, err error) error {
+func (p *parser) wrapLocation(file location.File, l location.Locator, err error) error {
 	var locErr *LocationError
 	// Do not wrap error if it is nil or is already a LocationError.
 	if err == nil || p == nil || errors.As(err, &locErr) {
 		return err
 	}
 
-	loc, ok := l.Location()
+	pos, ok := l.Position()
 	if !ok {
 		return err
 	}
-	if filename == "" {
-		filename = p.filename
+	if file.IsZero() {
+		file = p.file
 	}
 	return &LocationError{
-		File: filename,
-		Loc:  loc,
+		File: file,
+		Pos:  pos,
 		Err:  err,
 	}
 }
diff --git a/openapi/parser/parse_components.go b/openapi/parser/parse_components.go
index d4912d3d..7f57c9fd 100644
--- a/openapi/parser/parse_components.go
+++ b/openapi/parser/parse_components.go
@@ -27,7 +27,7 @@ func validateComponentsKey[Object any](p *parser, m map[string]Object, locator l
 		if !componentsKeyRegex.MatchString(name) {
 			locator := locator.Key(name)
 			err := errors.Errorf("invalid name: %q doesn't match %q", name, componentsKeyRegex)
-			return p.wrapLocation("", locator, err)
+			return p.wrapLocation(p.file, locator, err)
 		}
 	}
 	return nil
@@ -86,7 +86,7 @@ func (p *parser) parseComponents(c *ogen.Components) (_ *openapi.Components, rer
 	}
 	locator := c.Common.Locator
 	defer func() {
-		rerr = p.wrapLocation("", locator, rerr)
+		rerr = p.wrapLocation(p.file, locator, rerr)
 	}()
 
 	if err := validateComponentsKeys(p, c); err != nil {
@@ -94,7 +94,7 @@ func (p *parser) parseComponents(c *ogen.Components) (_ *openapi.Components, rer
 	}
 	if len(c.PathItems) > 0 {
 		if err := p.requireMinorVersion("pathItem components", 1); err != nil {
-			err := p.wrapLocation("", locator.Field("pathItems"), err)
+			err := p.wrapLocation(p.file, locator.Field("pathItems"), err)
 			return nil, err
 		}
 	}
@@ -109,7 +109,7 @@ func (p *parser) parseComponents(c *ogen.Components) (_ *openapi.Components, rer
 	wrapErr := func(component, name string, err error) error {
 		locator := locator.Field(component).Field(name)
 		err = errors.Wrapf(err, "%s: %q", component, name)
-		return p.wrapLocation("", locator, err)
+		return p.wrapLocation(p.file, locator, err)
 	}
 
 	for name := range c.Schemas {
diff --git a/openapi/parser/parse_example.go b/openapi/parser/parse_example.go
index 0c6c6bc6..4ed570c2 100644
--- a/openapi/parser/parse_example.go
+++ b/openapi/parser/parse_example.go
@@ -12,12 +12,12 @@ func (p *parser) parseExample(e *ogen.Example, ctx *jsonpointer.ResolveCtx) (_ *
 	}
 	locator := e.Common.Locator
 	defer func() {
-		rerr = p.wrapLocation(ctx.LastLoc(), locator, rerr)
+		rerr = p.wrapLocation(ctx.File(), locator, rerr)
 	}()
 	if ref := e.Ref; ref != "" {
 		resolved, err := p.resolveExample(ref, ctx)
 		if err != nil {
-			return nil, p.wrapRef(ctx.LastLoc(), locator, err)
+			return nil, p.wrapRef(ctx.File(), locator, err)
 		}
 		return resolved, nil
 	}
diff --git a/openapi/parser/parse_header.go b/openapi/parser/parse_header.go
index c8389980..155e86f0 100644
--- a/openapi/parser/parse_header.go
+++ b/openapi/parser/parse_header.go
@@ -30,12 +30,12 @@ func (p *parser) parseHeader(name string, header *ogen.Header, ctx *jsonpointer.
 	}
 	locator := header.Common.Locator
 	defer func() {
-		rerr = p.wrapLocation(ctx.LastLoc(), locator, rerr)
+		rerr = p.wrapLocation(ctx.File(), locator, rerr)
 	}()
 	if ref := header.Ref; ref != "" {
 		resolved, err := p.resolveHeader(name, ref, ctx)
 		if err != nil {
-			return nil, p.wrapRef(ctx.LastLoc(), locator, err)
+			return nil, p.wrapRef(ctx.File(), locator, err)
 		}
 		return resolved, nil
 	}
@@ -45,7 +45,7 @@ func (p *parser) parseHeader(name string, header *ogen.Header, ctx *jsonpointer.
 			return nil
 		}
 		err := errors.Errorf(`%q MUST NOT be specified, got %q`, name, got)
-		return p.wrapField(name, ctx.LastLoc(), locator, err)
+		return p.wrapField(name, ctx.File(), locator, err)
 	}
 	if err := mustNotSpecified("in", header.In); err != nil {
 		return nil, err
@@ -55,19 +55,19 @@ func (p *parser) parseHeader(name string, header *ogen.Header, ctx *jsonpointer.
 	}
 	locatedIn := openapi.LocationHeader
 
-	if err := p.validateParameter(name, locatedIn, header, ctx.LastLoc()); err != nil {
+	if err := p.validateParameter(name, locatedIn, header, ctx.File()); err != nil {
 		return nil, err
 	}
 
 	schema, err := p.parseSchema(header.Schema, ctx)
 	if err != nil {
-		return nil, p.wrapField("schema", ctx.LastLoc(), locator, err)
+		return nil, p.wrapField("schema", ctx.File(), locator, err)
 	}
 
 	content, err := p.parseParameterContent(header.Content, locator.Field("content"), ctx)
 	if err != nil {
 		err := errors.Wrap(err, "content")
-		return nil, p.wrapField("content", ctx.LastLoc(), locator, err)
+		return nil, p.wrapField("content", ctx.File(), locator, err)
 	}
 
 	op := &openapi.Header{
@@ -85,7 +85,7 @@ func (p *parser) parseHeader(name string, header *ogen.Header, ctx *jsonpointer.
 
 	// TODO: Validate content?
 	if header.Content == nil {
-		if err := p.validateParamStyle(op, ctx.LastLoc()); err != nil {
+		if err := p.validateParamStyle(op, ctx.File()); err != nil {
 			return nil, err
 		}
 	}
diff --git a/openapi/parser/parse_mediatype.go b/openapi/parser/parse_mediatype.go
index 42ca5e34..f20885c3 100644
--- a/openapi/parser/parse_mediatype.go
+++ b/openapi/parser/parse_mediatype.go
@@ -27,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.File(), locator.Key(name), err)
 		}
 
 		result[name], err = p.parseMediaType(ct, m, ctx)
@@ -54,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.File(), 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.File(), locator.Key(name), err)
 		}
 
 		media, err := p.parseMediaType(ct, m, ctx)
@@ -80,7 +80,7 @@ func (p *parser) parseParameterContent(
 func (p *parser) parseMediaType(ct string, m ogen.Media, ctx *jsonpointer.ResolveCtx) (_ *openapi.MediaType, rerr error) {
 	locator := m.Common.Locator
 	defer func() {
-		rerr = p.wrapLocation(ctx.LastLoc(), locator, rerr)
+		rerr = p.wrapLocation(ctx.File(), locator, rerr)
 	}()
 
 	s, err := p.parseSchema(m.Schema, ctx)
@@ -103,7 +103,7 @@ func (p *parser) parseMediaType(ct string, m ogen.Media, ctx *jsonpointer.Resolv
 			parseEncoding := func(name string, prop jsonschema.Property, e ogen.Encoding) (rerr error) {
 				locator := e.Common.Locator
 				defer func() {
-					rerr = p.wrapLocation(ctx.LastLoc(), locator, rerr)
+					rerr = p.wrapLocation(ctx.File(), locator, rerr)
 				}()
 
 				encoding := &openapi.Encoding{
@@ -116,7 +116,7 @@ func (p *parser) parseMediaType(ct string, m ogen.Media, ctx *jsonpointer.Resolv
 				}
 				encoding.Headers, err = p.parseHeaders(e.Headers, ctx)
 				if err != nil {
-					return p.wrapField("headers", ctx.LastLoc(), locator, err)
+					return p.wrapField("headers", ctx.File(), locator, err)
 				}
 				encodings[name] = encoding
 
@@ -129,7 +129,7 @@ func (p *parser) parseMediaType(ct string, m ogen.Media, ctx *jsonpointer.Resolv
 					Required:      prop.Required,
 					AllowReserved: encoding.AllowReserved,
 					Locator:       encoding.Locator,
-				}, ctx.LastLoc()); err != nil {
+				}, ctx.File()); err != nil {
 					return errors.Wrap(err, "param style")
 				}
 
@@ -142,7 +142,7 @@ func (p *parser) parseMediaType(ct string, m ogen.Media, ctx *jsonpointer.Resolv
 				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.File(), 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 bb08f1ea..3efb2771 100644
--- a/openapi/parser/parse_parameter.go
+++ b/openapi/parser/parse_parameter.go
@@ -55,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.File(), loc, err)
 		}
 
 		param, err := p.parseParameter(spec, ctx)
@@ -69,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.Common.Locator, err)
+			return nil, p.wrapLocation(ctx.File(), spec.Common.Locator, err)
 		}
 
 		unique[ploc] = struct{}{}
@@ -83,7 +83,7 @@ func (p *parser) validateParameter(
 	name string,
 	locatedIn openapi.ParameterLocation,
 	param *ogen.Parameter,
-	file string,
+	file location.File,
 ) error {
 	locator := param.Common.Locator
 	switch {
@@ -120,12 +120,12 @@ func (p *parser) parseParameter(param *ogen.Parameter, ctx *jsonpointer.ResolveC
 	}
 	locator := param.Common.Locator
 	defer func() {
-		rerr = p.wrapLocation(ctx.LastLoc(), locator, rerr)
+		rerr = p.wrapLocation(ctx.File(), locator, rerr)
 	}()
 	if ref := param.Ref; ref != "" {
 		parsed, err := p.resolveParameter(ref, ctx)
 		if err != nil {
-			return nil, p.wrapRef(ctx.LastLoc(), locator, err)
+			return nil, p.wrapRef(ctx.File(), locator, err)
 		}
 		return parsed, nil
 	}
@@ -140,22 +140,22 @@ func (p *parser) parseParameter(param *ogen.Parameter, ctx *jsonpointer.ResolveC
 	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(), locator, err)
+		return nil, p.wrapField("in", ctx.File(), locator, err)
 	}
 
-	if err := p.validateParameter(param.Name, locatedIn, param, ctx.LastLoc()); err != nil {
+	if err := p.validateParameter(param.Name, locatedIn, param, ctx.File()); err != nil {
 		return nil, err
 	}
 
 	schema, err := p.parseSchema(param.Schema, ctx)
 	if err != nil {
-		return nil, p.wrapField("schema", ctx.LastLoc(), locator, err)
+		return nil, p.wrapField("schema", ctx.File(), locator, err)
 	}
 
 	content, err := p.parseParameterContent(param.Content, locator.Field("content"), ctx)
 	if err != nil {
 		err := errors.Wrap(err, "content")
-		return nil, p.wrapField("content", ctx.LastLoc(), locator, err)
+		return nil, p.wrapField("content", ctx.File(), locator, err)
 	}
 
 	op := &openapi.Parameter{
@@ -174,7 +174,7 @@ func (p *parser) parseParameter(param *ogen.Parameter, ctx *jsonpointer.ResolveC
 
 	// TODO: Validate content?
 	if param.Content == nil {
-		if err := p.validateParamStyle(op, ctx.LastLoc()); err != nil {
+		if err := p.validateParamStyle(op, ctx.File()); err != nil {
 			return nil, err
 		}
 	}
@@ -211,7 +211,7 @@ func inferParamExplode(locatedIn openapi.ParameterLocation, explode *bool) bool
 	return false
 }
 
-func (p *parser) validateParamStyle(param *openapi.Parameter, loc string) error {
+func (p *parser) validateParamStyle(param *openapi.Parameter, file location.File) error {
 	// https://swagger.io/docs/specification/serialization/
 	const (
 		primitive byte = 1 << iota
@@ -252,7 +252,7 @@ func (p *parser) validateParamStyle(param *openapi.Parameter, loc string) error
 		},
 	}
 	wrap := func(field string, err error) error {
-		return p.wrapField(field, loc, param.Locator, err)
+		return p.wrapField(field, file, param.Locator, err)
 	}
 
 	styles, ok := table[param.In]
diff --git a/openapi/parser/parse_path_item.go b/openapi/parser/parse_path_item.go
index 352fb4b8..1534c778 100644
--- a/openapi/parser/parse_path_item.go
+++ b/openapi/parser/parse_path_item.go
@@ -21,13 +21,13 @@ func (p *parser) parsePathItem(
 	}
 	locator := item.Common.Locator
 	defer func() {
-		rerr = p.wrapLocation(ctx.LastLoc(), locator, rerr)
+		rerr = p.wrapLocation(ctx.File(), locator, rerr)
 	}()
 
 	if ref := item.Ref; ref != "" {
 		ops, err := p.resolvePathItem(path, ref, ctx)
 		if err != nil {
-			return nil, p.wrapRef(ctx.LastLoc(), locator, err)
+			return nil, p.wrapRef(ctx.File(), locator, err)
 		}
 		return ops, nil
 	}
@@ -41,13 +41,13 @@ func (p *parser) parsePathItem(
 	if err := forEachOps(item, func(method string, op ogen.Operation) error {
 		locator := op.Common.Locator
 		defer func() {
-			rerr = p.wrapLocation(ctx.LastLoc(), locator, rerr)
+			rerr = p.wrapLocation(ctx.File(), locator, rerr)
 		}()
 
 		if id := op.OperationID; id != "" {
 			if _, ok := p.operationIDs[id]; ok {
 				err := errors.Errorf("duplicate operationId: %q", id)
-				return p.wrapField("operationId", ctx.LastLoc(), locator, err)
+				return p.wrapField("operationId", ctx.File(), locator, err)
 			}
 			p.operationIDs[id] = struct{}{}
 		}
@@ -77,7 +77,7 @@ func (p *parser) parseOp(
 ) (_ *openapi.Operation, err error) {
 	locator := spec.Common.Locator
 	defer func() {
-		err = p.wrapLocation(ctx.LastLoc(), locator, err)
+		err = p.wrapLocation(ctx.File(), locator, err)
 	}()
 
 	op := &openapi.Operation{
@@ -114,7 +114,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.File(), locator, err)
 		}
 	}
 
@@ -122,7 +122,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.File(), locator, err)
 		}
 		return nil
 	}
diff --git a/openapi/parser/parse_request_body.go b/openapi/parser/parse_request_body.go
index b536def9..1097bef7 100644
--- a/openapi/parser/parse_request_body.go
+++ b/openapi/parser/parse_request_body.go
@@ -14,12 +14,12 @@ func (p *parser) parseRequestBody(body *ogen.RequestBody, ctx *jsonpointer.Resol
 	}
 	locator := body.Common.Locator
 	defer func() {
-		rerr = p.wrapLocation(ctx.LastLoc(), locator, rerr)
+		rerr = p.wrapLocation(ctx.File(), locator, rerr)
 	}()
 	if ref := body.Ref; ref != "" {
 		resolved, err := p.resolveRequestBody(ref, ctx)
 		if err != nil {
-			return nil, p.wrapRef(ctx.LastLoc(), locator, err)
+			return nil, p.wrapRef(ctx.File(), locator, err)
 		}
 		return resolved, nil
 	}
@@ -38,7 +38,7 @@ func (p *parser) parseRequestBody(body *ogen.RequestBody, ctx *jsonpointer.Resol
 		return content, nil
 	}()
 	if err != nil {
-		return nil, p.wrapField("content", ctx.LastLoc(), locator, err)
+		return nil, p.wrapField("content", ctx.File(), locator, err)
 	}
 
 	return &openapi.RequestBody{
diff --git a/openapi/parser/parse_response.go b/openapi/parser/parse_response.go
index 432aa8cf..08756e33 100644
--- a/openapi/parser/parse_response.go
+++ b/openapi/parser/parse_response.go
@@ -23,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.File(), 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.File(), locator.Field(status), err)
 		}
 
 		result[status] = resp
@@ -44,12 +44,12 @@ func (p *parser) parseResponse(resp *ogen.Response, ctx *jsonpointer.ResolveCtx)
 	}
 	locator := resp.Common.Locator
 	defer func() {
-		rerr = p.wrapLocation(ctx.LastLoc(), locator, rerr)
+		rerr = p.wrapLocation(ctx.File(), locator, rerr)
 	}()
 	if ref := resp.Ref; ref != "" {
 		resolved, err := p.resolveResponse(ref, ctx)
 		if err != nil {
-			return nil, p.wrapRef(ctx.LastLoc(), locator, err)
+			return nil, p.wrapRef(ctx.File(), locator, err)
 		}
 		return resolved, nil
 	}
@@ -57,13 +57,13 @@ func (p *parser) parseResponse(resp *ogen.Response, ctx *jsonpointer.ResolveCtx)
 	content, err := p.parseContent(resp.Content, locator.Field("content"), ctx)
 	if err != nil {
 		err := errors.Wrap(err, "content")
-		return nil, p.wrapField("content", ctx.LastLoc(), locator, err)
+		return nil, p.wrapField("content", ctx.File(), locator, err)
 	}
 
 	headers, err := p.parseHeaders(resp.Headers, ctx)
 	if err != nil {
 		err := errors.Wrap(err, "headers")
-		return nil, p.wrapField("headers", ctx.LastLoc(), locator, err)
+		return nil, p.wrapField("headers", ctx.File(), locator, err)
 	}
 
 	return &openapi.Response{
diff --git a/openapi/parser/parse_security.go b/openapi/parser/parse_security.go
index cf24ce19..46bb5972 100644
--- a/openapi/parser/parse_security.go
+++ b/openapi/parser/parse_security.go
@@ -22,13 +22,13 @@ func (p *parser) parseSecurityScheme(
 	}
 	locator := scheme.Common.Locator
 	defer func() {
-		rerr = p.wrapLocation(ctx.LastLoc(), locator, rerr)
+		rerr = p.wrapLocation(ctx.File(), locator, rerr)
 	}()
 
 	if ref := scheme.Ref; ref != "" {
 		resolved, err := p.resolveSecurityScheme(ref, ctx)
 		if err != nil {
-			return nil, p.wrapRef(ctx.LastLoc(), locator, err)
+			return nil, p.wrapRef(ctx.File(), locator, err)
 		}
 		return resolved, nil
 	}
@@ -40,11 +40,11 @@ func (p *parser) parseSecurityScheme(
 			case "query", "header", "cookie":
 			default:
 				err := errors.Errorf(`invalid "in": %q`, scheme.In)
-				return p.wrapField("in", ctx.LastLoc(), locator, err)
+				return p.wrapField("in", ctx.File(), locator, err)
 			}
 			if scheme.Name == "" {
 				err := errors.New(`"name" is required and MUST be a non-empty string`)
-				return p.wrapField("name", ctx.LastLoc(), locator, err)
+				return p.wrapField("name", ctx.File(), locator, err)
 			}
 			return nil
 		case "http":
@@ -66,26 +66,26 @@ func (p *parser) parseSecurityScheme(
 				"vapid":
 			default:
 				err := errors.Errorf(`invalid "scheme": %q`, scheme.Scheme)
-				return p.wrapField("scheme", ctx.LastLoc(), locator, err)
+				return p.wrapField("scheme", ctx.File(), locator, err)
 			}
 			return nil
 		case "mutualTLS":
 			if err := p.requireMinorVersion("mutualTLS security", 1); err != nil {
-				return p.wrapField("type", ctx.LastLoc(), locator, err)
+				return p.wrapField("type", ctx.File(), locator, err)
 			}
 			return nil
 		case "oauth2":
-			err := p.validateOAuthFlows(scheme.Flows, ctx.LastLoc())
-			return p.wrapField("flows", ctx.LastLoc(), locator, err)
+			err := p.validateOAuthFlows(scheme.Flows, ctx.File())
+			return p.wrapField("flows", ctx.File(), 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(), locator, err)
+				return p.wrapField("openIdConnectUrl", ctx.File(), locator, err)
 			}
 			return nil
 		default:
 			err := errors.Errorf("unknown security scheme type %q", scheme.Type)
-			return p.wrapField("type", ctx.LastLoc(), locator, err)
+			return p.wrapField("type", ctx.File(), locator, err)
 		}
 	}(); err != nil {
 		return nil, errors.Wrap(err, scheme.Type)
@@ -114,13 +114,13 @@ func forEachFlow(flows *ogen.OAuthFlows, cb func(flow *ogen.OAuthFlow, authURL,
 	return nil
 }
 
-func (p *parser) validateOAuthFlows(flows *ogen.OAuthFlows, loc string) (rerr error) {
+func (p *parser) validateOAuthFlows(flows *ogen.OAuthFlows, file location.File) (rerr error) {
 	if flows == nil {
 		return errors.New("oAuthFlows is empty or null")
 	}
 	locator := flows.Common.Locator
 	defer func() {
-		rerr = p.wrapLocation(loc, locator, rerr)
+		rerr = p.wrapLocation(file, locator, rerr)
 	}()
 
 	check := func(flow *ogen.OAuthFlow, authURL, tokenURL bool) (rerr error) {
@@ -129,7 +129,7 @@ func (p *parser) validateOAuthFlows(flows *ogen.OAuthFlows, loc string) (rerr er
 		}
 		locator := flow.Common.Locator
 		defer func() {
-			rerr = p.wrapLocation(loc, locator, rerr)
+			rerr = p.wrapLocation(file, locator, rerr)
 		}()
 
 		checkURL := func(name, input string, check bool) error {
@@ -138,7 +138,7 @@ func (p *parser) validateOAuthFlows(flows *ogen.OAuthFlows, loc string) (rerr er
 			}
 			if _, err := url.ParseRequestURI(input); err != nil {
 				err := errors.Wrapf(err, `%q MUST be in the form of a URL`, name)
-				return p.wrapField(name, loc, locator, err)
+				return p.wrapField(name, file, locator, err)
 			}
 			return nil
 		}
@@ -196,14 +196,14 @@ func (p *parser) parseSecurityRequirements(
 			v, ok := p.securitySchemes[name]
 			if !ok {
 				err := errors.Errorf("unknown security scheme %q", name)
-				return nil, p.wrapLocation(ctx.LastLoc(), locator.Key(name), err)
+				return nil, p.wrapLocation(ctx.File(), 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.File(), loc, err)
 			}
 
 			if len(scopes) > 0 {
@@ -213,7 +213,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.File(), locator, err)
 				}
 			}
 
diff --git a/openapi/parser/parse_server.go b/openapi/parser/parse_server.go
index 78911792..b34e44b2 100644
--- a/openapi/parser/parse_server.go
+++ b/openapi/parser/parse_server.go
@@ -37,7 +37,7 @@ func (p *parser) parseServer(
 ) (_ openapi.Server, rerr error) {
 	locator := s.Common.Locator
 	defer func() {
-		rerr = p.wrapLocation(ctx.LastLoc(), locator, rerr)
+		rerr = p.wrapLocation(ctx.File(), locator, rerr)
 	}()
 
 	// Validate variables.
@@ -47,7 +47,7 @@ func (p *parser) parseServer(
 			locator := locator.Field("enum")
 			if !slices.Contains(v.Enum, v.Default) {
 				err := errors.Errorf("the default value %q MUST exist in the enum's values", v.Default)
-				return openapi.Server{}, p.wrapLocation(ctx.LastLoc(), locator, err)
+				return openapi.Server{}, p.wrapLocation(ctx.File(), locator, err)
 			}
 			for i, e := range v.Enum {
 				locator := locator.Index(i)
@@ -57,7 +57,7 @@ func (p *parser) parseServer(
 				}
 				if found != i {
 					err := errors.Errorf("enum MUST NOT contain duplicate values: %q", e)
-					return openapi.Server{}, p.wrapLocation(ctx.LastLoc(), locator, err)
+					return openapi.Server{}, p.wrapLocation(ctx.File(), locator, err)
 				}
 			}
 		}
@@ -83,7 +83,7 @@ func (p *parser) parseServer(
 	}()
 	if err != nil {
 		locator := locator.Field("url")
-		return openapi.Server{}, p.wrapLocation(ctx.LastLoc(), locator, err)
+		return openapi.Server{}, p.wrapLocation(ctx.File(), locator, err)
 	}
 
 	server := openapi.Server{
@@ -118,7 +118,7 @@ func (p *parser) parseServer(
 			return nil
 		}(); err != nil {
 			locator := locator.Field(nameKey)
-			return openapi.Server{}, p.wrapLocation(ctx.LastLoc(), locator, err)
+			return openapi.Server{}, p.wrapLocation(ctx.File(), locator, err)
 		}
 	}
 
diff --git a/openapi/parser/parse_webhook.go b/openapi/parser/parse_webhook.go
index 028eeacc..5b186fe4 100644
--- a/openapi/parser/parse_webhook.go
+++ b/openapi/parser/parse_webhook.go
@@ -31,7 +31,7 @@ func (p *parser) parseWebhooks(webhooks map[string]*ogen.PathItem) (r []openapi.
 		ctx     = jsonpointer.NewResolveCtx(p.depthLimit)
 	)
 	defer func() {
-		rerr = p.wrapLocation(ctx.LastLoc(), locator, rerr)
+		rerr = p.wrapLocation(ctx.File(), locator, rerr)
 	}()
 	if err := p.requireMinorVersion("webhooks", 1); err != nil {
 		return nil, err
@@ -43,7 +43,7 @@ func (p *parser) parseWebhooks(webhooks map[string]*ogen.PathItem) (r []openapi.
 		webhook, err := p.parseWebhook(name, item, ctx)
 		if err != nil {
 			err := errors.Wrapf(err, "parse webhook %q", name)
-			return nil, p.wrapLocation(ctx.LastLoc(), locator.Field(name), err)
+			return nil, p.wrapLocation(ctx.File(), locator.Field(name), err)
 		}
 		r = append(r, webhook)
 	}
diff --git a/openapi/parser/parser.go b/openapi/parser/parser.go
index 32a27ea7..fd784252 100644
--- a/openapi/parser/parser.go
+++ b/openapi/parser/parser.go
@@ -3,7 +3,6 @@ package parser
 
 import (
 	"github.com/go-faster/errors"
-	yaml "github.com/go-faster/yamlx"
 	"golang.org/x/exp/maps"
 
 	"github.com/ogen-go/ogen"
@@ -48,9 +47,9 @@ type parser struct {
 	operationIDs map[string]struct{}
 
 	external   jsonschema.ExternalResolver
-	schemas    map[string]*yaml.Node
+	schemas    map[string]resolver
 	depthLimit int
-	filename   string // optional, used for error messages
+	file       location.File // optional, used for error messages
 
 	schemaParser *jsonschema.Parser
 }
@@ -86,27 +85,30 @@ func Parse(spec *ogen.Spec, s Settings) (_ *openapi.API, rerr error) {
 		},
 		securitySchemes: maps.Clone(spec.Components.SecuritySchemes),
 		external:        s.External,
-		schemas: map[string]*yaml.Node{
-			"": spec.Raw,
+		schemas: map[string]resolver{
+			"": {
+				node: spec.Raw,
+				file: s.File,
+			},
 		},
 		depthLimit: s.DepthLimit,
-		filename:   s.Filename,
+		file:       s.File,
 		schemaParser: jsonschema.NewParser(jsonschema.Settings{
 			External: s.External,
 			Resolver: componentsResolver{
 				components: spec.Components.Schemas,
 				root:       jsonschema.NewRootResolver(spec.Raw),
 			},
-			Filename:   s.Filename,
+			File:       s.File,
 			InferTypes: s.InferTypes,
 		}),
 	}
 	if spec.Raw != nil {
-		var loc location.Location
+		var loc location.Position
 		loc.FromNode(spec.Raw)
-		p.rootLoc.SetLocation(loc)
+		p.rootLoc.SetPosition(loc)
 		defer func() {
-			rerr = p.wrapLocation("", p.rootLoc, rerr)
+			rerr = p.wrapLocation(p.file, p.rootLoc, rerr)
 		}()
 	}
 
@@ -170,13 +172,13 @@ func (p *parser) parsePathItems() error {
 
 			return nil
 		}(); err != nil {
-			return p.wrapLocation("", pathsLoc.Key(path), err)
+			return p.wrapLocation(p.file, pathsLoc.Key(path), err)
 		}
 
 		ops, err := p.parsePathItem(path, item, jsonpointer.NewResolveCtx(p.depthLimit))
 		if err != nil {
 			err := errors.Wrapf(err, "path %q", path)
-			return p.wrapLocation("", pathsLoc.Field(path), err)
+			return p.wrapLocation(p.file, pathsLoc.Field(path), err)
 		}
 		p.operations = append(p.operations, ops...)
 	}
diff --git a/openapi/parser/resolve.go b/openapi/parser/resolve.go
index dc8a8c38..396a55ad 100644
--- a/openapi/parser/resolve.go
+++ b/openapi/parser/resolve.go
@@ -9,33 +9,45 @@ import (
 
 	"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) getSchema(ctx *jsonpointer.ResolveCtx) (*yaml.Node, error) {
-	loc := ctx.LastLoc()
+type resolver struct {
+	node *yaml.Node
+	file location.File
+}
 
+func (p *parser) getResolver(loc string) (r resolver, rerr error) {
 	r, ok := p.schemas[loc]
 	if ok {
 		return r, nil
 	}
 
-	var node yaml.Node
-	if err := func() error {
-		raw, err := p.external.Get(context.TODO(), loc)
-		if err != nil {
-			return errors.Wrap(err, "get")
-		}
+	raw, err := p.external.Get(context.TODO(), loc)
+	if err != nil {
+		return r, errors.Wrap(err, "get")
+	}
 
-		if err := yaml.Unmarshal(raw, &node); err != nil {
-			return errors.Wrap(err, "unmarshal")
+	file := location.NewFile(loc, loc, raw)
+	defer func() {
+		if rerr != nil {
+			rerr = &location.Error{
+				File: file,
+				Err:  rerr,
+			}
 		}
+	}()
+
+	var node yaml.Node
+	if err := yaml.Unmarshal(raw, &node); err != nil {
+		return r, errors.Wrap(err, "unmarshal")
+	}
 
-		return nil
-	}(); err != nil {
-		return nil, errors.Wrapf(err, "external %q", loc)
+	r = resolver{
+		node: &node,
+		file: file,
 	}
-	r = &node
 	p.schemas[loc] = r
 	return r, nil
 }
@@ -48,14 +60,6 @@ func resolvePointer(root *yaml.Node, ptr string, to any) error {
 	return n.Decode(to)
 }
 
-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)
-}
-
 // componentResolve contains all the information needed to resolve a component.
 type componentResolve[Raw, Target any] struct {
 	// prefix is the usual prefix of the component reference (e.g "#/components/requestBodies/").
@@ -87,13 +91,7 @@ func resolveComponent[Raw, Target any](
 		return r, true, nil
 	}
 
-	if err := ctx.AddKey(key); err != nil {
-		return zero, false, err
-	}
-	defer func() {
-		ctx.Delete(key)
-	}()
-
+	file := p.file
 	var raw Raw
 	if key.Loc == "" && ctx.LastLoc() == "" {
 		name := strings.TrimPrefix(ref, cr.prefix)
@@ -106,10 +104,22 @@ func resolveComponent[Raw, Target any](
 			}
 		}
 	} else {
-		if err := p.resolve(key, ctx, &raw); err != nil {
+		r, err := p.getResolver(key.Loc)
+		if err != nil {
 			return zero, false, err
 		}
+		file = r.file
+		if err := resolvePointer(r.node, key.Ref, &raw); err != nil {
+			return zero, false, err
+		}
+	}
+
+	if err := ctx.AddKey(key, file); err != nil {
+		return zero, false, err
 	}
+	defer func() {
+		ctx.Delete(key)
+	}()
 
 	r, err := cr.parse(raw, ctx)
 	if err != nil {
diff --git a/openapi/parser/settings.go b/openapi/parser/settings.go
index 2babe186..719a103b 100644
--- a/openapi/parser/settings.go
+++ b/openapi/parser/settings.go
@@ -2,6 +2,7 @@ package parser
 
 import (
 	"github.com/ogen-go/ogen/internal/jsonpointer"
+	"github.com/ogen-go/ogen/internal/location"
 	"github.com/ogen-go/ogen/jsonschema"
 )
 
@@ -10,10 +11,10 @@ type Settings struct {
 	// External is external JSON Schema resolver. If nil, NoExternal resolver is used.
 	External jsonschema.ExternalResolver
 
-	// Filename is a name of the file being parsed.
+	// File is the file that is being parsed.
 	//
 	// Used for error messages.
-	Filename string
+	File location.File
 
 	// DepthLimit limits the number of nested references. Default is 1000.
 	DepthLimit int
diff --git a/openapi/parser/version.go b/openapi/parser/version.go
index 6ab3d5a8..f0d8fb7c 100644
--- a/openapi/parser/version.go
+++ b/openapi/parser/version.go
@@ -10,7 +10,7 @@ import (
 
 func (p *parser) parseVersion() (rerr error) {
 	defer func() {
-		rerr = p.wrapLocation("", p.rootLoc.Field("openapi"), rerr)
+		rerr = p.wrapLocation(p.file, p.rootLoc.Field("openapi"), rerr)
 	}()
 	if err := p.version.UnmarshalText([]byte(p.spec.OpenAPI)); err != nil {
 		return errors.Wrap(err, "invalid version")


Test patch:
diff --git a/gen_test.go b/gen_test.go
index 89029481..99e303f9 100644
--- a/gen_test.go
+++ b/gen_test.go
@@ -33,7 +33,7 @@ func testGenerate(t *testing.T, filename string, data []byte, aliases ctAliases,
 			notImplemented[name] = struct{}{}
 		},
 		ContentTypeAliases: aliases,
-		Filename:           filename,
+		File:               location.NewFile(filename, filename, data),
 		Logger:             log,
 	}
 	t.Run("Gen", func(t *testing.T) {
@@ -158,18 +158,19 @@ func TestNegative(t *testing.T) {
 		spec, err := ogen.Parse(data)
 		a.NoError(err)
 
+		f := location.NewFile(name, name, data)
 		_, err = parser.Parse(spec, parser.Settings{
-			Filename: name,
+			File: f,
 		})
 		a.NoError(err, "If the error is related to parser, move this test to parser package testdata")
 
 		_, err = gen.NewGenerator(spec, gen.Options{
-			Filename: name,
+			File: f,
 		})
 		a.Error(err)
 
 		var buf strings.Builder
-		if location.PrintPrettyError(&buf, true, name, data, err) {
+		if location.PrintPrettyError(&buf, true, err) {
 			t.Log(buf.String())
 		} else {
 			t.Logf("%+v", err)
diff --git a/internal/location/location_test.go b/internal/location/position_test.go
similarity index 90%
rename from internal/location/location_test.go
rename to internal/location/position_test.go
index a8dcd158..3fa92d07 100644
--- a/internal/location/location_test.go
+++ b/internal/location/position_test.go
@@ -7,7 +7,7 @@ import (
 	"github.com/stretchr/testify/require"
 )
 
-func TestLocation_Key(t *testing.T) {
+func TestPosition_Key(t *testing.T) {
 	input := `{
   "a": 1,
   "b": {
@@ -19,7 +19,7 @@ func TestLocation_Key(t *testing.T) {
 	var node yaml.Node
 	a.NoError(yaml.Unmarshal([]byte(input), &node))
 
-	var loc Location
+	var loc Position
 	loc.FromNode(&node)
 	a.Equal(1, loc.Line)
 	a.Equal(1, loc.Column)
@@ -42,7 +42,7 @@ func TestLocation_Key(t *testing.T) {
 	}
 }
 
-func TestLocation_Field(t *testing.T) {
+func TestPosition_Field(t *testing.T) {
 	input := `{
   "a": 1,
   "b": {
@@ -54,7 +54,7 @@ func TestLocation_Field(t *testing.T) {
 	var node yaml.Node
 	a.NoError(yaml.Unmarshal([]byte(input), &node))
 
-	var loc Location
+	var loc Position
 	loc.FromNode(&node)
 	a.Equal(1, loc.Line)
 	a.Equal(1, loc.Column)
@@ -71,7 +71,7 @@ func TestLocation_Field(t *testing.T) {
 	a.Equal(10, loc.Column)
 }
 
-func TestLocation_Index(t *testing.T) {
+func TestPosition_Index(t *testing.T) {
 	input := `[
   1,
   2.125
@@ -81,7 +81,7 @@ func TestLocation_Index(t *testing.T) {
 	var node yaml.Node
 	a.NoError(yaml.Unmarshal([]byte(input), &node))
 
-	var loc Location
+	var loc Position
 	loc.FromNode(&node)
 	a.Equal(1, loc.Line)
 	a.Equal(1, loc.Column)
diff --git a/jsonschema/jsonschema_test.go b/jsonschema/jsonschema_test.go
index aa7f9597..22efefa6 100644
--- a/jsonschema/jsonschema_test.go
+++ b/jsonschema/jsonschema_test.go
@@ -32,13 +32,13 @@ func TestNegative(t *testing.T) {
 		a.NoError(err)
 
 		p := jsonschema.NewParser(jsonschema.Settings{
-			Filename: name,
+			File: location.NewFile(name, file, data),
 		})
 		_, err = p.Parse(&schema)
 		a.Error(err)
 
 		var buf strings.Builder
-		ok := location.PrintPrettyError(&buf, true, name, data, err)
+		ok := location.PrintPrettyError(&buf, true, err)
 		// Ensure that the error message is pretty printed.
 		//
 		// There should be a good reason to remove this line.
diff --git a/jsonschema/parser_test.go b/jsonschema/parser_test.go
index 5e001c85..ac96583e 100644
--- a/jsonschema/parser_test.go
+++ b/jsonschema/parser_test.go
@@ -369,12 +369,14 @@ func TestSchemaExtensions(t *testing.T) {
 		tt := tt
 		t.Run(fmt.Sprintf("Test%d", i+1), func(t *testing.T) {
 			a := require.New(t)
+			data := []byte(tt.raw)
 
 			var raw RawSchema
-			a.NoError(yaml.Unmarshal([]byte(tt.raw), &raw))
+			a.NoError(yaml.Unmarshal(data, &raw))
 
+			const filename = "test.yaml"
 			out, err := NewParser(Settings{
-				Filename: "test.yaml",
+				File: location.NewFile(filename, filename, data),
 			}).Parse(&raw)
 			if tt.expectErr {
 				a.Error(err)
diff --git a/location_test.go b/location_test.go
index b7053b67..a2596f72 100644
--- a/location_test.go
+++ b/location_test.go
@@ -96,14 +96,14 @@ var (
 	locationSpecYAML string
 )
 
-func TestLocation(t *testing.T) {
-	createEqualLoc := func(a *assert.Assertions, data []byte) func(l location.Locatable, line, column int) {
+func TestPosition(t *testing.T) {
+	createEqualPos := func(a *assert.Assertions, data []byte) func(l location.Locatable, line, column int) {
 		var lines location.Lines
 		lines.Collect(data)
 		return func(l location.Locatable, line, column int) {
 			t.Helper()
 
-			loc, ok := l.Location()
+			pos, ok := l.Position()
 			a.True(ok)
 			getLine := func(n int) string {
 				start, end := lines.Line(n)
@@ -111,20 +111,20 @@ func TestLocation(t *testing.T) {
 				return strings.Trim(string(data[start:end]), "\n\r")
 			}
 
-			type compareLoc struct {
+			type comparePos struct {
 				Line, Column int
 				Data         string
 			}
 			a.Equal(
-				compareLoc{line, column, getLine(line)},
-				compareLoc{loc.Line, loc.Column, getLine(loc.Line)},
+				comparePos{line, column, getLine(line)},
+				comparePos{pos.Line, pos.Column, getLine(pos.Line)},
 			)
 		}
 	}
 
 	t.Run("JSON", func(t *testing.T) {
 		a := assert.New(t)
-		equalLoc := createEqualLoc(a, []byte(locationSpecJSON))
+		equalLoc := createEqualPos(a, []byte(locationSpecJSON))
 
 		locationSpec, err := Parse([]byte(locationSpecJSON))
 		require.NoError(t, err)
@@ -161,7 +161,7 @@ func TestLocation(t *testing.T) {
 	})
 	t.Run("YAML", func(t *testing.T) {
 		a := assert.New(t)
-		equalLoc := createEqualLoc(a, []byte(locationSpecYAML))
+		equalLoc := createEqualPos(a, []byte(locationSpecYAML))
 
 		locationSpec, err := Parse([]byte(locationSpecYAML))
 		require.NoError(t, err)
diff --git a/openapi/parser/errors_test.go b/openapi/parser/errors_test.go
index f87589c1..23134950 100644
--- a/openapi/parser/errors_test.go
+++ b/openapi/parser/errors_test.go
@@ -8,6 +8,7 @@ import (
 	"github.com/stretchr/testify/require"
 
 	"github.com/ogen-go/ogen"
+	"github.com/ogen-go/ogen/internal/location"
 )
 
 func TestRemoteLocation(t *testing.T) {
@@ -40,7 +41,7 @@ func TestRemoteLocation(t *testing.T) {
 
 	_, err := Parse(root, Settings{
 		External: remote,
-		Filename: "root.json",
+		File:     location.NewFile("root.json", "root.json", nil),
 	})
 	a.Error(err)
 	var (
@@ -55,5 +56,5 @@ func TestRemoteLocation(t *testing.T) {
 	}
 	t.Log(locErr)
 	a.NotNil(locErr)
-	a.Equal("foo.json", locErr.File)
+	a.Equal("foo.json", locErr.File.Name)
 }
diff --git a/openapi/parser/parse_security_test.go b/openapi/parser/parse_security_test.go
index fd35f400..2a905008 100644
--- a/openapi/parser/parse_security_test.go
+++ b/openapi/parser/parse_security_test.go
@@ -7,6 +7,7 @@ import (
 	"github.com/stretchr/testify/require"
 
 	"github.com/ogen-go/ogen"
+	"github.com/ogen-go/ogen/internal/location"
 )
 
 func Test_validateOAuthFlows(t *testing.T) {
@@ -69,7 +70,7 @@ func Test_validateOAuthFlows(t *testing.T) {
 		t.Run(fmt.Sprintf("Test%d", i+1), func(t *testing.T) {
 			a := require.New(t)
 			var p parser
-			err := p.validateOAuthFlows(tt.flows, "")
+			err := p.validateOAuthFlows(tt.flows, location.File{})
 			if tt.wantErr {
 				a.Error(err)
 				return
diff --git a/openapi/parser/parser_test.go b/openapi/parser/parser_test.go
index 002f7bc5..a9d8488c 100644
--- a/openapi/parser/parser_test.go
+++ b/openapi/parser/parser_test.go
@@ -31,12 +31,12 @@ func TestNegative(t *testing.T) {
 		a.NoError(err)
 
 		_, err = parser.Parse(spec, parser.Settings{
-			Filename: name,
+			File: location.NewFile(name, file, data),
 		})
 		a.Error(err)
 
 		var buf strings.Builder
-		ok := location.PrintPrettyError(&buf, true, name, data, err)
+		ok := location.PrintPrettyError(&buf, true, err)
 		// Ensure that the error message is pretty printed.
 		//
 		// There should be a good reason to remove this line.
diff --git a/openapi/parser/resolve_external_test.go b/openapi/parser/resolve_external_test.go
index 27ecd543..b5eef13b 100644
--- a/openapi/parser/resolve_external_test.go
+++ b/openapi/parser/resolve_external_test.go
@@ -9,6 +9,7 @@ import (
 	"github.com/stretchr/testify/require"
 
 	"github.com/ogen-go/ogen"
+	"github.com/ogen-go/ogen/internal/location"
 	"github.com/ogen-go/ogen/jsonschema"
 	"github.com/ogen-go/ogen/openapi"
 )
@@ -346,8 +347,92 @@ func TestDuplicateOperationID(t *testing.T) {
 		},
 	}
 
+	a := require.New(t)
+	_, err := Parse(root, Settings{
+		External: remote,
+	})
+	a.ErrorContains(err, "duplicate operationId: \"testGet\"")
+	// Ensure that the error contains the file name.
+	var locErr *location.Error
+	a.ErrorAs(err, &locErr)
+	a.Equal("pathItem.json", locErr.File.Name)
+}
+
+// Ensure that parser adds location information to the error, even if the error is occurred in the external file.
+func TestLocationError(t *testing.T) {
+	root := &ogen.Spec{
+		OpenAPI: "3.0.3",
+		Paths: map[string]*ogen.PathItem{
+			"/get": {
+				Get: &ogen.Operation{
+					OperationID: "testGet",
+					Description: "local",
+					Responses: map[string]*ogen.Response{
+						"200": {
+							Description: "response description",
+						},
+					},
+				},
+			},
+			"/pathItem": {
+				Ref: "pathItem.json#",
+			},
+		},
+		Components: &ogen.Components{},
+	}
+	remote := external{
+		"pathItem.json": &ogen.PathItem{
+			Get: &ogen.Operation{
+				OperationID: "testGet",
+				Description: "remote",
+				Responses: map[string]*ogen.Response{
+					"200": {
+						Description: "response description",
+					},
+				},
+			},
+		},
+	}
+
+	a := require.New(t)
 	_, err := Parse(root, Settings{
 		External: remote,
 	})
-	require.ErrorContains(t, err, "duplicate operationId: \"testGet\"")
+	a.ErrorContains(err, "duplicate operationId: \"testGet\"")
+	// Ensure that the error contains the file name.
+	var locErr *location.Error
+	a.ErrorAs(err, &locErr)
+	a.Equal("pathItem.json", locErr.File.Name)
+}
+
+// Ensure that parser adds location information to the error, even if the error is occurred in the external file.
+func TestExternalErrors(t *testing.T) {
+	root := &ogen.Spec{
+		OpenAPI: "3.0.3",
+		Paths: map[string]*ogen.PathItem{
+			"/pathItem": {
+				Ref: "pathItem.json#",
+			},
+		},
+		Components: &ogen.Components{},
+	}
+	remote := external{
+		"pathItem.json": &ogen.PathItem{
+			Get: &ogen.Operation{
+				Responses: map[string]*ogen.Response{
+					"very bad status code": {},
+				},
+			},
+		},
+	}
+
+	a := require.New(t)
+	_, err := Parse(root, Settings{
+		External: remote,
+	})
+	a.ErrorContains(err, "parse status code")
+
+	var locErr *location.Error
+	a.ErrorAs(err, &locErr)
+	a.Equal("pathItem.json", locErr.File.Name)
 }