Repository: ogen-go/ogen
Pull request #456 (2022-07-05 04:03:40)

feat(parser): wrap errors with line and column


Patch:
diff --git a/cmd/ogen/main.go b/cmd/ogen/main.go
index 009b149b..7c5d0823 100644
--- a/cmd/ogen/main.go
+++ b/cmd/ogen/main.go
@@ -114,6 +114,7 @@ func run() error {
 	if flag.NArg() == 0 || specPath == "" {
 		return errors.New("no spec provided")
 	}
+	specPath = filepath.Clean(specPath)
 
 	switch files, err := os.ReadDir(*targetDir); {
 	case os.IsNotExist(err):
@@ -138,6 +139,7 @@ func run() error {
 		_ = logger.Sync()
 	}()
 
+	_, fileName := filepath.Split(specPath)
 	opts := gen.Options{
 		NoClient:             *noClient,
 		NoServer:             *noServer,
@@ -146,6 +148,7 @@ func run() error {
 		SkipUnimplemented:    *skipUnimplemented,
 		InferSchemaType:      *inferTypes,
 		AllowRemote:          *allowRemote,
+		Filename:             fileName,
 		Filters: gen.Filters{
 			PathRegex: filterPath,
 			Methods:   filterMethods,
diff --git a/gen/errors.go b/gen/errors.go
index 49bdafc6..9a0cc85d 100644
--- a/gen/errors.go
+++ b/gen/errors.go
@@ -14,9 +14,10 @@ type unimplementedError interface {
 
 var (
 	_ = []interface {
-		error
 		errors.Wrapper
 		errors.Formatter
+		fmt.Formatter
+		error
 	}{
 		(*ErrParseSpec)(nil),
 		(*ErrBuildRouter)(nil),
@@ -104,6 +105,11 @@ func (e *ErrParseSpec) FormatError(p errors.Printer) (next error) {
 	return e.err
 }
 
+// Format implements fmt.Formatter.
+func (e *ErrParseSpec) Format(s fmt.State, verb rune) {
+	errors.FormatError(e, s, verb)
+}
+
 // Error implements error.
 func (e *ErrParseSpec) Error() string {
 	return fmt.Sprintf("parse spec: %s", e.err)
@@ -125,6 +131,11 @@ func (e *ErrBuildRouter) FormatError(p errors.Printer) (next error) {
 	return e.err
 }
 
+// Format implements fmt.Formatter.
+func (e *ErrBuildRouter) Format(s fmt.State, verb rune) {
+	errors.FormatError(e, s, verb)
+}
+
 // Error implements error.
 func (e *ErrBuildRouter) Error() string {
 	return fmt.Sprintf("build router: %s", e.err)
@@ -146,6 +157,11 @@ func (e *ErrGoFormat) FormatError(p errors.Printer) (next error) {
 	return e.err
 }
 
+// Format implements fmt.Formatter.
+func (e *ErrGoFormat) Format(s fmt.State, verb rune) {
+	errors.FormatError(e, s, verb)
+}
+
 // Error implements error.
 func (e *ErrGoFormat) Error() string {
 	return fmt.Sprintf("goimports: %s", e.err)
diff --git a/gen/generator.go b/gen/generator.go
index d2841555..c58f40a8 100644
--- a/gen/generator.go
+++ b/gen/generator.go
@@ -49,6 +49,10 @@ type Options struct {
 	AllowRemote bool
 	// Remote is remote reference resolver options.
 	Remote RemoteOptions
+	// Filename is a name of the spec file.
+	//
+	// Used for error messages.
+	Filename string
 	// Filters contains filters to skip operations.
 	Filters Filters
 	// IgnoreNotImplemented contains ErrNotImplemented messages to ignore.
@@ -98,6 +102,7 @@ func NewGenerator(spec *ogen.Spec, opts Options) (*Generator, error) {
 	}
 	api, err := parser.Parse(spec, parser.Settings{
 		External:   external,
+		Filename:   opts.Filename,
 		InferTypes: opts.InferSchemaType,
 	})
 	if err != nil {
diff --git a/json/location.go b/json/location.go
new file mode 100644
index 00000000..ba631d47
--- /dev/null
+++ b/json/location.go
@@ -0,0 +1,91 @@
+package json
+
+import (
+	"bytes"
+	"fmt"
+	"strconv"
+
+	"github.com/go-json-experiment/json"
+)
+
+// LineColumn returns the line and column of the location.
+//
+// If offset is invalid, line and column are 0 and ok is false.
+func LineColumn(offset int64, data []byte) (line, column int64, ok bool) {
+	if offset < 0 || int64(len(data)) <= offset {
+		return 0, 0, false
+	}
+
+	{
+		unread := data[offset:]
+		trimmed := bytes.TrimLeft(unread, "\x20\t\r\n,:")
+		if len(trimmed) != len(unread) {
+			// Skip leading whitespace, because decoder does not do it.
+			offset += int64(len(unread) - len(trimmed))
+		}
+	}
+
+	lines := data[:offset]
+	// Lines count from 1.
+	line = int64(bytes.Count(lines, []byte("\n"))) + 1
+	// Find offset from last newline.
+	lastNL := int64(bytes.LastIndexByte(lines, '\n'))
+	column = offset - lastNL
+	return line, column, true
+}
+
+// Location is a JSON value location.
+type Location struct {
+	JSONPointer  string `json:"-"`
+	Offset       int64  `json:"-"`
+	Line, Column int64  `json:"-"`
+}
+
+// String implements fmt.Stringer.
+func (l Location) String() string {
+	if l.Line == 0 {
+		return strconv.Quote(l.JSONPointer)
+	}
+	return fmt.Sprintf("%d:%d", l.Line, l.Column)
+}
+
+// Locatable is an interface for JSON value location store.
+type Locatable interface {
+	setLocation(Location)
+	Location() (Location, bool)
+}
+
+// Locator stores the Location of a JSON value.
+type Locator struct {
+	location Location
+	set      bool
+}
+
+func (l *Locator) setLocation(loc Location) {
+	l.location = 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
+}
+
+// LocationUnmarshaler is json.Unmarshalers that sets the location.
+func LocationUnmarshaler(data []byte) *json.Unmarshalers {
+	return json.UnmarshalFuncV2(func(opts json.UnmarshalOptions, d *json.Decoder, l Locatable) error {
+		if _, ok := l.(*Locator); ok {
+			return nil
+		}
+
+		offset := d.InputOffset()
+		line, column, _ := LineColumn(offset, data)
+		l.setLocation(Location{
+			JSONPointer: d.StackPointer(),
+			Offset:      offset,
+			Line:        line,
+			Column:      column,
+		})
+		return json.SkipFunc
+	})
+}
diff --git a/jsonschema/raw_schema.go b/jsonschema/raw_schema.go
index 5c3350bf..1ba7ef51 100644
--- a/jsonschema/raw_schema.go
+++ b/jsonschema/raw_schema.go
@@ -36,48 +36,50 @@ func (n *Num) UnmarshalNextJSON(opts json.UnmarshalOptions, d *json.Decoder) err
 	if err != nil {
 		return err
 	}
-
-	*n = Num(val)
+	*n = append((*n)[:0], val...)
 	return nil
 }
 
 // RawSchema is unparsed JSON Schema.
 type RawSchema struct {
-	Ref                  string                   `json:"$ref,omitzero"`
-	Summary              string                   `json:"summary,omitzero"`
-	Description          string                   `json:"description,omitzero"`
-	Type                 string                   `json:"type,omitzero"`
-	Format               string                   `json:"format,omitzero"`
-	Properties           RawProperties            `json:"properties,omitzero"`
-	AdditionalProperties *AdditionalProperties    `json:"additionalProperties,omitzero"`
-	PatternProperties    RawPatternProperties     `json:"patternProperties,omitzero"`
-	Required             []string                 `json:"required,omitzero"`
-	Items                *RawSchema               `json:"items,omitzero"`
-	Nullable             bool                     `json:"nullable,omitzero"`
-	AllOf                []*RawSchema             `json:"allOf,omitzero"`
-	OneOf                []*RawSchema             `json:"oneOf,omitzero"`
-	AnyOf                []*RawSchema             `json:"anyOf,omitzero"`
-	Discriminator        *Discriminator           `json:"discriminator,omitzero"`
-	Enum                 Enum                     `json:"enum,omitzero"`
-	MultipleOf           Num                      `json:"multipleOf,omitzero"`
-	Maximum              Num                      `json:"maximum,omitzero"`
-	ExclusiveMaximum     bool                     `json:"exclusiveMaximum,omitzero"`
-	Minimum              Num                      `json:"minimum,omitzero"`
-	ExclusiveMinimum     bool                     `json:"exclusiveMinimum,omitzero"`
-	MaxLength            *uint64                  `json:"maxLength,omitzero"`
-	MinLength            *uint64                  `json:"minLength,omitzero"`
-	Pattern              string                   `json:"pattern,omitzero"`
-	MaxItems             *uint64                  `json:"maxItems,omitzero"`
-	MinItems             *uint64                  `json:"minItems,omitzero"`
-	UniqueItems          bool                     `json:"uniqueItems,omitzero"`
-	MaxProperties        *uint64                  `json:"maxProperties,omitzero"`
-	MinProperties        *uint64                  `json:"minProperties,omitzero"`
-	Default              json.RawValue            `json:"default,omitzero"`
-	Example              json.RawValue            `json:"example,omitzero"`
-	Deprecated           bool                     `json:"deprecated,omitzero"`
-	ContentEncoding      string                   `json:"contentEncoding,omitzero"`
-	ContentMediaType     string                   `json:"contentMediaType,omitzero"`
-	XAnnotations         map[string]json.RawValue `json:",inline"`
+	Ref                  string                `json:"$ref,omitzero"`
+	Summary              string                `json:"summary,omitzero"`
+	Description          string                `json:"description,omitzero"`
+	Type                 string                `json:"type,omitzero"`
+	Format               string                `json:"format,omitzero"`
+	Properties           RawProperties         `json:"properties,omitzero"`
+	AdditionalProperties *AdditionalProperties `json:"additionalProperties,omitzero"`
+	PatternProperties    RawPatternProperties  `json:"patternProperties,omitzero"`
+	Required             []string              `json:"required,omitzero"`
+	Items                *RawSchema            `json:"items,omitzero"`
+	Nullable             bool                  `json:"nullable,omitzero"`
+	AllOf                []*RawSchema          `json:"allOf,omitzero"`
+	OneOf                []*RawSchema          `json:"oneOf,omitzero"`
+	AnyOf                []*RawSchema          `json:"anyOf,omitzero"`
+	Discriminator        *Discriminator        `json:"discriminator,omitzero"`
+	Enum                 Enum                  `json:"enum,omitzero"`
+	MultipleOf           Num                   `json:"multipleOf,omitzero"`
+	Maximum              Num                   `json:"maximum,omitzero"`
+	ExclusiveMaximum     bool                  `json:"exclusiveMaximum,omitzero"`
+	Minimum              Num                   `json:"minimum,omitzero"`
+	ExclusiveMinimum     bool                  `json:"exclusiveMinimum,omitzero"`
+	MaxLength            *uint64               `json:"maxLength,omitzero"`
+	MinLength            *uint64               `json:"minLength,omitzero"`
+	Pattern              string                `json:"pattern,omitzero"`
+	MaxItems             *uint64               `json:"maxItems,omitzero"`
+	MinItems             *uint64               `json:"minItems,omitzero"`
+	UniqueItems          bool                  `json:"uniqueItems,omitzero"`
+	MaxProperties        *uint64               `json:"maxProperties,omitzero"`
+	MinProperties        *uint64               `json:"minProperties,omitzero"`
+	Default              json.RawValue         `json:"default,omitzero"`
+	Example              json.RawValue         `json:"example,omitzero"`
+	Deprecated           bool                  `json:"deprecated,omitzero"`
+	ContentEncoding      string                `json:"contentEncoding,omitzero"`
+	ContentMediaType     string                `json:"contentMediaType,omitzero"`
+
+	XAnnotations map[string]json.RawValue `json:",inline"`
+
+	ogenjson.Locator `json:"-"`
 }
 
 // Enum is JSON Schema enum validator description.
diff --git a/openapi/parser/errors.go b/openapi/parser/errors.go
new file mode 100644
index 00000000..bb9df54e
--- /dev/null
+++ b/openapi/parser/errors.go
@@ -0,0 +1,71 @@
+package parser
+
+import (
+	"fmt"
+
+	"github.com/go-faster/errors"
+
+	ogenjson "github.com/ogen-go/ogen/json"
+)
+
+var _ interface {
+	error
+	errors.Wrapper
+	errors.Formatter
+} = (*LocationError)(nil)
+
+// LocationError is a wrapper for an error that has a location.
+type LocationError struct {
+	file string
+	loc  ogenjson.Location
+	err  error
+}
+
+// Unwrap implements errors.Wrapper.
+func (e *LocationError) Unwrap() error {
+	return e.err
+}
+
+func (e *LocationError) fileName() string {
+	filename := e.file
+	if filename != "" {
+		switch {
+		case e.loc.Line != 0:
+			// Line is set, so return "${filename}:".
+			filename += ":"
+		case e.loc.JSONPointer != "":
+			// Line is not set, but JSONPointer is set, so return "${filename}#${JSONPointer}".
+			filename += "#"
+		default:
+			// Neither line nor JSONPointer is set, so return empty string.
+			return ""
+		}
+	}
+	return filename
+}
+
+// FormatError implements errors.Formatter.
+func (e *LocationError) FormatError(p errors.Printer) (next error) {
+	p.Printf("at %s%s", e.fileName(), e.loc)
+	return e.err
+}
+
+// Error implements error.
+func (e *LocationError) Error() string {
+	return fmt.Sprintf("at %s%s: %s", e.fileName(), e.loc, e.err)
+}
+
+func (p *parser) wrapLocation(l ogenjson.Locatable, err error) error {
+	if err == nil {
+		return nil
+	}
+	loc, ok := l.Location()
+	if !ok {
+		return err
+	}
+	return &LocationError{
+		file: p.filename,
+		loc:  loc,
+		err:  err,
+	}
+}
diff --git a/openapi/parser/parse_example.go b/openapi/parser/parse_example.go
index c625c2ec..0bea0d3e 100644
--- a/openapi/parser/parse_example.go
+++ b/openapi/parser/parse_example.go
@@ -7,7 +7,7 @@ import (
 	"github.com/ogen-go/ogen/openapi"
 )
 
-func (p *parser) parseExample(e *ogen.Example, ctx *resolveCtx) (*openapi.Example, error) {
+func (p *parser) parseExample(e *ogen.Example, ctx *resolveCtx) (_ *openapi.Example, rerr error) {
 	if e == nil {
 		return nil, nil
 	}
@@ -19,6 +19,9 @@ func (p *parser) parseExample(e *ogen.Example, ctx *resolveCtx) (*openapi.Exampl
 		}
 		return ex, nil
 	}
+	defer func() {
+		rerr = p.wrapLocation(e, rerr)
+	}()
 
 	return &openapi.Example{
 		Summary:       e.Summary,
diff --git a/openapi/parser/parse_header.go b/openapi/parser/parse_header.go
index 19c24d29..39e63cc7 100644
--- a/openapi/parser/parse_header.go
+++ b/openapi/parser/parse_header.go
@@ -23,7 +23,7 @@ func (p *parser) parseHeaders(headers map[string]*ogen.Header, ctx *resolveCtx)
 	return result, nil
 }
 
-func (p *parser) parseHeader(name string, header *ogen.Header, ctx *resolveCtx) (*openapi.Header, error) {
+func (p *parser) parseHeader(name string, header *ogen.Header, ctx *resolveCtx) (_ *openapi.Header, rerr error) {
 	if header == nil {
 		return nil, errors.New("header object is empty or null")
 	}
@@ -34,6 +34,9 @@ func (p *parser) parseHeader(name string, header *ogen.Header, ctx *resolveCtx)
 		}
 		return parsed, nil
 	}
+	defer func() {
+		rerr = p.wrapLocation(header, rerr)
+	}()
 
 	if header.In != "" {
 		return nil, errors.Errorf(`"in" MUST NOT be specified, got %q`, header.In)
diff --git a/openapi/parser/parse_mediatype.go b/openapi/parser/parse_mediatype.go
index 643a3cf3..92ae81ce 100644
--- a/openapi/parser/parse_mediatype.go
+++ b/openapi/parser/parse_mediatype.go
@@ -56,7 +56,11 @@ func (p *parser) parseParameterContent(content map[string]ogen.Media, ctx *resol
 	panic("unreachable")
 }
 
-func (p *parser) parseMediaType(m ogen.Media, ctx *resolveCtx) (*openapi.MediaType, error) {
+func (p *parser) parseMediaType(m ogen.Media, ctx *resolveCtx) (_ *openapi.MediaType, rerr error) {
+	defer func() {
+		rerr = p.wrapLocation(&m, rerr)
+	}()
+
 	s, err := p.parseSchema(m.Schema, ctx)
 	if err != nil {
 		return nil, errors.Wrap(err, "schema")
diff --git a/openapi/parser/parse_parameter.go b/openapi/parser/parse_parameter.go
index 787c7b5a..e66e69d9 100644
--- a/openapi/parser/parse_parameter.go
+++ b/openapi/parser/parse_parameter.go
@@ -73,7 +73,7 @@ func validateParameter(name string, locatedIn openapi.ParameterLocation, param *
 	return nil
 }
 
-func (p *parser) parseParameter(param *ogen.Parameter, ctx *resolveCtx) (*openapi.Parameter, error) {
+func (p *parser) parseParameter(param *ogen.Parameter, ctx *resolveCtx) (_ *openapi.Parameter, rerr error) {
 	if param == nil {
 		return nil, errors.New("parameter object is empty or null")
 	}
@@ -84,6 +84,9 @@ func (p *parser) parseParameter(param *ogen.Parameter, ctx *resolveCtx) (*openap
 		}
 		return parsed, nil
 	}
+	defer func() {
+		rerr = p.wrapLocation(param, rerr)
+	}()
 
 	types := map[string]openapi.ParameterLocation{
 		"query":  openapi.LocationQuery,
diff --git a/openapi/parser/parse_request_body.go b/openapi/parser/parse_request_body.go
index beb2a1d2..dc99afee 100644
--- a/openapi/parser/parse_request_body.go
+++ b/openapi/parser/parse_request_body.go
@@ -7,7 +7,7 @@ import (
 	"github.com/ogen-go/ogen/openapi"
 )
 
-func (p *parser) parseRequestBody(body *ogen.RequestBody, ctx *resolveCtx) (*openapi.RequestBody, error) {
+func (p *parser) parseRequestBody(body *ogen.RequestBody, ctx *resolveCtx) (_ *openapi.RequestBody, rerr error) {
 	if body == nil {
 		return nil, errors.New("requestBody object is empty or null")
 	}
@@ -19,6 +19,10 @@ func (p *parser) parseRequestBody(body *ogen.RequestBody, ctx *resolveCtx) (*ope
 
 		return reqBody, nil
 	}
+	defer func() {
+		rerr = p.wrapLocation(body, rerr)
+	}()
+
 	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")
diff --git a/openapi/parser/parse_response.go b/openapi/parser/parse_response.go
index 7badfd83..04b7a1b7 100644
--- a/openapi/parser/parse_response.go
+++ b/openapi/parser/parse_response.go
@@ -38,7 +38,7 @@ func (p *parser) parseResponses(responses ogen.Responses) (_ map[string]*openapi
 	return result, nil
 }
 
-func (p *parser) parseResponse(resp *ogen.Response, ctx *resolveCtx) (*openapi.Response, error) {
+func (p *parser) parseResponse(resp *ogen.Response, ctx *resolveCtx) (_ *openapi.Response, rerr error) {
 	if resp == nil {
 		return nil, errors.New("response object is empty or null")
 	}
@@ -50,6 +50,9 @@ func (p *parser) parseResponse(resp *ogen.Response, ctx *resolveCtx) (*openapi.R
 
 		return resp, nil
 	}
+	defer func() {
+		rerr = p.wrapLocation(resp, rerr)
+	}()
 
 	content, err := p.parseContent(resp.Content, ctx)
 	if err != nil {
diff --git a/openapi/parser/parse_schema.go b/openapi/parser/parse_schema.go
index 4f987bfe..7d5b2af2 100644
--- a/openapi/parser/parse_schema.go
+++ b/openapi/parser/parse_schema.go
@@ -10,7 +10,12 @@ import (
 	"github.com/ogen-go/ogen/jsonschema"
 )
 
-func (p *parser) parseSchema(schema *ogen.Schema, ctx *resolveCtx) (*jsonschema.Schema, error) {
+func (p *parser) parseSchema(schema *ogen.Schema, ctx *resolveCtx) (_ *jsonschema.Schema, rerr error) {
+	if schema != nil {
+		defer func() {
+			rerr = p.wrapLocation(schema, rerr)
+		}()
+	}
 	s := schema.ToJSONSchema()
 	if loc := ctx.lastLoc(); s != nil && s.Ref != "" && loc != "" {
 		base, err := url.Parse(loc)
diff --git a/openapi/parser/parse_security.go b/openapi/parser/parse_security.go
index 2b82d44a..dc16d371 100644
--- a/openapi/parser/parse_security.go
+++ b/openapi/parser/parse_security.go
@@ -11,31 +11,34 @@ import (
 )
 
 func (p *parser) parseSecurityScheme(
-	s *ogen.SecurityScheme,
+	scheme *ogen.SecurityScheme,
 	scopes []string,
 	ctx *resolveCtx,
-) (*ogen.SecurityScheme, error) {
-	if s == nil {
+) (_ *ogen.SecurityScheme, rerr error) {
+	if scheme == nil {
 		return nil, errors.New("securityScheme is empty or null")
 	}
 
-	if ref := s.Ref; ref != "" {
+	if ref := scheme.Ref; ref != "" {
 		sch, err := p.resolveSecurityScheme(ref, ctx)
 		if err != nil {
 			return nil, errors.Wrap(err, "resolve security schema")
 		}
 		return sch, nil
 	}
+	defer func() {
+		rerr = p.wrapLocation(scheme, rerr)
+	}()
 
 	if err := func() error {
-		switch s.Type {
+		switch scheme.Type {
 		case "apiKey":
-			switch s.In {
+			switch scheme.In {
 			case "query", "header", "cookie":
 			default:
-				return errors.Errorf(`invalid "in": %q`, s.In)
+				return errors.Errorf(`invalid "in": %q`, scheme.In)
 			}
-			if s.Name == "" {
+			if scheme.Name == "" {
 				return errors.New(`"name" is required and MUST be a non-empty string`)
 			}
 			return nil
@@ -45,7 +48,7 @@ func (p *parser) parseSecurityScheme(
 			// 	Probably such validation is too strict.
 
 			// Values from https://www.iana.org/assignments/http-authschemes/http-authschemes.xhtml.
-			switch strings.ToLower(s.Scheme) {
+			switch strings.ToLower(scheme.Scheme) {
 			case "basic",
 				"bearer",
 				"digest",
@@ -57,26 +60,26 @@ func (p *parser) parseSecurityScheme(
 				"scram-sha-256",
 				"vapid":
 			default:
-				return errors.Errorf(`invalid "scheme": %q`, s.Scheme)
+				return errors.Errorf(`invalid "scheme": %q`, scheme.Scheme)
 			}
 			return nil
 		case "mutualTLS":
 			return nil
 		case "oauth2":
-			return validateOAuthFlows(scopes, s.Flows)
+			return validateOAuthFlows(scopes, scheme.Flows)
 		case "openIdConnect":
-			if _, err := url.ParseRequestURI(s.OpenIDConnectURL); err != nil {
+			if _, err := url.ParseRequestURI(scheme.OpenIDConnectURL); err != nil {
 				return errors.Wrap(err, `"openIdConnectUrl" MUST be in the form of a URL`)
 			}
 			return nil
 		default:
-			return errors.Errorf("unknown security scheme type %q", s.Type)
+			return errors.Errorf("unknown security scheme type %q", scheme.Type)
 		}
 	}(); err != nil {
-		return nil, errors.Wrap(err, s.Type)
+		return nil, errors.Wrap(err, scheme.Type)
 	}
 
-	return s, nil
+	return scheme, nil
 }
 
 func validateOAuthFlows(scopes []string, flows *ogen.OAuthFlows) error {
diff --git a/openapi/parser/parser.go b/openapi/parser/parser.go
index c1dfa962..341d760f 100644
--- a/openapi/parser/parser.go
+++ b/openapi/parser/parser.go
@@ -26,6 +26,7 @@ type parser struct {
 	external   jsonschema.ExternalResolver
 	schemas    map[string][]byte
 	depthLimit int
+	filename   string // optional, used for error messages
 
 	schemaParser *jsonschema.Parser
 }
@@ -58,6 +59,7 @@ func Parse(spec *ogen.Spec, s Settings) (*openapi.API, error) {
 			"": spec.Raw,
 		},
 		depthLimit: s.DepthLimit,
+		filename:   s.Filename,
 		schemaParser: jsonschema.NewParser(jsonschema.Settings{
 			External: s.External,
 			Resolver: componentsResolver{
@@ -130,6 +132,10 @@ func (p *parser) parseOps() error {
 }
 
 func (p *parser) parseOp(path, httpMethod string, spec ogen.Operation, itemParams []*openapi.Parameter) (_ *openapi.Operation, err error) {
+	defer func() {
+		err = p.wrapLocation(&spec, err)
+	}()
+
 	op := &openapi.Operation{
 		OperationID: spec.OperationID,
 		Summary:     spec.Summary,
diff --git a/openapi/parser/resolve.go b/openapi/parser/resolve.go
index 34965190..dead26ef 100644
--- a/openapi/parser/resolve.go
+++ b/openapi/parser/resolve.go
@@ -34,7 +34,6 @@ func resolvePointer(root []byte, ptr string, to interface{}) error {
 	if err != nil {
 		return err
 	}
-
 	return json.Unmarshal(data, to)
 }
 
diff --git a/openapi/parser/settings.go b/openapi/parser/settings.go
index 2450b89e..1bb6c528 100644
--- a/openapi/parser/settings.go
+++ b/openapi/parser/settings.go
@@ -7,6 +7,11 @@ 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.
+	//
+	// Used for error messages.
+	Filename string
+
 	// DepthLimit limits the number of nested references. Default is 1000.
 	DepthLimit int
 
diff --git a/parse_error.go b/parse_error.go
index c9b1c1f3..8bfe5955 100644
--- a/parse_error.go
+++ b/parse_error.go
@@ -1,10 +1,10 @@
 package ogen
 
 import (
-	"bytes"
-
 	"github.com/go-faster/errors"
 	"github.com/go-json-experiment/json"
+
+	ogenjson "github.com/ogen-go/ogen/json"
 )
 
 func unmarshal(data []byte, out any) error {
@@ -15,6 +15,7 @@ func unmarshal(data []byte, out any) error {
 				begin = d.InputOffset()
 				return json.SkipFunc
 			}),
+			ogenjson.LocationUnmarshaler(data),
 		),
 	}
 
@@ -25,24 +26,9 @@ func unmarshal(data []byte, out any) error {
 }
 
 func wrapLineOffset(offset int64, data []byte, err error) error {
-	if offset < 0 || int64(len(data)) <= offset {
+	line, column, ok := ogenjson.LineColumn(offset, data)
+	if !ok {
 		return err
 	}
-
-	{
-		unread := data[offset:]
-		trimmed := bytes.TrimLeft(unread, "\x20\t\r\n,:")
-		if len(trimmed) != len(unread) {
-			// Skip leading whitespace, because decoder does not do it.
-			offset += int64(len(unread) - len(trimmed))
-		}
-	}
-
-	lines := data[:offset]
-	// Lines count from 1.
-	line := bytes.Count(lines, []byte("\n")) + 1
-	lastNL := int64(bytes.LastIndexByte(lines, '\n'))
-	column := offset - lastNL
-
 	return errors.Wrapf(err, "line %d:%d", line, column)
 }
diff --git a/schema.go b/schema.go
new file mode 100644
index 00000000..eeea6852
--- /dev/null
+++ b/schema.go
@@ -0,0 +1,409 @@
+package ogen
+
+import (
+	"reflect"
+
+	"github.com/go-faster/errors"
+	"github.com/go-json-experiment/json"
+)
+
+// The Schema Object allows the definition of input and output data types.
+// These types can be objects, but also primitives and arrays.
+type Schema struct {
+	Ref         string `json:"$ref,omitzero"` // ref object
+	Summary     string `json:"summary,omitzero"`
+	Description string `json:"description,omitzero"`
+
+	// Additional external documentation for this schema.
+	ExternalDocs *ExternalDocumentation `json:"externalDocs,omitzero"`
+
+	// Value MUST be a string. Multiple types via an array are not supported.
+	Type string `json:"type,omitzero"`
+
+	// See Data Type Formats for further details (https://swagger.io/specification/#data-type-format).
+	// While relying on JSON Schema's defined formats,
+	// the OAS offers a few additional predefined formats.
+	Format string `json:"format,omitzero"`
+
+	// Property definitions MUST be a Schema Object and not a standard JSON Schema
+	// (inline or referenced).
+	Properties Properties `json:"properties,omitzero"`
+
+	// Value can be boolean or object. Inline or referenced schema MUST be of a Schema Object
+	// and not a standard JSON Schema. Consistent with JSON Schema, additionalProperties defaults to true.
+	AdditionalProperties *AdditionalProperties `json:"additionalProperties,omitzero"`
+
+	// The value of "patternProperties" MUST be an object. Each property
+	// name of this object SHOULD be a valid regular expression, according
+	// to the ECMA-262 regular expression dialect. Each property value of
+	// this object MUST be a valid JSON Schema.
+	PatternProperties PatternProperties `json:"patternProperties,omitzero"`
+
+	// The value of this keyword MUST be an array.
+	// This array MUST have at least one element.
+	// Elements of this array MUST be strings, and MUST be unique.
+	Required []string `json:"required,omitzero"`
+
+	// Value MUST be an object and not an array.
+	// Inline or referenced schema MUST be of a Schema Object and not a standard JSON Schema.
+	// MUST be present if the Type is "array".
+	Items *Schema `json:"items,omitzero"`
+
+	// A true value adds "null" to the allowed type specified by the type keyword,
+	// only if type is explicitly defined within the same Schema Object.
+	// Other Schema Object constraints retain their defined behavior,
+	// and therefore may disallow the use of null as a value.
+	// A false value leaves the specified or default type unmodified.
+	// The default value is false.
+	Nullable bool `json:"nullable,omitzero"`
+
+	// AllOf takes an array of object definitions that are used
+	// for independent validation but together compose a single object.
+	// Still, it does not imply a hierarchy between the models.
+	// For that purpose, you should include the discriminator.
+	AllOf []*Schema `json:"allOf,omitzero"` // TODO: implement.
+
+	// OneOf validates the value against exactly one of the subschemas
+	OneOf []*Schema `json:"oneOf,omitzero"`
+
+	// AnyOf validates the value against any (one or more) of the subschemas
+	AnyOf []*Schema `json:"anyOf,omitzero"`
+
+	// Discriminator for subschemas.
+	Discriminator *Discriminator `json:"discriminator,omitzero"`
+
+	// The value of this keyword MUST be an array.
+	// This array SHOULD have at least one element.
+	// Elements in the array SHOULD be unique.
+	Enum Enum `json:"enum,omitzero"`
+
+	// The value of "multipleOf" MUST be a number, strictly greater than 0.
+	//
+	// A numeric instance is only valid if division by this keyword's value
+	// results in an integer.
+	MultipleOf Num `json:"multipleOf,omitzero"`
+
+	// The value of "maximum" MUST be a number, representing an upper limit
+	// for a numeric instance.
+	//
+	// If the instance is a number, then this keyword validates if
+	// "exclusiveMaximum" is true and instance is less than the provided
+	// value, or else if the instance is less than or exactly equal to the
+	// provided value.
+	Maximum Num `json:"maximum,omitzero"`
+
+	// The value of "exclusiveMaximum" MUST be a boolean, representing
+	// whether the limit in "maximum" is exclusive or not.  An undefined
+	// value is the same as false.
+	//
+	// If "exclusiveMaximum" is true, then a numeric instance SHOULD NOT be
+	// equal to the value specified in "maximum".  If "exclusiveMaximum" is
+	// false (or not specified), then a numeric instance MAY be equal to the
+	// value of "maximum".
+	ExclusiveMaximum bool `json:"exclusiveMaximum,omitzero"`
+
+	// The value of "minimum" MUST be a number, representing a lower limit
+	// for a numeric instance.
+	//
+	// If the instance is a number, then this keyword validates if
+	// "exclusiveMinimum" is true and instance is greater than the provided
+	// value, or else if the instance is greater than or exactly equal to
+	// the provided value.
+	Minimum Num `json:"minimum,omitzero"`
+
+	// The value of "exclusiveMinimum" MUST be a boolean, representing
+	// whether the limit in "minimum" is exclusive or not.  An undefined
+	// value is the same as false.
+	//
+	// If "exclusiveMinimum" is true, then a numeric instance SHOULD NOT be
+	// equal to the value specified in "minimum".  If "exclusiveMinimum" is
+	// false (or not specified), then a numeric instance MAY be equal to the
+	// value of "minimum".
+	ExclusiveMinimum bool `json:"exclusiveMinimum,omitzero"`
+
+	// The value of this keyword MUST be a non-negative integer.
+	//
+	// The value of this keyword MUST be an integer.  This integer MUST be
+	// greater than, or equal to, 0.
+	//
+	// A string instance is valid against this keyword if its length is less
+	// than, or equal to, the value of this keyword.
+	//
+	// The length of a string instance is defined as the number of its
+	// characters as defined by RFC 7159 [RFC7159].
+	MaxLength *uint64 `json:"maxLength,omitzero"`
+
+	// A string instance is valid against this keyword if its length is
+	// greater than, or equal to, the value of this keyword.
+	//
+	// The length of a string instance is defined as the number of its
+	// characters as defined by RFC 7159 [RFC7159].
+	//
+	// The value of this keyword MUST be an integer.  This integer MUST be
+	// greater than, or equal to, 0.
+	//
+	// "minLength", if absent, may be considered as being present with
+	// integer value 0.
+	MinLength *uint64 `json:"minLength,omitzero"`
+
+	// The value of this keyword MUST be a string.  This string SHOULD be a
+	// valid regular expression, according to the ECMA 262 regular
+	// expression dialect.
+	//
+	// A string instance is considered valid if the regular expression
+	// matches the instance successfully. Recall: regular expressions are
+	// not implicitly anchored.
+	Pattern string `json:"pattern,omitzero"`
+
+	// The value of this keyword MUST be an integer.  This integer MUST be
+	// greater than, or equal to, 0.
+	//
+	// An array instance is valid against "maxItems" if its size is less
+	// than, or equal to, the value of this keyword.
+	MaxItems *uint64 `json:"maxItems,omitzero"`
+
+	// The value of this keyword MUST be an integer.  This integer MUST be
+	// greater than, or equal to, 0.
+	//
+	// An array instance is valid against "minItems" if its size is greater
+	// than, or equal to, the value of this keyword.
+	//
+	// If this keyword is not present, it may be considered present with a
+	// value of 0.
+	MinItems *uint64 `json:"minItems,omitzero"`
+
+	// The value of this keyword MUST be a boolean.
+	//
+	// If this keyword has boolean value false, the instance validates
+	// successfully.  If it has boolean value true, the instance validates
+	// successfully if all of its elements are unique.
+	//
+	// If not present, this keyword may be considered present with boolean
+	// value false.
+	UniqueItems bool `json:"uniqueItems,omitzero"`
+
+	// The value of this keyword MUST be an integer.  This integer MUST be
+	// greater than, or equal to, 0.
+	//
+	// An object instance is valid against "maxProperties" if its number of
+	// properties is less than, or equal to, the value of this keyword.
+	MaxProperties *uint64 `json:"maxProperties,omitzero"`
+
+	// The value of this keyword MUST be an integer.  This integer MUST be
+	// greater than, or equal to, 0.
+	//
+	// An object instance is valid against "minProperties" if its number of
+	// properties is greater than, or equal to, the value of this keyword.
+	//
+	// If this keyword is not present, it may be considered present with a
+	// value of 0.
+	MinProperties *uint64 `json:"minProperties,omitzero"`
+
+	// Default value.
+	Default json.RawValue `json:"default,omitzero"`
+
+	// A free-form property to include an example of an instance for this schema.
+	// To represent examples that cannot be naturally represented in JSON or YAML,
+	// a string value can be used to contain the example with escaping where necessary.
+	Example json.RawValue `json:"example,omitzero"`
+
+	// Specifies that a schema is deprecated and SHOULD be transitioned out
+	// of usage.
+	Deprecated bool `json:"deprecated,omitzero"`
+
+	// If the instance value is a string, this property defines that the
+	// string SHOULD be interpreted as binary data and decoded using the
+	// encoding named by this property.  RFC 2045, Section 6.1 lists
+	// the possible values for this property.
+	//
+	// The value of this property MUST be a string.
+	//
+	// The value of this property SHOULD be ignored if the instance
+	// described is not a string.
+	ContentEncoding string `json:"contentEncoding,omitzero"`
+
+	// The value of this property must be a media type, as defined by RFC
+	// 2046. This property defines the media type of instances
+	// which this schema defines.
+	//
+	// The value of this property MUST be a string.
+	//
+	// The value of this property SHOULD be ignored if the instance
+	// described is not a string.
+	ContentMediaType string `json:"contentMediaType,omitzero"`
+
+	Locator `json:"-"`
+}
+
+// Property is item of Properties.
+type Property struct {
+	Name   string
+	Schema *Schema
+}
+
+// Properties is unparsed JSON Schema properties validator description.
+type Properties []Property
+
+// MarshalNextJSON implements json.MarshalerV2.
+func (p Properties) MarshalNextJSON(opts json.MarshalOptions, e *json.Encoder) error {
+	if err := e.WriteToken(json.ObjectStart); err != nil {
+		return err
+	}
+	for _, member := range p {
+		if err := opts.MarshalNext(e, member.Name); err != nil {
+			return err
+		}
+		if err := opts.MarshalNext(e, member.Schema); err != nil {
+			return err
+		}
+	}
+	if err := e.WriteToken(json.ObjectEnd); err != nil {
+		return err
+	}
+	return nil
+}
+
+// UnmarshalNextJSON implements json.UnmarshalerV2.
+func (p *Properties) UnmarshalNextJSON(opts json.UnmarshalOptions, d *json.Decoder) error {
+	offset := d.InputOffset()
+	// Check Kind for invalid, next call will return error.
+	if kind := d.PeekKind(); kind != '{' && kind != 0 {
+		return &json.SemanticError{
+			ByteOffset:  offset,
+			JSONPointer: d.StackPointer(),
+			JSONKind:    kind,
+			GoType:      reflect.TypeOf(p),
+			Err:         errors.Errorf("unexpected type %s", kind.String()),
+		}
+	}
+	// Read the opening brace.
+	if _, err := d.ReadToken(); err != nil {
+		return err
+	}
+
+	// Keep non-nil value, to distinguish from not set object.
+	properties := Properties{}
+	for d.PeekKind() == '"' {
+		var (
+			name   string
+			schema *Schema
+		)
+		if err := opts.UnmarshalNext(d, &name); err != nil {
+			return err
+		}
+		if err := opts.UnmarshalNext(d, &schema); err != nil {
+			return err
+		}
+		properties = append(properties, Property{Name: name, Schema: schema})
+	}
+	// Read the closing brace.
+	if _, err := d.ReadToken(); err != nil {
+		return err
+	}
+
+	*p = properties
+	return nil
+}
+
+// AdditionalProperties is JSON Schema additionalProperties validator description.
+type AdditionalProperties struct {
+	Bool   *bool
+	Schema Schema
+}
+
+// MarshalNextJSON implements json.MarshalerV2.
+func (p AdditionalProperties) MarshalNextJSON(opts json.MarshalOptions, e *json.Encoder) error {
+	if p.Bool != nil {
+		return opts.MarshalNext(e, p.Bool)
+	}
+	return opts.MarshalNext(e, p.Schema)
+}
+
+// UnmarshalNextJSON implements json.UnmarshalerV2.
+func (p *AdditionalProperties) UnmarshalNextJSON(opts json.UnmarshalOptions, d *json.Decoder) error {
+	offset := d.InputOffset()
+	switch kind := d.PeekKind(); kind {
+	case 't', 'f':
+		return opts.UnmarshalNext(d, &p.Bool)
+	case '{':
+		return opts.UnmarshalNext(d, &p.Schema)
+	default:
+		return &json.SemanticError{
+			ByteOffset:  offset,
+			JSONPointer: d.StackPointer(),
+			JSONKind:    kind,
+			GoType:      reflect.TypeOf(p),
+			Err:         errors.Errorf("unexpected type %s", kind.String()),
+		}
+	}
+}
+
+// PatternProperty is item of PatternProperties.
+type PatternProperty struct {
+	Pattern string
+	Schema  *Schema
+}
+
+// PatternProperties is unparsed JSON Schema patternProperties validator description.
+type PatternProperties []PatternProperty
+
+// MarshalNextJSON implements json.MarshalerV2.
+func (p PatternProperties) MarshalNextJSON(opts json.MarshalOptions, e *json.Encoder) error {
+	if err := e.WriteToken(json.ObjectStart); err != nil {
+		return err
+	}
+	for _, member := range p {
+		if err := opts.MarshalNext(e, member.Pattern); err != nil {
+			return err
+		}
+		if err := opts.MarshalNext(e, member.Schema); err != nil {
+			return err
+		}
+	}
+	if err := e.WriteToken(json.ObjectEnd); err != nil {
+		return err
+	}
+	return nil
+}
+
+// UnmarshalNextJSON implements json.UnmarshalerV2.
+func (p *PatternProperties) UnmarshalNextJSON(opts json.UnmarshalOptions, d *json.Decoder) error {
+	offset := d.InputOffset()
+	// Check Kind for invalid, next call will return error.
+	if kind := d.PeekKind(); kind != '{' && kind != 0 {
+		return &json.SemanticError{
+			ByteOffset:  offset,
+			JSONPointer: d.StackPointer(),
+			JSONKind:    kind,
+			GoType:      reflect.TypeOf(p),
+			Err:         errors.Errorf("unexpected type %s", kind.String()),
+		}
+	}
+	// Read the opening brace.
+	if _, err := d.ReadToken(); err != nil {
+		return err
+	}
+
+	// Keep non-nil value, to distinguish from not set object.
+	patternProperties := PatternProperties{}
+	for d.PeekKind() == '"' {
+		var (
+			pattern string
+			schema  *Schema
+		)
+		if err := opts.UnmarshalNext(d, &pattern); err != nil {
+			return err
+		}
+		if err := opts.UnmarshalNext(d, &schema); err != nil {
+			return err
+		}
+		patternProperties = append(patternProperties, PatternProperty{Pattern: pattern, Schema: schema})
+	}
+	// Read the closing brace.
+	if _, err := d.ReadToken(); err != nil {
+		return err
+	}
+
+	*p = patternProperties
+	return nil
+}
diff --git a/spec_test.go b/schema_test.go
similarity index 100%
rename from spec_test.go
rename to schema_test.go
diff --git a/security_spec.go b/security_scheme.go
similarity index 97%
rename from security_spec.go
rename to security_scheme.go
index 63d33cd7..ae92dc87 100644
--- a/security_spec.go
+++ b/security_scheme.go
@@ -22,6 +22,8 @@ type SecurityScheme struct {
 	// OpenId Connect URL to discover OAuth2 configuration values.
 	// This MUST be in the form of a URL. The OpenID Connect standard requires the use of TLS.
 	OpenIDConnectURL string `json:"openIdConnectUrl,omitempty"`
+
+	Locator `json:"-"`
 }
 
 // OAuthFlows allows configuration of the supported OAuth Flows.
@@ -34,6 +36,8 @@ type OAuthFlows struct {
 	ClientCredentials *OAuthFlow `json:"clientCredentials"`
 	// Configuration for the OAuth Authorization Code flow. Previously called accessCode in OpenAPI 2.0.
 	AuthorizationCode *OAuthFlow `json:"authorizationCode"`
+
+	Locator `json:"-"`
 }
 
 // OAuthFlow is configuration details for a supported OAuth Flow.
@@ -50,6 +54,8 @@ type OAuthFlow struct {
 	// The available scopes for the OAuth2 security scheme.
 	// A map between the scope name and a short description for it. The map MAY be empty.
 	Scopes map[string]string `json:"scopes"`
+
+	Locator `json:"-"`
 }
 
 // SecurityRequirements lists the required security schemes to execute this operation.
diff --git a/spec.go b/spec.go
index 07295475..c1ce1731 100644
--- a/spec.go
+++ b/spec.go
@@ -1,11 +1,10 @@
 package ogen
 
 import (
-	"reflect"
-
 	"github.com/go-faster/errors"
 	"github.com/go-json-experiment/json"
 
+	ogenjson "github.com/ogen-go/ogen/json"
 	"github.com/ogen-go/ogen/jsonschema"
 )
 
@@ -14,6 +13,8 @@ type (
 	Num = jsonschema.Num
 	// Enum is JSON Schema enum validator description.
 	Enum = jsonschema.Enum
+	// Locator stores location of JSON value.
+	Locator = ogenjson.Locator
 )
 
 // Spec is the root document object of the OpenAPI document.
@@ -38,7 +39,7 @@ type Spec struct {
 	ExternalDocs *ExternalDocumentation `json:"externalDocs,omitzero"`
 
 	// Raw JSON value. Used by JSON Schema resolver.
-	Raw []byte `json:"-"`
+	Raw json.RawValue `json:"-"`
 }
 
 // UnmarshalNextJSON implements json.UnmarshalerV2.
@@ -54,7 +55,7 @@ func (s *Spec) UnmarshalNextJSON(opts json.UnmarshalOptions, d *json.Decoder) er
 		return errors.Wrap(err, "spec")
 	}
 
-	a.Raw = append(a.Raw, value...)
+	a.Raw = append(a.Raw[:0], value...)
 	*s = Spec(a)
 	return nil
 }
@@ -64,23 +65,7 @@ func (s *Spec) Init() {
 	if s.Components == nil {
 		s.Components = &Components{}
 	}
-
-	c := s.Components
-	if c.Schemas == nil {
-		c.Schemas = make(map[string]*Schema)
-	}
-	if c.Responses == nil {
-		c.Responses = make(map[string]*Response)
-	}
-	if c.Parameters == nil {
-		c.Parameters = make(map[string]*Parameter)
-	}
-	if c.RequestBodies == nil {
-		c.RequestBodies = make(map[string]*RequestBody)
-	}
-	if c.Examples == nil {
-		c.Examples = make(map[string]*Example)
-	}
+	s.Components.Init()
 }
 
 // Example object.
@@ -92,6 +77,8 @@ type Example struct {
 	Description   string        `json:"description,omitzero"`
 	Value         json.RawValue `json:"value,omitzero"`
 	ExternalValue string        `json:"externalValue,omitzero"`
+
+	Locator `json:"-"`
 }
 
 // Tag object.
@@ -165,6 +152,14 @@ type ServerVariable struct {
 	Description string `json:"description,omitzero"`
 }
 
+// ExternalDocumentation describes a reference to external resource for extended documentation.
+type ExternalDocumentation struct {
+	// A description of the target documentation. CommonMark syntax MAY be used for rich text representation.
+	Description string `json:"description,omitzero"`
+	// REQUIRED. The URL for the target documentation. This MUST be in the form of a URL.
+	URL string `json:"url"`
+}
+
 // Components hold a set of reusable objects for different aspects of the OAS.
 // All objects defined within the components object will have no effect on the API
 // unless they are explicitly referenced from properties outside the components object.
@@ -172,14 +167,42 @@ type Components struct {
 	Schemas         map[string]*Schema         `json:"schemas,omitzero"`
 	Responses       map[string]*Response       `json:"responses,omitzero"`
 	Parameters      map[string]*Parameter      `json:"parameters,omitzero"`
-	Headers         map[string]*Header         `json:"headers,omitzero"`
 	Examples        map[string]*Example        `json:"examples,omitzero"`
 	RequestBodies   map[string]*RequestBody    `json:"requestBodies,omitzero"`
+	Headers         map[string]*Header         `json:"headers,omitzero"`
 	SecuritySchemes map[string]*SecurityScheme `json:"securitySchemes,omitzero"`
 	// Links           map[string]Link            `json:"links"`
 	// Callbacks       map[string]Callback        `json:"callback"`
 }
 
+// Init initializes all fields.
+func (c *Components) Init() {
+	if c == nil {
+		return
+	}
+	if c.Schemas == nil {
+		c.Schemas = map[string]*Schema{}
+	}
+	if c.Responses == nil {
+		c.Responses = map[string]*Response{}
+	}
+	if c.Parameters == nil {
+		c.Parameters = map[string]*Parameter{}
+	}
+	if c.Headers == nil {
+		c.Headers = map[string]*Header{}
+	}
+	if c.Examples == nil {
+		c.Examples = map[string]*Example{}
+	}
+	if c.RequestBodies == nil {
+		c.RequestBodies = map[string]*RequestBody{}
+	}
+	if c.SecuritySchemes == nil {
+		c.SecuritySchemes = map[string]*SecurityScheme{}
+	}
+}
+
 // Paths holds the relative paths to the individual endpoints and their operations.
 // The path is appended to the URL from the Server Object in order to construct the full URL.
 // The Paths MAY be empty, due to ACL constraints.
@@ -211,28 +234,21 @@ type PathItem struct {
 
 // Operation describes a single API operation on a path.
 type Operation struct {
+	OperationID string               `json:"operationId,omitzero"`
+	Security    SecurityRequirements `json:"security,omitzero"`
+	Parameters  []*Parameter         `json:"parameters,omitzero"`
+	RequestBody *RequestBody         `json:"requestBody,omitzero"`
+	Responses   Responses            `json:"responses,omitzero"`
+
 	// A list of tags for API documentation control.
 	// Tags can be used for logical grouping of operations by resources or any other qualifier.
-	Tags []string `json:"tags,omitzero"`
-
+	Tags         []string               `json:"tags,omitzero"`
 	Summary      string                 `json:"summary,omitzero"`
 	Description  string                 `json:"description,omitzero"`
 	ExternalDocs *ExternalDocumentation `json:"externalDocs,omitzero"`
+	Deprecated   bool                   `json:"deprecated,omitzero"`
 
-	OperationID string               `json:"operationId,omitzero"`
-	Parameters  []*Parameter         `json:"parameters,omitzero"`
-	RequestBody *RequestBody         `json:"requestBody,omitzero"`
-	Responses   Responses            `json:"responses,omitzero"`
-	Security    SecurityRequirements `json:"security,omitzero"`
-	Deprecated  bool                 `json:"deprecated,omitzero"`
-}
-
-// ExternalDocumentation describes a reference to external resource for extended documentation.
-type ExternalDocumentation struct {
-	// A description of the target documentation. CommonMark syntax MAY be used for rich text representation.
-	Description string `json:"description,omitzero"`
-	// REQUIRED. The URL for the target documentation. This MUST be in the form of a URL.
-	URL string `json:"url"`
+	Locator `json:"-"`
 }
 
 // Parameter describes a single operation parameter.
@@ -278,6 +294,8 @@ type Parameter struct {
 
 	Example  json.RawValue       `json:"example,omitzero"`
 	Examples map[string]*Example `json:"examples,omitzero"`
+
+	Locator `json:"-"`
 }
 
 // RequestBody describes a single request body.
@@ -293,6 +311,8 @@ type RequestBody struct {
 
 	// Determines if the request body is required in the request. Defaults to false.
 	Required bool `json:"required,omitzero"`
+
+	Locator `json:"-"`
 }
 
 // Responses is a container for the expected responses of an operation.
@@ -307,6 +327,8 @@ type Response struct {
 	Headers     map[string]*Header     `json:"headers,omitzero"`
 	Content     map[string]Media       `json:"content,omitzero"`
 	Links       map[string]interface{} `json:"links,omitzero"` // TODO: implement
+
+	Locator `json:"-"`
 }
 
 // Header describes header response.
@@ -330,6 +352,8 @@ type Media struct {
 	// the schema as a property. The encoding object SHALL only apply to requestBody objects when the media
 	// type is multipart or application/x-www-form-urlencoded.
 	Encoding map[string]Encoding `json:"encoding,omitzero"`
+
+	Locator `json:"-"`
 }
 
 // Encoding describes single encoding definition applied to a single schema property.
@@ -357,6 +381,8 @@ type Encoding struct {
 	// The default value is false. This property SHALL be ignored if the request body media type
 	// is not application/x-www-form-urlencoded.
 	AllowReserved bool `json:"allowReserved,omitzero"`
+
+	Locator `json:"-"`
 }
 
 // Discriminator discriminates types for OneOf, AllOf, AnyOf.
@@ -364,402 +390,3 @@ type Discriminator struct {
 	PropertyName string            `json:"propertyName"`
 	Mapping      map[string]string `json:"mapping,omitzero"`
 }
-
-// The Schema Object allows the definition of input and output data types.
-// These types can be objects, but also primitives and arrays.
-type Schema struct {
-	Ref         string `json:"$ref,omitzero"` // ref object
-	Summary     string `json:"summary,omitzero"`
-	Description string `json:"description,omitzero"`
-
-	// Additional external documentation for this schema.
-	ExternalDocs *ExternalDocumentation `json:"externalDocs,omitzero"`
-
-	// Value MUST be a string. Multiple types via an array are not supported.
-	Type string `json:"type,omitzero"`
-
-	// See Data Type Formats for further details (https://swagger.io/specification/#data-type-format).
-	// While relying on JSON Schema's defined formats,
-	// the OAS offers a few additional predefined formats.
-	Format string `json:"format,omitzero"`
-
-	// Property definitions MUST be a Schema Object and not a standard JSON Schema
-	// (inline or referenced).
-	Properties Properties `json:"properties,omitzero"`
-
-	// Value can be boolean or object. Inline or referenced schema MUST be of a Schema Object
-	// and not a standard JSON Schema. Consistent with JSON Schema, additionalProperties defaults to true.
-	AdditionalProperties *AdditionalProperties `json:"additionalProperties,omitzero"`
-
-	// The value of "patternProperties" MUST be an object. Each property
-	// name of this object SHOULD be a valid regular expression, according
-	// to the ECMA-262 regular expression dialect. Each property value of
-	// this object MUST be a valid JSON Schema.
-	PatternProperties PatternProperties `json:"patternProperties,omitzero"`
-
-	// The value of this keyword MUST be an array.
-	// This array MUST have at least one element.
-	// Elements of this array MUST be strings, and MUST be unique.
-	Required []string `json:"required,omitzero"`
-
-	// Value MUST be an object and not an array.
-	// Inline or referenced schema MUST be of a Schema Object and not a standard JSON Schema.
-	// MUST be present if the Type is "array".
-	Items *Schema `json:"items,omitzero"`
-
-	// A true value adds "null" to the allowed type specified by the type keyword,
-	// only if type is explicitly defined within the same Schema Object.
-	// Other Schema Object constraints retain their defined behavior,
-	// and therefore may disallow the use of null as a value.
-	// A false value leaves the specified or default type unmodified.
-	// The default value is false.
-	Nullable bool `json:"nullable,omitzero"`
-
-	// AllOf takes an array of object definitions that are used
-	// for independent validation but together compose a single object.
-	// Still, it does not imply a hierarchy between the models.
-	// For that purpose, you should include the discriminator.
-	AllOf []*Schema `json:"allOf,omitzero"` // TODO: implement.
-
-	// OneOf validates the value against exactly one of the subschemas
-	OneOf []*Schema `json:"oneOf,omitzero"`
-
-	// AnyOf validates the value against any (one or more) of the subschemas
-	AnyOf []*Schema `json:"anyOf,omitzero"`
-
-	// Discriminator for subschemas.
-	Discriminator *Discriminator `json:"discriminator,omitzero"`
-
-	// The value of this keyword MUST be an array.
-	// This array SHOULD have at least one element.
-	// Elements in the array SHOULD be unique.
-	Enum Enum `json:"enum,omitzero"`
-
-	// The value of "multipleOf" MUST be a number, strictly greater than 0.
-	//
-	// A numeric instance is only valid if division by this keyword's value
-	// results in an integer.
-	MultipleOf Num `json:"multipleOf,omitzero"`
-
-	// The value of "maximum" MUST be a number, representing an upper limit
-	// for a numeric instance.
-	//
-	// If the instance is a number, then this keyword validates if
-	// "exclusiveMaximum" is true and instance is less than the provided
-	// value, or else if the instance is less than or exactly equal to the
-	// provided value.
-	Maximum Num `json:"maximum,omitzero"`
-
-	// The value of "exclusiveMaximum" MUST be a boolean, representing
-	// whether the limit in "maximum" is exclusive or not.  An undefined
-	// value is the same as false.
-	//
-	// If "exclusiveMaximum" is true, then a numeric instance SHOULD NOT be
-	// equal to the value specified in "maximum".  If "exclusiveMaximum" is
-	// false (or not specified), then a numeric instance MAY be equal to the
-	// value of "maximum".
-	ExclusiveMaximum bool `json:"exclusiveMaximum,omitzero"`
-
-	// The value of "minimum" MUST be a number, representing a lower limit
-	// for a numeric instance.
-	//
-	// If the instance is a number, then this keyword validates if
-	// "exclusiveMinimum" is true and instance is greater than the provided
-	// value, or else if the instance is greater than or exactly equal to
-	// the provided value.
-	Minimum Num `json:"minimum,omitzero"`
-
-	// The value of "exclusiveMinimum" MUST be a boolean, representing
-	// whether the limit in "minimum" is exclusive or not.  An undefined
-	// value is the same as false.
-	//
-	// If "exclusiveMinimum" is true, then a numeric instance SHOULD NOT be
-	// equal to the value specified in "minimum".  If "exclusiveMinimum" is
-	// false (or not specified), then a numeric instance MAY be equal to the
-	// value of "minimum".
-	ExclusiveMinimum bool `json:"exclusiveMinimum,omitzero"`
-
-	// The value of this keyword MUST be a non-negative integer.
-	//
-	// The value of this keyword MUST be an integer.  This integer MUST be
-	// greater than, or equal to, 0.
-	//
-	// A string instance is valid against this keyword if its length is less
-	// than, or equal to, the value of this keyword.
-	//
-	// The length of a string instance is defined as the number of its
-	// characters as defined by RFC 7159 [RFC7159].
-	MaxLength *uint64 `json:"maxLength,omitzero"`
-
-	// A string instance is valid against this keyword if its length is
-	// greater than, or equal to, the value of this keyword.
-	//
-	// The length of a string instance is defined as the number of its
-	// characters as defined by RFC 7159 [RFC7159].
-	//
-	// The value of this keyword MUST be an integer.  This integer MUST be
-	// greater than, or equal to, 0.
-	//
-	// "minLength", if absent, may be considered as being present with
-	// integer value 0.
-	MinLength *uint64 `json:"minLength,omitzero"`
-
-	// The value of this keyword MUST be a string.  This string SHOULD be a
-	// valid regular expression, according to the ECMA 262 regular
-	// expression dialect.
-	//
-	// A string instance is considered valid if the regular expression
-	// matches the instance successfully. Recall: regular expressions are
-	// not implicitly anchored.
-	Pattern string `json:"pattern,omitzero"`
-
-	// The value of this keyword MUST be an integer.  This integer MUST be
-	// greater than, or equal to, 0.
-	//
-	// An array instance is valid against "maxItems" if its size is less
-	// than, or equal to, the value of this keyword.
-	MaxItems *uint64 `json:"maxItems,omitzero"`
-
-	// The value of this keyword MUST be an integer.  This integer MUST be
-	// greater than, or equal to, 0.
-	//
-	// An array instance is valid against "minItems" if its size is greater
-	// than, or equal to, the value of this keyword.
-	//
-	// If this keyword is not present, it may be considered present with a
-	// value of 0.
-	MinItems *uint64 `json:"minItems,omitzero"`
-
-	// The value of this keyword MUST be a boolean.
-	//
-	// If this keyword has boolean value false, the instance validates
-	// successfully.  If it has boolean value true, the instance validates
-	// successfully if all of its elements are unique.
-	//
-	// If not present, this keyword may be considered present with boolean
-	// value false.
-	UniqueItems bool `json:"uniqueItems,omitzero"`
-
-	// The value of this keyword MUST be an integer.  This integer MUST be
-	// greater than, or equal to, 0.
-	//
-	// An object instance is valid against "maxProperties" if its number of
-	// properties is less than, or equal to, the value of this keyword.
-	MaxProperties *uint64 `json:"maxProperties,omitzero"`
-
-	// The value of this keyword MUST be an integer.  This integer MUST be
-	// greater than, or equal to, 0.
-	//
-	// An object instance is valid against "minProperties" if its number of
-	// properties is greater than, or equal to, the value of this keyword.
-	//
-	// If this keyword is not present, it may be considered present with a
-	// value of 0.
-	MinProperties *uint64 `json:"minProperties,omitzero"`
-
-	// Default value.
-	Default json.RawValue `json:"default,omitzero"`
-
-	// A free-form property to include an example of an instance for this schema.
-	// To represent examples that cannot be naturally represented in JSON or YAML,
-	// a string value can be used to contain the example with escaping where necessary.
-	Example json.RawValue `json:"example,omitzero"`
-
-	// Specifies that a schema is deprecated and SHOULD be transitioned out
-	// of usage.
-	Deprecated bool `json:"deprecated,omitzero"`
-
-	// If the instance value is a string, this property defines that the
-	// string SHOULD be interpreted as binary data and decoded using the
-	// encoding named by this property.  RFC 2045, Section 6.1 lists
-	// the possible values for this property.
-	//
-	// The value of this property MUST be a string.
-	//
-	// The value of this property SHOULD be ignored if the instance
-	// described is not a string.
-	ContentEncoding string `json:"contentEncoding,omitzero"`
-
-	// The value of this property must be a media type, as defined by RFC
-	// 2046. This property defines the media type of instances
-	// which this schema defines.
-	//
-	// The value of this property MUST be a string.
-	//
-	// The value of this property SHOULD be ignored if the instance
-	// described is not a string.
-	ContentMediaType string `json:"contentMediaType,omitzero"`
-}
-
-// Property is item of Properties.
-type Property struct {
-	Name   string
-	Schema *Schema
-}
-
-// Properties is unparsed JSON Schema properties validator description.
-type Properties []Property
-
-// MarshalNextJSON implements json.MarshalerV2.
-func (p Properties) MarshalNextJSON(opts json.MarshalOptions, e *json.Encoder) error {
-	if err := e.WriteToken(json.ObjectStart); err != nil {
-		return err
-	}
-	for _, member := range p {
-		if err := opts.MarshalNext(e, member.Name); err != nil {
-			return err
-		}
-		if err := opts.MarshalNext(e, member.Schema); err != nil {
-			return err
-		}
-	}
-	if err := e.WriteToken(json.ObjectEnd); err != nil {
-		return err
-	}
-	return nil
-}
-
-// UnmarshalNextJSON implements json.UnmarshalerV2.
-func (p *Properties) UnmarshalNextJSON(opts json.UnmarshalOptions, d *json.Decoder) error {
-	offset := d.InputOffset()
-	// Check Kind for invalid, next call will return error.
-	if kind := d.PeekKind(); kind != '{' && kind != 0 {
-		return &json.SemanticError{
-			ByteOffset:  offset,
-			JSONPointer: d.StackPointer(),
-			JSONKind:    kind,
-			GoType:      reflect.TypeOf(p),
-			Err:         errors.Errorf("unexpected type %s", kind.String()),
-		}
-	}
-	// Read the opening brace.
-	if _, err := d.ReadToken(); err != nil {
-		return err
-	}
-
-	// Keep non-nil value, to distinguish from not set object.
-	properties := Properties{}
-	for d.PeekKind() == '"' {
-		var (
-			name   string
-			schema *Schema
-		)
-		if err := opts.UnmarshalNext(d, &name); err != nil {
-			return err
-		}
-		if err := opts.UnmarshalNext(d, &schema); err != nil {
-			return err
-		}
-		properties = append(properties, Property{Name: name, Schema: schema})
-	}
-	// Read the closing brace.
-	if _, err := d.ReadToken(); err != nil {
-		return err
-	}
-
-	*p = properties
-	return nil
-}
-
-// AdditionalProperties is JSON Schema additionalProperties validator description.
-type AdditionalProperties struct {
-	Bool   *bool
-	Schema Schema
-}
-
-// MarshalNextJSON implements json.MarshalerV2.
-func (p AdditionalProperties) MarshalNextJSON(opts json.MarshalOptions, e *json.Encoder) error {
-	if p.Bool != nil {
-		return opts.MarshalNext(e, p.Bool)
-	}
-	return opts.MarshalNext(e, p.Schema)
-}
-
-// UnmarshalNextJSON implements json.UnmarshalerV2.
-func (p *AdditionalProperties) UnmarshalNextJSON(opts json.UnmarshalOptions, d *json.Decoder) error {
-	offset := d.InputOffset()
-	switch kind := d.PeekKind(); kind {
-	case 't', 'f':
-		return opts.UnmarshalNext(d, &p.Bool)
-	case '{':
-		return opts.UnmarshalNext(d, &p.Schema)
-	default:
-		return &json.SemanticError{
-			ByteOffset:  offset,
-			JSONPointer: d.StackPointer(),
-			JSONKind:    kind,
-			GoType:      reflect.TypeOf(p),
-			Err:         errors.Errorf("unexpected type %s", kind.String()),
-		}
-	}
-}
-
-// PatternProperty is item of PatternProperties.
-type PatternProperty struct {
-	Pattern string
-	Schema  *Schema
-}
-
-// PatternProperties is unparsed JSON Schema patternProperties validator description.
-type PatternProperties []PatternProperty
-
-// MarshalNextJSON implements json.MarshalerV2.
-func (r PatternProperties) MarshalNextJSON(opts json.MarshalOptions, e *json.Encoder) error {
-	if err := e.WriteToken(json.ObjectStart); err != nil {
-		return err
-	}
-	for _, member := range r {
-		if err := opts.MarshalNext(e, member.Pattern); err != nil {
-			return err
-		}
-		if err := opts.MarshalNext(e, member.Schema); err != nil {
-			return err
-		}
-	}
-	if err := e.WriteToken(json.ObjectEnd); err != nil {
-		return err
-	}
-	return nil
-}
-
-// UnmarshalNextJSON implements json.UnmarshalerV2.
-func (r *PatternProperties) UnmarshalNextJSON(opts json.UnmarshalOptions, d *json.Decoder) error {
-	offset := d.InputOffset()
-	// Check Kind for invalid, next call will return error.
-	if kind := d.PeekKind(); kind != '{' && kind != 0 {
-		return &json.SemanticError{
-			ByteOffset:  offset,
-			JSONPointer: d.StackPointer(),
-			JSONKind:    kind,
-			GoType:      reflect.TypeOf(r),
-			Err:         errors.Errorf("unexpected type %s", kind.String()),
-		}
-	}
-	// Read the opening brace.
-	if _, err := d.ReadToken(); err != nil {
-		return err
-	}
-
-	// Keep non-nil value, to distinguish from not set object.
-	patternProperties := PatternProperties{}
-	for d.PeekKind() == '"' {
-		var (
-			pattern string
-			schema  *Schema
-		)
-		if err := opts.UnmarshalNext(d, &pattern); err != nil {
-			return err
-		}
-		if err := opts.UnmarshalNext(d, &schema); err != nil {
-			return err
-		}
-		patternProperties = append(patternProperties, PatternProperty{Pattern: pattern, Schema: schema})
-	}
-	// Read the closing brace.
-	if _, err := d.ReadToken(); err != nil {
-		return err
-	}
-
-	*r = patternProperties
-	return nil
-}
diff --git a/spec_backcomp.go b/spec_backcomp.go
index bbae0f5c..52e2dc2a 100644
--- a/spec_backcomp.go
+++ b/spec_backcomp.go
@@ -56,6 +56,7 @@ func (s *Schema) ToJSONSchema() *jsonschema.RawSchema {
 		ContentEncoding:      s.ContentEncoding,
 		ContentMediaType:     s.ContentMediaType,
 		XAnnotations:         map[string]json.RawValue{},
+		Locator:              s.Locator,
 	}
 }
 
@@ -94,8 +95,8 @@ func (p *AdditionalProperties) ToJSONSchema() *jsonschema.AdditionalProperties {
 }
 
 // ToJSONSchema converts PatternProperties to jsonschema.RawPatternProperties.
-func (r PatternProperties) ToJSONSchema() (result jsonschema.RawPatternProperties) {
-	for _, val := range r {
+func (p PatternProperties) ToJSONSchema() (result jsonschema.RawPatternProperties) {
+	for _, val := range p {
 		result = append(result, jsonschema.RawPatternProperty{
 			Pattern: val.Pattern,
 			Schema:  val.Schema.ToJSONSchema(),


Test patch:
diff --git a/gen_test.go b/gen_test.go
index f56a0143..326b5065 100644
--- a/gen_test.go
+++ b/gen_test.go
@@ -119,12 +119,15 @@ func TestGenerate(t *testing.T) {
 func TestNegative(t *testing.T) {
 	walkTestdata(t, "_testdata/negative", func(t *testing.T, file string, data []byte) {
 		a := require.New(t)
+		_, name := path.Split(file)
 
 		spec, err := ogen.Parse(data)
 		a.NoError(err)
 
-		_, err = gen.NewGenerator(spec, gen.Options{})
+		_, err = gen.NewGenerator(spec, gen.Options{
+			Filename: name,
+		})
 		a.Error(err)
-		t.Log(err.Error())
+		t.Logf("%+v", err)
 	})
 }