Repository: ogen-go/ogen
Pull request #480 (2022-07-12 10:21:03)

feat(parser): make parser errors more precise


Patch:
diff --git a/cmd/ogen/main.go b/cmd/ogen/main.go
index 2a4e3f6b..5cf45990 100644
--- a/cmd/ogen/main.go
+++ b/cmd/ogen/main.go
@@ -16,6 +16,7 @@ import (
 	"github.com/ogen-go/ogen"
 	"github.com/ogen-go/ogen/gen"
 	"github.com/ogen-go/ogen/gen/genfs"
+	"github.com/ogen-go/ogen/internal/location"
 	"github.com/ogen-go/ogen/internal/ogenzap"
 )
 
@@ -173,7 +174,7 @@ func run() error {
 		Format: *performFormat,
 	}
 	if err := generate(data, *packageName, fs, opts); err != nil {
-		if printPrettyError(os.Stderr, specPath, data, err) {
+		if location.PrintPrettyError(os.Stderr, specPath, data, err) {
 			return errors.New("generation failed")
 		}
 		return errors.Wrap(err, "generate")
diff --git a/cmd/ogen/pretty_error.go b/cmd/ogen/pretty_error.go
deleted file mode 100644
index 0a38bbb4..00000000
--- a/cmd/ogen/pretty_error.go
+++ /dev/null
@@ -1,86 +0,0 @@
-package main
-
-import (
-	"fmt"
-	"io"
-
-	"github.com/go-faster/errors"
-	"go.uber.org/multierr"
-	"gopkg.in/yaml.v3"
-
-	ogenjson "github.com/ogen-go/ogen/json"
-)
-
-func printPrettyError(w io.Writer, filename string, data []byte, err error) bool {
-	var lines ogenjson.Lines
-	lines.Collect(data)
-
-	write := func(msg string, loc ogenjson.Location, context int) {
-		_ = lines.PrettyError(w, filename, msg, loc, 5)
-	}
-
-	if e, ok := errors.Into[*yaml.SyntaxError](err); ok {
-		loc := ogenjson.Location{
-			Line: e.Line,
-		}
-		write(e.Msg, loc, 5)
-		return true
-	}
-
-	if e, ok := errors.Into[*yaml.TypeError](err); ok {
-		// TODO(tdakkota): make it configurable?
-		const limit = 5
-		printed := 0
-		for _, e := range multierr.Errors(e.Group) {
-			if printed >= limit {
-				break
-			}
-			if e, ok := errors.Into[*yaml.UnmarshalError](e); ok && e.Node != nil {
-				loc := ogenjson.Location{
-					Line:   e.Node.Line,
-					Column: e.Node.Column,
-					Node:   e.Node,
-				}
-				write(e.Err.Error(), loc, 3)
-				printed++
-			}
-		}
-		// Consider the error as handled if it is printed at least once.
-		return printed > 0
-	}
-
-	if e, ok := errors.Into[*ogenjson.LocationError](err); ok {
-		var (
-			iterErr = e.Err
-			locErr  = e
-		)
-		for {
-			e, ok := errors.Into[*ogenjson.LocationError](iterErr)
-			if !ok {
-				break
-			}
-			locErr = e
-			iterErr = e.Err
-		}
-		// TODO(tdakkota): handle different files.
-		if locErr.File == filename {
-			write(locErr.Err.Error(), locErr.Loc, 5)
-			return true
-		}
-	}
-
-	return false
-}
-
-// PrettyError is pretty-printed error.
-type PrettyError struct {
-	Filename string
-	Msg      string
-	Loc      ogenjson.Location
-	Lines    ogenjson.Lines
-}
-
-// Error implements error.
-func (p *PrettyError) Error() string {
-	return fmt.Sprintf("%s: %s", p.Loc, p.Msg)
-}
diff --git a/gen/utils.go b/gen/utils.go
index 5da0f7f7..a645e4c9 100644
--- a/gen/utils.go
+++ b/gen/utils.go
@@ -7,7 +7,7 @@ import (
 	"go.uber.org/zap"
 
 	"github.com/ogen-go/ogen/gen/ir"
-	ogenjson "github.com/ogen-go/ogen/json"
+	"github.com/ogen-go/ogen/internal/location"
 	"github.com/ogen-go/ogen/jsonschema"
 )
 
@@ -76,7 +76,7 @@ func statusText(code int) string {
 }
 
 func (g *Generator) zapLocation(l interface {
-	Location() (ogenjson.Location, bool)
+	Location() (location.Location, bool)
 }) zap.Field {
 	loc, ok := l.Location()
 	if !ok {
@@ -86,7 +86,7 @@ func (g *Generator) zapLocation(l interface {
 }
 
 func (g *schemaGen) zapLocation(l interface {
-	Location() (ogenjson.Location, bool)
+	Location() (location.Location, bool)
 }) zap.Field {
 	loc, ok := l.Location()
 	if !ok {
diff --git a/json/location_error.go b/internal/location/error.go
similarity index 60%
rename from json/location_error.go
rename to internal/location/error.go
index ed9db7bf..3ea5233c 100644
--- a/json/location_error.go
+++ b/internal/location/error.go
@@ -1,4 +1,4 @@
-package json
+package location
 
 import (
 	"fmt"
@@ -11,21 +11,21 @@ var _ interface {
 	errors.Formatter
 	fmt.Formatter
 	error
-} = (*LocationError)(nil)
+} = (*Error)(nil)
 
-// LocationError is a wrapper for an error that has a location.
-type LocationError struct {
+// Error is a wrapper for an error that has a location.
+type Error struct {
 	File string
 	Loc  Location
 	Err  error
 }
 
 // Unwrap implements errors.Wrapper.
-func (e *LocationError) Unwrap() error {
+func (e *Error) Unwrap() error {
 	return e.Err
 }
 
-func (e *LocationError) fileName() string {
+func (e *Error) fileName() string {
 	filename := e.File
 	if filename == "" || e.Loc.Line == 0 {
 		return ""
@@ -34,17 +34,17 @@ func (e *LocationError) fileName() string {
 }
 
 // FormatError implements errors.Formatter.
-func (e *LocationError) FormatError(p errors.Printer) (next error) {
+func (e *Error) FormatError(p errors.Printer) (next error) {
 	p.Printf("at %s%s", e.fileName(), e.Loc)
 	return e.Err
 }
 
 // Format implements fmt.Formatter.
-func (e *LocationError) Format(s fmt.State, verb rune) {
+func (e *Error) Format(s fmt.State, verb rune) {
 	errors.FormatError(e, s, verb)
 }
 
 // Error implements error.
-func (e *LocationError) Error() string {
+func (e *Error) Error() string {
 	return fmt.Sprintf("at %s%s: %s", e.fileName(), e.Loc, e.Err)
 }
diff --git a/internal/location/lines.go b/internal/location/lines.go
new file mode 100644
index 00000000..3b0dd7d6
--- /dev/null
+++ b/internal/location/lines.go
@@ -0,0 +1,74 @@
+package location
+
+import (
+	"bytes"
+	"sort"
+)
+
+// Lines is a sorted slice of newline offsets.
+type Lines struct {
+	data []byte
+	// lines stores newline offsets.
+	//
+	// idx is the line number (counts from 0).
+	lines []int
+}
+
+// Search returns the nearest line number to the given offset.
+//
+// NOTE: may return index bigger than lines length.
+func (l Lines) Search(offset int) int {
+	// The index is the line number.
+	lines := l.lines
+	idx := sort.Search(len(lines), func(i int) bool {
+		return lines[i] >= offset
+	})
+	return idx
+}
+
+// Line returns offset range of the line.
+//
+// NOTE: the line number is 1-based. Returns (-1, -1) if the line is invalid.
+func (l Lines) Line(n int) (start, end int) {
+	n--
+	end = len(l.data)
+	switch {
+	case n < 0:
+		// Line 0 is invalid.
+		return -1, -1
+	case n >= len(l.lines):
+		// Last line.
+		if len(l.lines) > 0 {
+			start = l.lines[len(l.lines)-1]
+		}
+		return start, end
+	default:
+		if n > 0 {
+			start = l.lines[n-1]
+		}
+		end = l.lines[n]
+		return start, end
+	}
+}
+
+// Collect fills the given slice with the offset of newlines.
+func (l *Lines) Collect(data []byte) {
+	l.data = data
+	l.lines = l.lines[:0]
+
+	var (
+		// Remaining data to process.
+		remain = data
+		// Absolute offset of the current line.
+		offset = 0
+	)
+	for {
+		idx := bytes.IndexByte(remain, '\n')
+		if idx < 0 {
+			break
+		}
+		l.lines = append(l.lines, offset+idx)
+		offset += idx + 1
+		remain = remain[idx+1:]
+	}
+}
diff --git a/json/location.go b/internal/location/location.go
similarity index 69%
rename from json/location.go
rename to internal/location/location.go
index ea10b37a..16569711 100644
--- a/json/location.go
+++ b/internal/location/location.go
@@ -1,89 +1,20 @@
-package json
+package location
 
 import (
-	"bytes"
 	"fmt"
-	"sort"
 	"strconv"
 
 	"gopkg.in/yaml.v3"
 )
 
-// Lines is a sorted slice of newline offsets.
-type Lines struct {
-	data []byte
-	// lines stores newline offsets.
-	//
-	// idx is the line number (counts from 0).
-	lines []int
-}
-
-// Search returns the nearest line number to the given offset.
-//
-// NOTE: may return index bigger than lines length.
-func (l Lines) Search(offset int) int {
-	// The index is the line number.
-	lines := l.lines
-	idx := sort.Search(len(lines), func(i int) bool {
-		return lines[i] >= offset
-	})
-	return idx
-}
-
-// Line returns offset range of the line.
-//
-// NOTE: the line number is 1-based. Returns (-1, -1) if the line is invalid.
-func (l Lines) Line(n int) (start, end int) {
-	n--
-	end = len(l.data)
-	switch {
-	case n < 0:
-		// Line 0 is invalid.
-		return -1, -1
-	case n >= len(l.lines):
-		// Last line.
-		if len(l.lines) > 0 {
-			start = l.lines[len(l.lines)-1]
-		}
-		return start, end
-	default:
-		if n > 0 {
-			start = l.lines[n-1]
-		}
-		end = l.lines[n]
-		return start, end
-	}
-}
-
-// Collect fills the given slice with the offset of newlines.
-func (l *Lines) Collect(data []byte) {
-	l.data = data
-	l.lines = l.lines[:0]
-
-	var (
-		// Remaining data to process.
-		remain = data
-		// Absolute offset of the current line.
-		offset = 0
-	)
-	for {
-		idx := bytes.IndexByte(remain, '\n')
-		if idx < 0 {
-			break
-		}
-		l.lines = append(l.lines, offset+idx)
-		offset += idx + 1
-		remain = remain[idx+1:]
-	}
-}
-
 // Location is a JSON value location.
 type Location struct {
 	Line, Column int
 	Node         *yaml.Node
 }
 
-func (l *Location) fromNode(node *yaml.Node) {
+// FromNode sets the location of the value from the given node.
+func (l *Location) FromNode(node *yaml.Node) {
 	*l = Location{
 		Line:   node.Line,
 		Column: node.Column,
@@ -91,10 +22,40 @@ func (l *Location) fromNode(node *yaml.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.
+//
+// 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
+	if n != nil && n.Kind == yaml.DocumentNode {
+		if len(n.Content) < 1 {
+			return l
+		}
+		n = n.Content[0]
+	}
+
+	if n == nil || n.Kind != yaml.MappingNode || len(n.Content) < 2 {
+		return l
+	}
+
+	children := n.Content
+	for i := 0; i < len(children); i += 2 {
+		keyNode := children[i]
+		if keyNode.Value == key {
+			loc.FromNode(keyNode)
+			return loc
+		}
+	}
+	return l
+}
+
 // 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.
 //
 // 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
 	if n != nil && n.Kind == yaml.DocumentNode {
@@ -112,7 +73,7 @@ func (l Location) Field(key string) (loc Location) {
 	for i := 0; i < len(children); i += 2 {
 		keyNode, valueNode := children[i], children[i+1]
 		if keyNode.Value == key {
-			loc.fromNode(valueNode)
+			loc.FromNode(valueNode)
 			return loc
 		}
 	}
@@ -138,7 +99,7 @@ func (l Location) Index(idx int) (loc Location) {
 	if idx < 0 || idx >= len(children) {
 		return l
 	}
-	loc.fromNode(children[idx])
+	loc.FromNode(children[idx])
 	return loc
 }
 
@@ -190,6 +151,16 @@ func (l Locator) Location() (Location, bool) {
 	return l.location, l.set
 }
 
+// Key tries to find the child node using given key and returns its location.
+//
+// See Key method of Location.
+func (l Locator) Key(key string) (loc Locator) {
+	if l.set {
+		loc.SetLocation(l.location.Key(key))
+	}
+	return
+}
+
 // Field tries to find the child node using given key and returns its location.
 //
 // See Field method of Location.
@@ -218,7 +189,7 @@ func (l *Locator) MarshalYAML(n *yaml.Node) error {
 // UnmarshalYAML implements yaml.Unmarshaler.
 func (l *Locator) UnmarshalYAML(n *yaml.Node) error {
 	var loc Location
-	loc.fromNode(n)
+	loc.FromNode(n)
 	l.SetLocation(loc)
 	return nil
 }
diff --git a/internal/location/pretty_error.go b/internal/location/pretty_error.go
new file mode 100644
index 00000000..80f0defe
--- /dev/null
+++ b/internal/location/pretty_error.go
@@ -0,0 +1,79 @@
+package location
+
+import (
+	"io"
+
+	"github.com/go-faster/errors"
+	"go.uber.org/multierr"
+	"gopkg.in/yaml.v3"
+)
+
+// PrintPrettyError prints the error in a pretty way and returns true if it was printed successfully.
+func PrintPrettyError(w io.Writer, 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:  contextLines,
+		}
+		_ = 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/json/location_print.go b/internal/location/print.go
similarity index 58%
rename from json/location_print.go
rename to internal/location/print.go
index 6051cbbd..34ef82b7 100644
--- a/json/location_print.go
+++ b/internal/location/print.go
@@ -1,4 +1,4 @@
-package json
+package location
 
 import (
 	"bytes"
@@ -33,25 +33,73 @@ func (p padLine) Format(f fmt.State, verb rune) {
 	_, _ = f.Write(b)
 }
 
-// PrettyError prints given message with line number and file listing to the writer.
+// PrintListingOptions is a set of options for PrintListing.
+type PrintListingOptions struct {
+	// Filename is a name of the file to print with location.
+	Filename string
+	// Context is the number of lines to print before and after the error line.
+	//
+	// If is zero, the default value 5 is used.
+	Context int
+	// If is nil, the default value color.New(color.FgRed) is used.
+	ErrColor *color.Color
+	// If is nil, the default value color.New(color.Reset) is used.
+	PlainColor *color.Color
+}
+
+func (o PrintListingOptions) contextLines(errLine int) (padNum, top, bottom int) {
+	context := o.Context
+
+	// Round up to the nearest odd number.
+	if context%2 == 0 {
+		context++
+	}
+	top, bottom = errLine-context/2, errLine+context/2
+
+	padNum = 2
+	if l := log10(bottom); l > 2 {
+		padNum = l
+	}
+
+	return padNum, top, bottom
+}
+
+func (o *PrintListingOptions) setDefaults() {
+	if o.Context == 0 {
+		o.Context = 5
+	}
+	if o.ErrColor == nil {
+		o.ErrColor = color.New(color.FgRed)
+	}
+	if o.PlainColor == nil {
+		o.PlainColor = color.New(color.Reset)
+	}
+}
+
+// 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) PrettyError(w io.Writer, filename, msg string, loc Location, context int) error {
-	tw := w
-	const (
-		leftPad           = "  "
-		verticalBorder    = "|"
-		verticalPointer   = "\u2191"
-		horizontalPointer = "\u2192"
-	)
+func (l Lines) PrintListing(w io.Writer, msg string, loc Location, opts PrintListingOptions) error {
+	opts.setDefaults()
 
 	// Line starts from 1, but index starts from 0.
 	errLine := loc.Line - 1
 	if len(l.data) == 0 || errLine < 0 || errLine >= len(l.lines) {
 		return errors.New("line number is out of range")
 	}
-	plainColor := color.New(color.Reset)
-	errColor := color.New(color.FgRed)
+
+	const (
+		leftPad           = "  "
+		verticalBorder    = "|"
+		verticalPointer   = "\u2191"
+		horizontalPointer = "\u2192"
+	)
+	var (
+		plainColor          = opts.PlainColor
+		errColor            = opts.ErrColor
+		filename            = opts.Filename
+		padNum, top, bottom = opts.contextLines(loc.Line)
+	)
 
 	if _, err := errColor.Fprintf(w, "%s- %s -> %s\n",
 		leftPad,
@@ -61,17 +109,6 @@ func (l Lines) PrettyError(w io.Writer, filename, msg string, loc Location, cont
 		return err
 	}
 
-	// Round up to the nearest odd number.
-	if context%2 == 0 {
-		context++
-	}
-	topContext, bottomContext := errLine-context/2, errLine+context/2
-
-	padNum := 2
-	if l := log10(bottomContext); l > 2 {
-		padNum = l
-	}
-
 	line := func(n int) []byte {
 		start, end := l.Line(n)
 		if start < 0 || end < 0 {
@@ -87,12 +124,12 @@ func (l Lines) PrettyError(w io.Writer, filename, msg string, loc Location, cont
 		}
 		// Line number is 1-based, but index is 0-based.
 		lineText := line(n + 1)
-		_, err := c.Fprintf(tw, "\t%s%d %s %s\t\n", leftPad, lineNumber, verticalBorder, lineText)
+		_, err := c.Fprintf(w, "\t%s%d %s %s\t\n", leftPad, lineNumber, verticalBorder, lineText)
 		return err
 	}
 
 	// Print top context.
-	for contextLine := topContext; contextLine < errLine; contextLine++ {
+	for contextLine := top; contextLine < errLine; contextLine++ {
 		if contextLine < 0 || contextLine >= len(l.lines) {
 			continue
 		}
@@ -108,7 +145,7 @@ func (l Lines) PrettyError(w io.Writer, filename, msg string, loc Location, cont
 
 	// Print column pointer.
 	if loc.Column > 0 {
-		if _, err := errColor.Fprintf(tw,
+		if _, err := errColor.Fprintf(w,
 			"\t%s%s %s %s%s\t\n",
 			leftPad,
 			strings.Repeat(" ", padNum+1),
@@ -121,7 +158,7 @@ func (l Lines) PrettyError(w io.Writer, filename, msg string, loc Location, cont
 	}
 
 	// Print bottom context.
-	for contextLine := errLine + 1; contextLine <= bottomContext; contextLine++ {
+	for contextLine := errLine + 1; contextLine <= bottom; contextLine++ {
 		if contextLine < 0 || contextLine >= len(l.lines) {
 			continue
 		}
diff --git a/jsonschema/errors.go b/jsonschema/errors.go
index f576e223..5644c925 100644
--- a/jsonschema/errors.go
+++ b/jsonschema/errors.go
@@ -1,13 +1,13 @@
 package jsonschema
 
 import (
-	ogenjson "github.com/ogen-go/ogen/json"
+	"github.com/ogen-go/ogen/internal/location"
 )
 
 // LocationError is a wrapper for an error that has a location.
-type LocationError = ogenjson.LocationError
+type LocationError = location.Error
 
-func (p *Parser) wrapLocation(filename string, l ogenjson.Locatable, err error) error {
+func (p *Parser) wrapLocation(filename string, l location.Locatable, err error) error {
 	if err == nil || l == nil || p == nil {
 		return err
 	}
diff --git a/jsonschema/raw_schema.go b/jsonschema/raw_schema.go
index a9096428..c04d5534 100644
--- a/jsonschema/raw_schema.go
+++ b/jsonschema/raw_schema.go
@@ -7,7 +7,7 @@ import (
 	"github.com/go-faster/jx"
 	"gopkg.in/yaml.v3"
 
-	ogenjson "github.com/ogen-go/ogen/json"
+	"github.com/ogen-go/ogen/internal/location"
 )
 
 // RawSchema is unparsed JSON Schema.
@@ -48,7 +48,7 @@ type RawSchema struct {
 	ContentMediaType     string                `json:"contentMediaType,omitempty" yaml:"contentMediaType,omitempty"`
 
 	XAnnotations map[string]json.RawMessage `json:"-" yaml:"-"`
-	Locator      ogenjson.Locator           `json:"-" yaml:",inline"`
+	Locator      location.Locator           `json:"-" yaml:",inline"`
 }
 
 // RawProperty is item of RawProperties.
diff --git a/jsonschema/schema.go b/jsonschema/schema.go
index cba988fb..7abd7f34 100644
--- a/jsonschema/schema.go
+++ b/jsonschema/schema.go
@@ -3,7 +3,7 @@ package jsonschema
 import (
 	"regexp"
 
-	ogenjson "github.com/ogen-go/ogen/json"
+	"github.com/ogen-go/ogen/internal/location"
 )
 
 // SchemaType is a JSON Schema type.
@@ -82,7 +82,7 @@ type Schema struct {
 	Default    interface{}
 	DefaultSet bool
 
-	ogenjson.Locator `json:"-" yaml:"-"`
+	location.Locator `json:"-" yaml:"-"`
 }
 
 // AddExample adds example for this Schema.
diff --git a/openapi/example.go b/openapi/example.go
index 82ed8d3b..c1d12b5b 100644
--- a/openapi/example.go
+++ b/openapi/example.go
@@ -1,7 +1,7 @@
 package openapi
 
 import (
-	ogenjson "github.com/ogen-go/ogen/json"
+	"github.com/ogen-go/ogen/internal/location"
 	"github.com/ogen-go/ogen/jsonschema"
 )
 
@@ -14,5 +14,5 @@ type Example struct {
 	Value         jsonschema.Example
 	ExternalValue string
 
-	ogenjson.Locator `json:"-" yaml:"-"`
+	location.Locator `json:"-" yaml:"-"`
 }
diff --git a/openapi/mediatype.go b/openapi/mediatype.go
index 499cfbcf..0018b78f 100644
--- a/openapi/mediatype.go
+++ b/openapi/mediatype.go
@@ -3,7 +3,7 @@ package openapi
 import (
 	"encoding/json"
 
-	ogenjson "github.com/ogen-go/ogen/json"
+	"github.com/ogen-go/ogen/internal/location"
 	"github.com/ogen-go/ogen/jsonschema"
 )
 
@@ -14,7 +14,7 @@ type MediaType struct {
 	Examples map[string]*Example
 	Encoding map[string]*Encoding
 
-	ogenjson.Locator `json:"-" yaml:"-"`
+	location.Locator `json:"-" yaml:"-"`
 }
 
 // Encoding is Encoding Type Object.
@@ -25,5 +25,5 @@ type Encoding struct {
 	Explode       bool
 	AllowReserved bool
 
-	ogenjson.Locator `json:"-" yaml:"-"`
+	location.Locator `json:"-" yaml:"-"`
 }
diff --git a/openapi/operation.go b/openapi/operation.go
index 6b449630..abafb3b3 100644
--- a/openapi/operation.go
+++ b/openapi/operation.go
@@ -1,8 +1,6 @@
 package openapi
 
-import (
-	ogenjson "github.com/ogen-go/ogen/json"
-)
+import "github.com/ogen-go/ogen/internal/location"
 
 // Operation is an OpenAPI Operation.
 type Operation struct {
@@ -28,7 +26,7 @@ type Operation struct {
 	//  * 1XX, 2XX, 3XX, 4XX, 5XX
 	Responses map[string]*Response
 
-	ogenjson.Locator `json:"-" yaml:"-"`
+	location.Locator `json:"-" yaml:"-"`
 }
 
 // Path is an operation path.
@@ -73,7 +71,7 @@ type RequestBody struct {
 	Required    bool
 	Content     map[string]*MediaType
 
-	ogenjson.Locator `json:"-" yaml:"-"`
+	location.Locator `json:"-" yaml:"-"`
 }
 
 // Header is an OpenAPI Header definition.
@@ -87,5 +85,5 @@ type Response struct {
 	Content     map[string]*MediaType
 	// Links map[string]*Link
 
-	ogenjson.Locator `json:"-" yaml:"-"`
+	location.Locator `json:"-" yaml:"-"`
 }
diff --git a/openapi/parameter.go b/openapi/parameter.go
index 2399253a..a476bd63 100644
--- a/openapi/parameter.go
+++ b/openapi/parameter.go
@@ -1,7 +1,7 @@
 package openapi
 
 import (
-	ogenjson "github.com/ogen-go/ogen/json"
+	"github.com/ogen-go/ogen/internal/location"
 	"github.com/ogen-go/ogen/jsonschema"
 )
 
@@ -45,7 +45,7 @@ type Parameter struct {
 	Explode     bool
 	Required    bool
 
-	ogenjson.Locator `json:"-" yaml:"-"`
+	location.Locator `json:"-" yaml:"-"`
 }
 
 // ParameterContent describes OpenAPI Parameter content field.
diff --git a/openapi/parser/errors.go b/openapi/parser/errors.go
index 9838ca69..8cfb27a6 100644
--- a/openapi/parser/errors.go
+++ b/openapi/parser/errors.go
@@ -1,13 +1,13 @@
 package parser
 
 import (
-	ogenjson "github.com/ogen-go/ogen/json"
+	"github.com/ogen-go/ogen/internal/location"
 )
 
 // LocationError is a wrapper for an error that has a location.
-type LocationError = ogenjson.LocationError
+type LocationError = location.Error
 
-func (p *parser) wrapLocation(filename string, l ogenjson.Locator, err error) error {
+func (p *parser) wrapLocation(filename string, l location.Locator, err error) error {
 	if err == nil || p == nil {
 		return err
 	}
@@ -25,7 +25,7 @@ func (p *parser) wrapLocation(filename string, l ogenjson.Locator, err error) er
 	}
 }
 
-func (p *parser) wrapField(field, filename string, l ogenjson.Locator, err error) error {
+func (p *parser) wrapField(field, filename string, l location.Locator, err error) error {
 	if err == nil || p == nil {
 		return err
 	}
diff --git a/openapi/parser/parse_header.go b/openapi/parser/parse_header.go
index e83f0ee7..a536f88f 100644
--- a/openapi/parser/parse_header.go
+++ b/openapi/parser/parse_header.go
@@ -53,7 +53,7 @@ func (p *parser) parseHeader(name string, header *ogen.Header, ctx *resolveCtx)
 	}
 	locatedIn := openapi.LocationHeader
 
-	if err := validateParameter(name, locatedIn, header); err != nil {
+	if err := p.validateParameter(name, locatedIn, header, ctx.lastLoc()); err != nil {
 		return nil, err
 	}
 
@@ -85,7 +85,7 @@ func (p *parser) parseHeader(name string, header *ogen.Header, ctx *resolveCtx)
 		return op, nil
 	}
 
-	if err := validateParamStyle(op); err != nil {
+	if err := p.validateParamStyle(op, ctx.lastLoc()); err != nil {
 		return nil, err
 	}
 
diff --git a/openapi/parser/parse_mediatype.go b/openapi/parser/parse_mediatype.go
index 95bcd006..829366c3 100644
--- a/openapi/parser/parse_mediatype.go
+++ b/openapi/parser/parse_mediatype.go
@@ -94,11 +94,11 @@ func (p *parser) parseMediaType(m ogen.Media, ctx *resolveCtx) (_ *openapi.Media
 			}
 			encoding.Headers, err = p.parseHeaders(e.Headers, ctx)
 			if err != nil {
-				return errors.Wrap(err, "headers")
+				return p.wrapField("headers", ctx.lastLoc(), e.Locator, err)
 			}
 			encodings[name] = encoding
 
-			if err := validateParamStyle(&openapi.Parameter{
+			if err := p.validateParamStyle(&openapi.Parameter{
 				Name:     name,
 				Schema:   prop.Schema,
 				In:       openapi.LocationQuery,
@@ -106,7 +106,7 @@ func (p *parser) parseMediaType(m ogen.Media, ctx *resolveCtx) (_ *openapi.Media
 				Explode:  encoding.Explode,
 				Required: prop.Required,
 				Locator:  encoding.Locator,
-			}); err != nil {
+			}, ctx.lastLoc()); err != nil {
 				return errors.Wrap(err, "param style")
 			}
 
diff --git a/openapi/parser/parse_parameter.go b/openapi/parser/parse_parameter.go
index d07400c5..efc733a8 100644
--- a/openapi/parser/parse_parameter.go
+++ b/openapi/parser/parse_parameter.go
@@ -71,22 +71,30 @@ func (p *parser) parseParams(params []*ogen.Parameter, ctx *resolveCtx) ([]*open
 	return result, nil
 }
 
-func validateParameter(name string, locatedIn openapi.ParameterLocation, param *ogen.Parameter) error {
+func (p *parser) validateParameter(
+	name string,
+	locatedIn openapi.ParameterLocation,
+	param *ogen.Parameter,
+	loc string,
+) error {
 	switch {
 	case param.Schema != nil && param.Content != nil:
-		return errors.New("parameter MUST contain either a schema property, or a content property, but not both")
+		err := errors.New("parameter MUST contain either a schema property, or a content property, but not both")
+		return p.wrapField("schema", loc, param.Locator, err)
 	case param.Schema == nil && param.Content == nil:
 		return errors.New("parameter MUST contain either a schema property, or a content property")
 	case param.Content != nil && len(param.Content) < 1:
 		// https://github.com/OAI/OpenAPI-Specification/discussions/2875
-		return errors.New("content must have at least one entry")
+		err := errors.New("content must have at least one entry")
+		return p.wrapField("content", loc, param.Locator, err)
 	}
 
 	// Path parameters are always required.
 	switch locatedIn {
 	case openapi.LocationPath:
 		if !param.Required {
-			return errors.New("path parameters must be required")
+			err := errors.New("path parameters must be required")
+			return p.wrapField("required", loc, param.Locator, err)
 		}
 	case openapi.LocationHeader:
 		if !httpguts.ValidHeaderFieldName(name) {
@@ -124,18 +132,20 @@ func (p *parser) parseParameter(param *ogen.Parameter, ctx *resolveCtx) (_ *open
 		return nil, p.wrapField("in", ctx.lastLoc(), param.Locator, err)
 	}
 
-	if err := validateParameter(param.Name, locatedIn, param); err != nil {
+	if err := p.validateParameter(param.Name, locatedIn, param, ctx.lastLoc()); err != nil {
 		return nil, err
 	}
 
 	schema, err := p.parseSchema(param.Schema, ctx)
 	if err != nil {
-		return nil, errors.Wrap(err, "schema")
+		err = errors.Wrap(err, "schema")
+		return nil, p.wrapField("schema", ctx.lastLoc(), param.Locator, err)
 	}
 
 	content, err := p.parseParameterContent(param.Content, ctx)
 	if err != nil {
-		return nil, errors.Wrap(err, "content")
+		err = errors.Wrap(err, "content")
+		return nil, p.wrapField("content", ctx.lastLoc(), param.Locator, err)
 	}
 
 	op := &openapi.Parameter{
@@ -155,7 +165,7 @@ func (p *parser) parseParameter(param *ogen.Parameter, ctx *resolveCtx) (_ *open
 		return op, nil
 	}
 
-	if err := validateParamStyle(op); err != nil {
+	if err := p.validateParamStyle(op, ctx.lastLoc()); err != nil {
 		return nil, err
 	}
 
@@ -191,7 +201,7 @@ func inferParamExplode(locatedIn openapi.ParameterLocation, explode *bool) bool
 	return false
 }
 
-func validateParamStyle(p *openapi.Parameter) error {
+func (p *parser) validateParamStyle(param *openapi.Parameter, loc string) error {
 	// https://swagger.io/docs/specification/serialization/
 	const (
 		primitive byte = 1 << iota
@@ -231,20 +241,23 @@ func validateParamStyle(p *openapi.Parameter) error {
 			{openapi.CookieStyleForm, false}: primitive | array | object,
 		},
 	}
+	wrap := func(field string, err error) error {
+		return p.wrapField(field, loc, param.Locator, err)
+	}
 
-	styles, ok := table[p.In]
+	styles, ok := table[param.In]
 	if !ok {
-		return errors.Errorf("invalid in: %q", p.In)
+		return wrap("in", errors.Errorf(`invalid "in": %q`, param.In))
 	}
 
-	types, ok := styles[stexp{p.Style, p.Explode}]
+	types, ok := styles[stexp{param.Style, param.Explode}]
 	if !ok {
-		return errors.Errorf("invalid style explode combination %q, explode:%v", p.Style, p.Explode)
+		return errors.Errorf("invalid style explode combination %q, explode:%v", param.Style, param.Explode)
 	}
 
 	allowed := func(t byte) bool { return types&t != 0 }
 
-	switch p.Schema.Type {
+	switch param.Schema.Type {
 	case jsonschema.String, jsonschema.Integer, jsonschema.Number, jsonschema.Boolean:
 		if allowed(primitive) {
 			return nil
@@ -258,8 +271,8 @@ func validateParamStyle(p *openapi.Parameter) error {
 			return nil
 		}
 	case jsonschema.Empty:
-		if p.Schema.OneOf != nil {
-			for _, s := range p.Schema.OneOf {
+		if param.Schema.OneOf != nil {
+			for _, s := range param.Schema.OneOf {
 				switch s.Type {
 				case jsonschema.String, jsonschema.Integer, jsonschema.Number, jsonschema.Boolean:
 					// ok
@@ -275,5 +288,5 @@ func validateParamStyle(p *openapi.Parameter) error {
 	}
 
 	return errors.Errorf("invalid schema:style:explode combination: (%q:%q:%v)",
-		p.Schema.Type, p.Style, p.Explode)
+		param.Schema.Type, param.Style, param.Explode)
 }
diff --git a/openapi/parser/parse_request_body.go b/openapi/parser/parse_request_body.go
index db4ebea1..221f89e1 100644
--- a/openapi/parser/parse_request_body.go
+++ b/openapi/parser/parse_request_body.go
@@ -23,14 +23,21 @@ func (p *parser) parseRequestBody(body *ogen.RequestBody, ctx *resolveCtx) (_ *o
 		return reqBody, nil
 	}
 
-	if len(body.Content) < 1 {
-		// See https://github.com/OAI/OpenAPI-Specification/discussions/2875.
-		return nil, errors.New("content must have at least one entry")
-	}
+	content, err := func() (map[string]*openapi.MediaType, error) {
+		if len(body.Content) < 1 {
+			// See https://github.com/OAI/OpenAPI-Specification/discussions/2875.
+			return nil, errors.New("content must have at least one entry")
+		}
+
+		content, err := p.parseContent(body.Content, ctx)
+		if err != nil {
+			return nil, errors.Wrap(err, "parse content")
+		}
 
-	content, err := p.parseContent(body.Content, ctx)
+		return content, nil
+	}()
 	if err != nil {
-		return nil, errors.Wrap(err, "content")
+		return nil, p.wrapField("content", ctx.lastLoc(), body.Locator, err)
 	}
 
 	return &openapi.RequestBody{
diff --git a/openapi/parser/parse_response.go b/openapi/parser/parse_response.go
index 6ff1cbf2..259d3460 100644
--- a/openapi/parser/parse_response.go
+++ b/openapi/parser/parse_response.go
@@ -9,19 +9,6 @@ import (
 	"github.com/ogen-go/ogen/openapi"
 )
 
-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, ctx)
-	if err != nil {
-		return nil, err
-	}
-
-	return resp, nil
-}
-
 func (p *parser) parseResponses(responses ogen.Responses, ctx *resolveCtx) (_ map[string]*openapi.Response, err error) {
 	if len(responses) == 0 {
 		return nil, errors.New("no responses")
@@ -38,6 +25,19 @@ func (p *parser) parseResponses(responses ogen.Responses, ctx *resolveCtx) (_ ma
 	return result, nil
 }
 
+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, ctx)
+	if err != nil {
+		return nil, err
+	}
+
+	return resp, nil
+}
+
 func (p *parser) parseResponse(resp *ogen.Response, ctx *resolveCtx) (_ *openapi.Response, rerr error) {
 	if resp == nil {
 		return nil, errors.New("response object is empty or null")
@@ -56,12 +56,14 @@ func (p *parser) parseResponse(resp *ogen.Response, ctx *resolveCtx) (_ *openapi
 
 	content, err := p.parseContent(resp.Content, ctx)
 	if err != nil {
-		return nil, errors.Wrap(err, "content")
+		err := errors.Wrap(err, "content")
+		return nil, p.wrapField("content", ctx.lastLoc(), resp.Locator, err)
 	}
 
 	headers, err := p.parseHeaders(resp.Headers, ctx)
 	if err != nil {
-		return nil, errors.Wrap(err, "headers")
+		err := errors.Wrap(err, "headers")
+		return nil, p.wrapField("headers", ctx.lastLoc(), resp.Locator, err)
 	}
 
 	return &openapi.Response{
diff --git a/openapi/parser/parse_security.go b/openapi/parser/parse_security.go
index 2160001a..a6685966 100644
--- a/openapi/parser/parse_security.go
+++ b/openapi/parser/parse_security.go
@@ -12,7 +12,6 @@ import (
 
 func (p *parser) parseSecurityScheme(
 	scheme *ogen.SecurityScheme,
-	scopes []string,
 	ctx *resolveCtx,
 ) (_ *ogen.SecurityScheme, rerr error) {
 	if scheme == nil {
@@ -36,10 +35,12 @@ func (p *parser) parseSecurityScheme(
 			switch scheme.In {
 			case "query", "header", "cookie":
 			default:
-				return errors.Errorf(`invalid "in": %q`, scheme.In)
+				err := errors.Errorf(`invalid "in": %q`, scheme.In)
+				return p.wrapField("in", ctx.lastLoc(), scheme.Locator, err)
 			}
 			if scheme.Name == "" {
-				return errors.New(`"name" is required and MUST be a non-empty string`)
+				err := errors.New(`"name" is required and MUST be a non-empty string`)
+				return p.wrapField("name", ctx.lastLoc(), scheme.Locator, err)
 			}
 			return nil
 		case "http":
@@ -60,20 +61,23 @@ func (p *parser) parseSecurityScheme(
 				"scram-sha-256",
 				"vapid":
 			default:
-				return errors.Errorf(`invalid "scheme": %q`, scheme.Scheme)
+				err := errors.Errorf(`invalid "scheme": %q`, scheme.Scheme)
+				return p.wrapField("name", ctx.lastLoc(), scheme.Locator, err)
 			}
 			return nil
 		case "mutualTLS":
 			return nil
 		case "oauth2":
-			return p.validateOAuthFlows(scopes, scheme.Flows, ctx.lastLoc())
+			return p.validateOAuthFlows(scheme.Flows, ctx.lastLoc())
 		case "openIdConnect":
 			if _, err := url.ParseRequestURI(scheme.OpenIDConnectURL); err != nil {
-				return errors.Wrap(err, `"openIdConnectUrl" MUST be in the form of a URL`)
+				err := errors.Wrap(err, `"openIdConnectUrl" MUST be in the form of a URL`)
+				return p.wrapField("openIdConnectUrl", ctx.lastLoc(), scheme.Locator, err)
 			}
 			return nil
 		default:
-			return errors.Errorf("unknown security scheme type %q", scheme.Type)
+			err := errors.Errorf("unknown security scheme type %q", scheme.Type)
+			return p.wrapField("type", ctx.lastLoc(), scheme.Locator, err)
 		}
 	}(); err != nil {
 		return nil, errors.Wrap(err, scheme.Type)
@@ -82,7 +86,27 @@ func (p *parser) parseSecurityScheme(
 	return scheme, nil
 }
 
-func (p *parser) validateOAuthFlows(scopes []string, flows *ogen.OAuthFlows, loc string) (rerr error) {
+func forEachFlow(flows *ogen.OAuthFlows, cb func(flow *ogen.OAuthFlow, authURL, tokenURL bool) error) error {
+	for flowName, v := range map[string]struct {
+		flow              *ogen.OAuthFlow
+		authURL, tokenURL bool
+	}{
+		"implicit":          {flows.Implicit, true, false},
+		"password":          {flows.Password, false, true},
+		"clientCredentials": {flows.ClientCredentials, false, true},
+		"authorizationCode": {flows.AuthorizationCode, true, true},
+	} {
+		if v.flow == nil {
+			continue
+		}
+		if err := cb(v.flow, v.authURL, v.tokenURL); err != nil {
+			return errors.Wrapf(err, "flow %q", flowName)
+		}
+	}
+	return nil
+}
+
+func (p *parser) validateOAuthFlows(flows *ogen.OAuthFlows, loc string) (rerr error) {
 	if flows == nil {
 		return errors.New("oAuthFlows is empty or null")
 	}
@@ -98,45 +122,30 @@ func (p *parser) validateOAuthFlows(scopes []string, flows *ogen.OAuthFlows, loc
 			rerr = p.wrapLocation(loc, flow.Locator, rerr)
 		}()
 
-		if tokenURL {
-			if _, err := url.ParseRequestURI(flow.TokenURL); err != nil {
-				return errors.Wrap(err, `"tokenUrl" MUST be in the form of a URL`)
+		checkURL := func(name, input string, check bool) error {
+			if !check {
+				return nil
 			}
-		}
-		if authURL {
-			if _, err := url.ParseRequestURI(flow.AuthorizationURL); err != nil {
-				return errors.Wrap(err, `"authorizationUrl" MUST be in the form of a URL`)
-			}
-		}
-		if flow.RefreshURL != "" {
-			if _, err := url.ParseRequestURI(flow.RefreshURL); err != nil {
-				return errors.Wrap(err, `"refreshUrl" MUST be in the form of a URL`)
+			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, flow.Locator, err)
 			}
+			return nil
 		}
 
-		for _, scope := range scopes {
-			if _, ok := flow.Scopes[scope]; !ok {
-				return errors.Errorf("unknown scope %q", scope)
-			}
+		if err := checkURL("tokenUrl", flow.TokenURL, tokenURL); err != nil {
+			return err
 		}
-		return nil
-	}
-
-	for flowName, v := range map[string]struct {
-		flow              *ogen.OAuthFlow
-		authURL, tokenURL bool
-	}{
-		"implicit":          {flows.Implicit, true, false},
-		"password":          {flows.Password, false, true},
-		"clientCredentials": {flows.ClientCredentials, false, true},
-		"authorizationCode": {flows.AuthorizationCode, true, true},
-	} {
-		if err := check(v.flow, v.authURL, v.tokenURL); err != nil {
-			return errors.Wrapf(err, "flow %q", flowName)
+		if err := checkURL("authorizationUrl", flow.AuthorizationURL, authURL); err != nil {
+			return err
+		}
+		if err := checkURL("refreshUrl", flow.RefreshURL, flow.RefreshURL != ""); err != nil {
+			return err
 		}
+		return nil
 	}
 
-	return nil
+	return forEachFlow(flows, check)
 }
 
 func cloneOAuthFlows(flows ogen.OAuthFlows) (r openapi.OAuthFlows) {
@@ -178,11 +187,19 @@ func (p *parser) parseSecurityRequirements(
 				return nil, errors.Errorf("unknown security schema %q", requirementName)
 			}
 
-			spec, err := p.parseSecurityScheme(v, scopes, ctx)
+			spec, err := p.parseSecurityScheme(v, ctx)
 			if err != nil {
 				return nil, errors.Wrapf(err, "parse security scheme %q", requirementName)
 			}
 
+			if len(scopes) > 0 {
+				switch spec.Type {
+				case "openIdConnect", "oauth2":
+				default:
+					return nil, errors.Errorf(`list of scopes MUST be empty for "type" %q`, spec.Type)
+				}
+			}
+
 			var flows ogen.OAuthFlows
 			if spec.Flows != nil {
 				flows = *spec.Flows
diff --git a/openapi/parser/parser.go b/openapi/parser/parser.go
index 8ad7d0ae..f324d6a1 100644
--- a/openapi/parser/parser.go
+++ b/openapi/parser/parser.go
@@ -5,6 +5,7 @@ import (
 	"gopkg.in/yaml.v3"
 
 	"github.com/ogen-go/ogen"
+	"github.com/ogen-go/ogen/internal/location"
 	"github.com/ogen-go/ogen/jsonschema"
 	"github.com/ogen-go/ogen/openapi"
 )
@@ -12,6 +13,8 @@ import (
 type parser struct {
 	// api spec, immutable.
 	spec *ogen.Spec
+	// root location of the spec, immutable.
+	rootLoc location.Locator
 	// parsed operations.
 	operations []*openapi.Operation
 	// refs contains lazy-initialized referenced components.
@@ -75,6 +78,11 @@ func Parse(spec *ogen.Spec, s Settings) (*openapi.API, error) {
 			InferTypes: s.InferTypes,
 		}),
 	}
+	if spec.Raw != nil {
+		var loc location.Location
+		loc.FromNode(spec.Raw)
+		p.rootLoc.SetLocation(loc)
+	}
 
 	for name, s := range spec.Components.SecuritySchemes {
 		p.refs.securitySchemes[name] = s
@@ -116,7 +124,9 @@ func (p *parser) parsePathItems() error {
 			}
 
 			if _, ok := paths[id]; ok {
-				return errors.New("duplicate path")
+				pathLoc := p.rootLoc.Field("paths").Key(path)
+				err := errors.Errorf("duplicate path: %q", path)
+				return p.wrapLocation("", pathLoc, err)
 			}
 			paths[id] = struct{}{}
 
@@ -218,13 +228,16 @@ func (p *parser) parseOp(
 		// Use operation level security.
 		op.Security, err = p.parseSecurityRequirements(spec.Security, ctx)
 		if err != nil {
-			return nil, errors.Wrap(err, "security")
+			err = errors.Wrap(err, "security")
+			return nil, p.wrapField("security", ctx.lastLoc(), spec.Locator, err)
 		}
 	} else {
 		// Use root level security.
 		op.Security, err = p.parseSecurityRequirements(p.spec.Security, ctx)
 		if err != nil {
-			return nil, errors.Wrap(err, "security")
+			loc := p.rootLoc.Field("security")
+			err = errors.Wrap(err, "security")
+			return nil, p.wrapField("security", ctx.lastLoc(), loc, err)
 		}
 	}
 
diff --git a/openapi/security.go b/openapi/security.go
index c638d179..9601dddc 100644
--- a/openapi/security.go
+++ b/openapi/security.go
@@ -1,8 +1,6 @@
 package openapi
 
-import (
-	ogenjson "github.com/ogen-go/ogen/json"
-)
+import "github.com/ogen-go/ogen/internal/location"
 
 // SecurityRequirements is parsed security requirements.
 type SecurityRequirements struct {
@@ -22,7 +20,7 @@ type Security struct {
 	Flows            OAuthFlows
 	OpenIDConnectURL string
 
-	ogenjson.Locator `json:"-" yaml:"-"`
+	location.Locator `json:"-" yaml:"-"`
 }
 
 // OAuthFlows allows configuration of the supported OAuth Flows.
@@ -32,7 +30,7 @@ type OAuthFlows struct {
 	ClientCredentials *OAuthFlow
 	AuthorizationCode *OAuthFlow
 
-	ogenjson.Locator `json:"-" yaml:"-"`
+	location.Locator `json:"-" yaml:"-"`
 }
 
 // OAuthFlow is configuration details for a supported OAuth Flow.
@@ -42,5 +40,5 @@ type OAuthFlow struct {
 	RefreshURL       string
 	Scopes           map[string]string // name -> description
 
-	ogenjson.Locator `json:"-" yaml:"-"`
+	location.Locator `json:"-" yaml:"-"`
 }
diff --git a/spec.go b/spec.go
index fdaff934..db59cb12 100644
--- a/spec.go
+++ b/spec.go
@@ -5,7 +5,7 @@ import (
 
 	"gopkg.in/yaml.v3"
 
-	ogenjson "github.com/ogen-go/ogen/json"
+	"github.com/ogen-go/ogen/internal/location"
 	"github.com/ogen-go/ogen/jsonschema"
 )
 
@@ -20,7 +20,7 @@ type (
 	ExampleValue = jsonschema.Example
 
 	// Locator stores location of JSON value.
-	Locator = ogenjson.Locator
+	Locator = location.Locator
 )
 
 // Spec is the root document object of the OpenAPI document.


Test patch:
diff --git a/_testdata/negative/securityScheme/invalid_name.json b/_testdata/negative/securityScheme/invalid_apiKey_name.json
similarity index 88%
rename from _testdata/negative/securityScheme/invalid_name.json
rename to _testdata/negative/securityScheme/invalid_apiKey_name.json
index 25d89e44..427b9389 100644
--- a/_testdata/negative/securityScheme/invalid_name.json
+++ b/_testdata/negative/securityScheme/invalid_apiKey_name.json
@@ -9,9 +9,7 @@
       "get": {
         "security": [
           {
-            "api_key": [
-              "read"
-            ]
+            "api_key": []
           }
         ],
         "responses": {
@@ -26,6 +24,7 @@
     "securitySchemes": {
       "api_key": {
         "type": "apiKey",
+        "name": "",
         "in": "query"
       }
     }
diff --git a/_testdata/negative/securityScheme/invalid_oauth2_implicit_authorizationUrl.json b/_testdata/negative/securityScheme/invalid_oauth2_implicit_authorizationUrl.json
new file mode 100644
index 00000000..72f600c8
--- /dev/null
+++ b/_testdata/negative/securityScheme/invalid_oauth2_implicit_authorizationUrl.json
@@ -0,0 +1,40 @@
+{
+  "openapi": "3.0.3",
+  "info": {
+    "title": "title",
+    "version": "v0.1.0"
+  },
+  "paths": {
+    "/foo": {
+      "get": {
+        "security": [
+          {
+            "api_key": [
+              "read"
+            ]
+          }
+        ],
+        "responses": {
+          "default": {
+            "description": "default"
+          }
+        }
+      }
+    }
+  },
+  "components": {
+    "securitySchemes": {
+      "api_key": {
+        "type": "oauth2",
+        "flows": {
+          "implicit": {
+            "authorizationUrl": "-",
+            "scopes": {
+              "read": "read"
+            }
+          }
+        }
+      }
+    }
+  }
+}
diff --git a/_testdata/negative/securityScheme/unknown_oauth2_scope.json b/_testdata/negative/securityScheme/invalid_oauth2_implicit_refreshUrl.json
similarity index 88%
rename from _testdata/negative/securityScheme/unknown_oauth2_scope.json
rename to _testdata/negative/securityScheme/invalid_oauth2_implicit_refreshUrl.json
index d4c84001..54a6156f 100644
--- a/_testdata/negative/securityScheme/unknown_oauth2_scope.json
+++ b/_testdata/negative/securityScheme/invalid_oauth2_implicit_refreshUrl.json
@@ -15,7 +15,7 @@
           }
         ],
         "responses": {
-          "200": {
+          "default": {
             "description": "default"
           }
         }
@@ -29,8 +29,9 @@
         "flows": {
           "implicit": {
             "authorizationUrl": "https://example.com/oauth/authorize",
+            "refreshUrl": "-",
             "scopes": {
-              "foobar": "read access"
+              "read": "read"
             }
           }
         }
diff --git a/_testdata/negative/securityScheme/invalid_oauth2_password_tokenUrl.json b/_testdata/negative/securityScheme/invalid_oauth2_password_tokenUrl.json
new file mode 100644
index 00000000..137685cd
--- /dev/null
+++ b/_testdata/negative/securityScheme/invalid_oauth2_password_tokenUrl.json
@@ -0,0 +1,40 @@
+{
+  "openapi": "3.0.3",
+  "info": {
+    "title": "title",
+    "version": "v0.1.0"
+  },
+  "paths": {
+    "/foo": {
+      "get": {
+        "security": [
+          {
+            "api_key": [
+              "read"
+            ]
+          }
+        ],
+        "responses": {
+          "default": {
+            "description": "default"
+          }
+        }
+      }
+    }
+  },
+  "components": {
+    "securitySchemes": {
+      "api_key": {
+        "type": "oauth2",
+        "flows": {
+          "password": {
+            "tokenUrl": "-",
+            "scopes": {
+              "read": "read"
+            }
+          }
+        }
+      }
+    }
+  }
+}
diff --git a/_testdata/negative/securityScheme/unexpected_scopes.json b/_testdata/negative/securityScheme/unexpected_scopes.json
new file mode 100644
index 00000000..d8fc13fd
--- /dev/null
+++ b/_testdata/negative/securityScheme/unexpected_scopes.json
@@ -0,0 +1,34 @@
+{
+  "openapi": "3.0.3",
+  "info": {
+    "title": "title",
+    "version": "v0.1.0"
+  },
+  "paths": {
+    "/foo": {
+      "get": {
+        "security": [
+          {
+            "api_key": [
+              "read"
+            ]
+          }
+        ],
+        "responses": {
+          "200": {
+            "description": "default"
+          }
+        }
+      }
+    }
+  },
+  "components": {
+    "securitySchemes": {
+      "api_key": {
+        "type": "apiKey",
+        "name": "api_key",
+        "in": "header"
+      }
+    }
+  }
+}
diff --git a/_testdata/negative/securityScheme/unknown_in.json b/_testdata/negative/securityScheme/unknown_apiKey_in.json
similarity index 88%
rename from _testdata/negative/securityScheme/unknown_in.json
rename to _testdata/negative/securityScheme/unknown_apiKey_in.json
index 3dafdc53..5fcbb2bf 100644
--- a/_testdata/negative/securityScheme/unknown_in.json
+++ b/_testdata/negative/securityScheme/unknown_apiKey_in.json
@@ -9,9 +9,7 @@
       "get": {
         "security": [
           {
-            "api_key": [
-              "read"
-            ]
+            "api_key": []
           }
         ],
         "responses": {
diff --git a/_testdata/negative/securityScheme/unknown_scheme.json b/_testdata/negative/securityScheme/unknown_http_scheme.json
similarity index 88%
rename from _testdata/negative/securityScheme/unknown_scheme.json
rename to _testdata/negative/securityScheme/unknown_http_scheme.json
index ab8b9dea..d5496ff6 100644
--- a/_testdata/negative/securityScheme/unknown_scheme.json
+++ b/_testdata/negative/securityScheme/unknown_http_scheme.json
@@ -9,9 +9,7 @@
       "get": {
         "security": [
           {
-            "api_key": [
-              "read"
-            ]
+            "api_key": []
           }
         ],
         "responses": {
diff --git a/gen_test.go b/gen_test.go
index 2d9dad98..b80f278c 100644
--- a/gen_test.go
+++ b/gen_test.go
@@ -12,6 +12,7 @@ import (
 	"github.com/ogen-go/ogen"
 	"github.com/ogen-go/ogen/gen"
 	"github.com/ogen-go/ogen/gen/genfs"
+	"github.com/ogen-go/ogen/internal/location"
 )
 
 func testGenerate(t *testing.T, filename string, data []byte, ignore ...string) {
@@ -129,6 +130,12 @@ func TestNegative(t *testing.T) {
 			Filename: name,
 		})
 		a.Error(err)
-		t.Logf("%+v", err)
+
+		var buf strings.Builder
+		if location.PrintPrettyError(&buf, name, data, err) {
+			t.Log(buf.String())
+		} else {
+			t.Logf("%+v", err)
+		}
 	})
 }
diff --git a/json/location_test.go b/internal/location/lines_test.go
similarity index 54%
rename from json/location_test.go
rename to internal/location/lines_test.go
index a1ab50ac..04440565 100644
--- a/json/location_test.go
+++ b/internal/location/lines_test.go
@@ -1,4 +1,4 @@
-package json
+package location
 
 import (
 	"fmt"
@@ -6,8 +6,6 @@ import (
 	"testing"
 
 	"github.com/stretchr/testify/assert"
-	"github.com/stretchr/testify/require"
-	"gopkg.in/yaml.v3"
 )
 
 func TestLines_Collect(t *testing.T) {
@@ -67,66 +65,3 @@ func TestLines_Line(t *testing.T) {
 		})
 	}
 }
-
-func TestLocation_Field(t *testing.T) {
-	input := `{
-  "a": 1,
-  "b": {
-    "c": 2
-  }
-}`
-	a := require.New(t)
-
-	var node yaml.Node
-	a.NoError(yaml.Unmarshal([]byte(input), &node))
-
-	var loc Location
-	loc.fromNode(&node)
-	a.Equal(1, loc.Line)
-	a.Equal(1, loc.Column)
-	a.Equal(loc, loc.Field("abc"))
-
-	loc = loc.Field("b")
-	a.Equal("!!map", loc.Node.ShortTag())
-	a.Equal(3, loc.Line)
-	a.Equal(8, loc.Column)
-
-	loc = loc.Field("c")
-	a.Equal("2", loc.Node.Value)
-	a.Equal(4, loc.Line)
-	a.Equal(10, loc.Column)
-}
-
-func TestLocation_Index(t *testing.T) {
-	input := `[
-  1,
-  2.125
-]`
-	a := require.New(t)
-
-	var node yaml.Node
-	a.NoError(yaml.Unmarshal([]byte(input), &node))
-
-	var loc Location
-	loc.fromNode(&node)
-	a.Equal(1, loc.Line)
-	a.Equal(1, loc.Column)
-	a.Equal(loc, loc.Index(-10))
-	a.Equal(loc, loc.Index(2))
-
-	{
-		loc := loc.Index(0)
-		a.Equal("!!int", loc.Node.ShortTag())
-		a.Equal("1", loc.Node.Value)
-		a.Equal(2, loc.Line)
-		a.Equal(3, loc.Column)
-	}
-
-	{
-		loc := loc.Index(1)
-		a.Equal("!!float", loc.Node.ShortTag())
-		a.Equal("2.125", loc.Node.Value)
-		a.Equal(3, loc.Line)
-		a.Equal(3, loc.Column)
-	}
-}
diff --git a/internal/location/location_test.go b/internal/location/location_test.go
new file mode 100644
index 00000000..bf4a9089
--- /dev/null
+++ b/internal/location/location_test.go
@@ -0,0 +1,106 @@
+package location
+
+import (
+	"testing"
+
+	"github.com/stretchr/testify/require"
+	"gopkg.in/yaml.v3"
+)
+
+func TestLocation_Key(t *testing.T) {
+	input := `{
+  "a": 1,
+  "b": {
+    "c": 2
+  }
+}`
+	a := require.New(t)
+
+	var node yaml.Node
+	a.NoError(yaml.Unmarshal([]byte(input), &node))
+
+	var loc Location
+	loc.FromNode(&node)
+	a.Equal(1, loc.Line)
+	a.Equal(1, loc.Column)
+	a.Equal(loc, loc.Key("abc"))
+
+	{
+		loc := loc.Key("b")
+		a.Equal("!!str", loc.Node.ShortTag())
+		a.Equal("b", loc.Node.Value)
+		a.Equal(3, loc.Line)
+		a.Equal(3, loc.Column)
+	}
+
+	{
+		loc := loc.Field("b").Key("c")
+		a.Equal("!!str", loc.Node.ShortTag())
+		a.Equal("c", loc.Node.Value)
+		a.Equal(4, loc.Line)
+		a.Equal(5, loc.Column)
+	}
+}
+
+func TestLocation_Field(t *testing.T) {
+	input := `{
+  "a": 1,
+  "b": {
+    "c": 2
+  }
+}`
+	a := require.New(t)
+
+	var node yaml.Node
+	a.NoError(yaml.Unmarshal([]byte(input), &node))
+
+	var loc Location
+	loc.FromNode(&node)
+	a.Equal(1, loc.Line)
+	a.Equal(1, loc.Column)
+	a.Equal(loc, loc.Field("abc"))
+
+	loc = loc.Field("b")
+	a.Equal("!!map", loc.Node.ShortTag())
+	a.Equal(3, loc.Line)
+	a.Equal(8, loc.Column)
+
+	loc = loc.Field("c")
+	a.Equal("2", loc.Node.Value)
+	a.Equal(4, loc.Line)
+	a.Equal(10, loc.Column)
+}
+
+func TestLocation_Index(t *testing.T) {
+	input := `[
+  1,
+  2.125
+]`
+	a := require.New(t)
+
+	var node yaml.Node
+	a.NoError(yaml.Unmarshal([]byte(input), &node))
+
+	var loc Location
+	loc.FromNode(&node)
+	a.Equal(1, loc.Line)
+	a.Equal(1, loc.Column)
+	a.Equal(loc, loc.Index(-10))
+	a.Equal(loc, loc.Index(2))
+
+	{
+		loc := loc.Index(0)
+		a.Equal("!!int", loc.Node.ShortTag())
+		a.Equal("1", loc.Node.Value)
+		a.Equal(2, loc.Line)
+		a.Equal(3, loc.Column)
+	}
+
+	{
+		loc := loc.Index(1)
+		a.Equal("!!float", loc.Node.ShortTag())
+		a.Equal("2.125", loc.Node.Value)
+		a.Equal(3, loc.Line)
+		a.Equal(3, loc.Column)
+	}
+}
diff --git a/jsonschema/resolve_test.go b/jsonschema/resolve_test.go
index c136714d..b424a854 100644
--- a/jsonschema/resolve_test.go
+++ b/jsonschema/resolve_test.go
@@ -9,7 +9,7 @@ import (
 	"github.com/go-faster/jx"
 	"github.com/stretchr/testify/require"
 
-	ogenjson "github.com/ogen-go/ogen/json"
+	"github.com/ogen-go/ogen/internal/location"
 )
 
 type external map[string]components
@@ -137,7 +137,7 @@ func TestExternalReference(t *testing.T) {
 }
 
 func zeroLocator(s *Schema) {
-	var zeroed ogenjson.Locator
+	var zeroed location.Locator
 	if s == nil {
 		return
 	}
diff --git a/location_test.go b/location_test.go
index 7f9110c1..cd024ffa 100644
--- a/location_test.go
+++ b/location_test.go
@@ -10,7 +10,7 @@ import (
 	"github.com/stretchr/testify/require"
 	"gopkg.in/yaml.v3"
 
-	ogenjson "github.com/ogen-go/ogen/json"
+	"github.com/ogen-go/ogen/internal/location"
 )
 
 func TestLocator(t *testing.T) {
@@ -97,27 +97,27 @@ var (
 )
 
 func TestLocation(t *testing.T) {
-	createEqualLoc := func(a *assert.Assertions, data []byte) func(l ogenjson.Locatable, line, column int) {
-		var lines ogenjson.Lines
+	createEqualLoc := func(a *assert.Assertions, data []byte) func(l location.Locatable, line, column int) {
+		var lines location.Lines
 		lines.Collect(data)
-		return func(l ogenjson.Locatable, line, column int) {
+		return func(l location.Locatable, line, column int) {
 			t.Helper()
 
 			loc, ok := l.Location()
 			a.True(ok)
-			type location struct {
-				Line, Column int
-				Data         string
-			}
 			getLine := func(n int) string {
 				start, end := lines.Line(n)
 				// Offset points exactly to the newline, trim it.
 				return strings.Trim(string(data[start:end]), "\n\r")
 			}
 
+			type compareLoc struct {
+				Line, Column int
+				Data         string
+			}
 			a.Equal(
-				location{line, column, getLine(line)},
-				location{loc.Line, loc.Column, getLine(loc.Line)},
+				compareLoc{line, column, getLine(line)},
+				compareLoc{loc.Line, loc.Column, getLine(loc.Line)},
 			)
 		}
 	}
diff --git a/openapi/parser/parse_security_test.go b/openapi/parser/parse_security_test.go
index c2ccc089..fd35f400 100644
--- a/openapi/parser/parse_security_test.go
+++ b/openapi/parser/parse_security_test.go
@@ -24,44 +24,39 @@ func Test_validateOAuthFlows(t *testing.T) {
 	}
 
 	tests := []struct {
-		scopes  []string
 		flows   *ogen.OAuthFlows
 		wantErr bool
 	}{
 		// OAuthFlows is required.
-		{nil, nil, true},
-		{nil, new(ogen.OAuthFlows), false},
+		{nil, true},
+		{new(ogen.OAuthFlows), false},
 
 		// `implicit` requires `authorizationUrl`.
-		{nil, implicit(ogen.OAuthFlow{}), true},
+		{implicit(ogen.OAuthFlow{}), true},
 		// `authCode` requires `authorizationUrl` and `tokenUrl`.
-		{nil, authCode(ogen.OAuthFlow{}), true},
+		{authCode(ogen.OAuthFlow{}), true},
 		// `password` requires `tokenUrl`.
-		{nil, password(ogen.OAuthFlow{}), true},
+		{password(ogen.OAuthFlow{}), true},
 		// `clientCredentials` requires `tokenUrl`.
-		{nil, clientCreds(ogen.OAuthFlow{}), true},
+		{clientCreds(ogen.OAuthFlow{}), true},
 
 		// `authorizationUrl` must be a valid URL.
-		{nil, implicit(ogen.OAuthFlow{
+		{implicit(ogen.OAuthFlow{
 			AuthorizationURL: "-",
 		}), true},
 		// `tokenUrl` must be a valid URL.
-		{nil, password(ogen.OAuthFlow{
+		{password(ogen.OAuthFlow{
 			TokenURL: "-",
 		}), true},
 
 		// `refreshUrl` must be a valid URL.
-		{nil, implicit(ogen.OAuthFlow{
+		{implicit(ogen.OAuthFlow{
 			AuthorizationURL: "https://example.com/authorization",
 			RefreshURL:       "-",
 		}), true},
 
-		// `unknown_scope` must be defined in `flows`.
-		{[]string{"unknown_scope"}, implicit(ogen.OAuthFlow{
-			AuthorizationURL: "https://example.com/authorization",
-			RefreshURL:       "https://example.com/refresh",
-		}), true},
-		{[]string{"unknown_scope"}, implicit(ogen.OAuthFlow{
+		// It's OK.
+		{implicit(ogen.OAuthFlow{
 			AuthorizationURL: "https://example.com/authorization",
 			RefreshURL:       "https://example.com/refresh",
 			Scopes: map[string]string{
@@ -74,7 +69,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.scopes, tt.flows, "")
+			err := p.validateOAuthFlows(tt.flows, "")
 			if tt.wantErr {
 				a.Error(err)
 				return