Repository: ogen-go/ogen
Pull request #567 (2022-09-13 12:24:48)

feat: parse extensions


Patch:
diff --git a/jsonschema/extensions.go b/jsonschema/extensions.go
new file mode 100644
index 00000000..fa6c7206
--- /dev/null
+++ b/jsonschema/extensions.go
@@ -0,0 +1,154 @@
+package jsonschema
+
+import (
+	"encoding/json"
+	"reflect"
+	"strings"
+
+	"github.com/go-faster/errors"
+	"github.com/go-faster/jx"
+	yaml "github.com/go-faster/yamlx"
+
+	"github.com/ogen-go/ogen/internal/location"
+)
+
+// Extensions map is "^x-" fields list.
+type Extensions map[string]yaml.Node
+
+func isExtensionKey(key string) bool {
+	return strings.HasPrefix(key, "x-")
+}
+
+// MarshalYAML implements yaml.Marshaler.
+func (p Extensions) MarshalYAML() (any, error) {
+	content := make([]*yaml.Node, 0, len(p)*2)
+	for key, val := range p {
+		val := val
+		if !isExtensionKey(key) {
+			continue
+		}
+		content = append(content,
+			&yaml.Node{Kind: yaml.ScalarNode, Tag: "!!str", Value: key},
+			&val,
+		)
+	}
+
+	return &yaml.Node{
+		Kind:    yaml.MappingNode,
+		Content: content,
+	}, nil
+}
+
+// UnmarshalYAML implements yaml.Unmarshaler.
+func (p *Extensions) UnmarshalYAML(node *yaml.Node) error {
+	if node.Kind != yaml.MappingNode {
+		return &yaml.UnmarshalError{
+			Node: node,
+			Type: reflect.TypeOf(p),
+			Err:  errors.Errorf("cannot unmarshal %s into %T", node.ShortTag(), p),
+		}
+	}
+	m := *p
+	if m == nil {
+		m = make(Extensions, len(node.Content)/2)
+		*p = m
+	}
+	for i := 0; i < len(node.Content); i += 2 {
+		var (
+			keyNode = node.Content[i]
+			value   = node.Content[i+1]
+			key     string
+		)
+		if err := keyNode.Decode(&key); err != nil {
+			return err
+		}
+		// FIXME(tdakkota): use *yamlx.Node instead of yaml.Node
+		if isExtensionKey(key) && value != nil {
+			m[key] = *value
+		}
+	}
+	return nil
+}
+
+// MarshalJSON implements json.Marshaler.
+func (p Extensions) MarshalJSON() ([]byte, error) {
+	e := &jx.Encoder{}
+
+	e.ObjStart()
+	for key, val := range p {
+		val := val
+		if !isExtensionKey(key) {
+			continue
+		}
+
+		e.FieldStart(key)
+		b, err := convertYAMLtoRawJSON(&val)
+		if err != nil {
+			return nil, errors.Wrap(err, "marshal")
+		}
+		e.Raw(b)
+	}
+	e.ObjEnd()
+	return e.Bytes(), nil
+}
+
+// UnmarshalJSON implements json.Unmarshaler.
+func (p *Extensions) UnmarshalJSON(data []byte) error {
+	m := *p
+	if m == nil {
+		m = make(Extensions)
+		*p = m
+	}
+
+	d := jx.DecodeBytes(data)
+	return d.Obj(func(d *jx.Decoder, key string) error {
+		if !isExtensionKey(key) {
+			return d.Skip()
+		}
+
+		b, err := d.Raw()
+		if err != nil {
+			return err
+		}
+
+		value, err := convertJSONToRawYAML(json.RawMessage(b))
+		if err != nil {
+			return err
+		}
+		m[key] = *value
+
+		return nil
+	})
+}
+
+// OpenAPICommon is common fields for OpenAPI objects.
+type OpenAPICommon struct {
+	Extensions
+	location.Locator
+}
+
+// MarshalYAML implements yaml.Marshaler.
+func (p OpenAPICommon) MarshalYAML() (any, error) {
+	return p.Extensions.MarshalYAML()
+}
+
+// UnmarshalYAML implements yaml.Unmarshaler.
+func (p *OpenAPICommon) UnmarshalYAML(node *yaml.Node) error {
+	if err := p.Extensions.UnmarshalYAML(node); err != nil {
+		return errors.Wrap(err, "unmarshal extensions")
+	}
+	if err := p.Locator.UnmarshalYAML(node); err != nil {
+		return errors.Wrap(err, "unmarshal locator")
+	}
+	return nil
+}
+
+// MarshalJSON implements json.Marshaler.
+func (p OpenAPICommon) MarshalJSON() ([]byte, error) {
+	return p.Extensions.MarshalJSON()
+}
+
+// UnmarshalJSON implements json.Unmarshaler.
+func (p *OpenAPICommon) UnmarshalJSON(data []byte) error {
+	return p.Extensions.UnmarshalJSON(data)
+}
diff --git a/jsonschema/parser.go b/jsonschema/parser.go
index 04e99f95..1e8b3e4e 100644
--- a/jsonschema/parser.go
+++ b/jsonschema/parser.go
@@ -7,7 +7,6 @@ import (
 	"regexp"
 
 	"github.com/go-faster/errors"
-	"github.com/go-faster/jx"
 
 	"github.com/ogen-go/ogen/internal/jsonpointer"
 	"github.com/ogen-go/ogen/internal/location"
@@ -57,7 +56,7 @@ func (p *Parser) Resolve(ref string, ctx *jsonpointer.ResolveCtx) (*Schema, erro
 func (p *Parser) parse(schema *RawSchema, ctx *jsonpointer.ResolveCtx) (_ *Schema, rerr error) {
 	if schema != nil {
 		defer func() {
-			rerr = p.wrapLocation(ctx.LastLoc(), schema.Locator, rerr)
+			rerr = p.wrapLocation(ctx.LastLoc(), schema.Common.Locator, rerr)
 		}()
 	}
 	return p.parse1(schema, ctx, func(s *Schema) *Schema {
@@ -73,7 +72,7 @@ func (p *Parser) parse1(schema *RawSchema, ctx *jsonpointer.ResolveCtx, hook fun
 
 	if schema != nil && s != nil {
 		if enum := schema.Enum; len(enum) > 0 {
-			loc := schema.Locator.Field("enum")
+			loc := schema.Common.Locator.Field("enum")
 			for i := range enum {
 				for j := range enum {
 					if i == j {
@@ -112,15 +111,13 @@ func (p *Parser) parse1(schema *RawSchema, ctx *jsonpointer.ResolveCtx, hook fun
 				return nil
 			}(); err != nil {
 				err := errors.Wrap(err, "parse default")
-				return nil, p.wrapField("default", ctx.LastLoc(), schema.Locator, err)
+				return nil, p.wrapField("default", ctx.LastLoc(), schema.Common.Locator, err)
 			}
 		}
-		if a, ok := schema.XAnnotations["x-ogen-name"]; ok {
-			name, err := jx.DecodeBytes(a).Str()
-			if err != nil {
-				return nil, errors.Wrapf(err, "decode %q", a)
+		if a, ok := schema.Common.Extensions["x-ogen-name"]; ok {
+			if err := a.Decode(&s.XOgenName); err != nil {
+				return nil, errors.Wrap(err, "parse x-ogen-name")
 			}
-			s.XOgenName = name
 		}
 	}
 
@@ -132,7 +129,7 @@ func (p *Parser) parseSchema(schema *RawSchema, ctx *jsonpointer.ResolveCtx, hoo
 		return nil, nil
 	}
 	wrapField := func(field string, err error) error {
-		return p.wrapField(field, ctx.LastLoc(), schema.Locator, err)
+		return p.wrapField(field, ctx.LastLoc(), schema.Common.Locator, err)
 	}
 
 	validateMinMax := func(prop string, min, max *uint64) (rerr error) {
@@ -180,7 +177,7 @@ func (p *Parser) parseSchema(schema *RawSchema, ctx *jsonpointer.ResolveCtx, hoo
 	case len(schema.OneOf) > 0:
 		s := hook(&Schema{})
 
-		schemas, err := p.parseMany(schema.OneOf, schema.Locator, ctx)
+		schemas, err := p.parseMany(schema.OneOf, schema.Common.Locator, ctx)
 		if err != nil {
 			return nil, wrapField("oneOf", err)
 		}
@@ -208,7 +205,7 @@ func (p *Parser) parseSchema(schema *RawSchema, ctx *jsonpointer.ResolveCtx, hoo
 			Pattern:   schema.Pattern,
 		})
 
-		schemas, err := p.parseMany(schema.AnyOf, schema.Locator, ctx)
+		schemas, err := p.parseMany(schema.AnyOf, schema.Common.Locator, ctx)
 		if err != nil {
 			return nil, wrapField("anyOf", err)
 		}
@@ -218,7 +215,7 @@ func (p *Parser) parseSchema(schema *RawSchema, ctx *jsonpointer.ResolveCtx, hoo
 	case len(schema.AllOf) > 0:
 		s := hook(&Schema{})
 
-		schemas, err := p.parseMany(schema.AllOf, schema.Locator, ctx)
+		schemas, err := p.parseMany(schema.AllOf, schema.Common.Locator, ctx)
 		if err != nil {
 			return nil, wrapField("allOf", err)
 		}
@@ -302,7 +299,7 @@ func (p *Parser) parseSchema(schema *RawSchema, ctx *jsonpointer.ResolveCtx, hoo
 		}
 
 		if pp := schema.PatternProperties; len(pp) > 0 {
-			ppLoc := schema.Locator.Field("patternProperties")
+			ppLoc := schema.Common.Locator.Field("patternProperties")
 
 			patterns := make([]PatternProperty, len(pp))
 			for idx, prop := range pp {
@@ -326,7 +323,7 @@ func (p *Parser) parseSchema(schema *RawSchema, ctx *jsonpointer.ResolveCtx, hoo
 			s.PatternProperties = patterns
 		}
 
-		propsLoc := schema.Locator.Field("properties")
+		propsLoc := schema.Common.Locator.Field("properties")
 		for _, propSpec := range schema.Properties {
 			prop, err := p.parse(propSpec.Schema, ctx)
 			if err != nil {
@@ -489,6 +486,6 @@ func (p *Parser) extendInfo(schema *RawSchema, s *Schema) *Schema {
 		}
 	}
 
-	s.Locator = schema.Locator
+	s.Locator = schema.Common.Locator
 	return s
 }
diff --git a/jsonschema/raw_schema.go b/jsonschema/raw_schema.go
index 90d040fb..564f92c4 100644
--- a/jsonschema/raw_schema.go
+++ b/jsonschema/raw_schema.go
@@ -1,11 +1,5 @@
 package jsonschema
 
-import (
-	"encoding/json"
-
-	"github.com/ogen-go/ogen/internal/location"
-)
-
 // RawSchema is unparsed JSON Schema.
 type RawSchema struct {
 	Ref                  string                `json:"$ref,omitempty" yaml:"$ref,omitempty"`
@@ -45,8 +39,7 @@ type RawSchema struct {
 	XML           *XML           `json:"xml,omitempty" yaml:"xml,omitempty"`
 	Example       Example        `json:"example,omitempty" yaml:"example,omitempty"`
 
-	XAnnotations map[string]json.RawMessage `json:"-" yaml:"-"`
-	Locator      location.Locator           `json:"-" yaml:",inline"`
+	Common OpenAPICommon `json:"-" yaml:",inline"`
 }
 
 // Discriminator discriminates types for OneOf, AllOf, AnyOf.
@@ -57,6 +50,8 @@ type Discriminator struct {
 	PropertyName string `json:"propertyName" yaml:"propertyName"`
 	// An object to hold mappings between payload values and schema names or references.
 	Mapping map[string]string `json:"mapping,omitempty" yaml:"mapping,omitempty"`
+
+	Common OpenAPICommon `json:"-" yaml:",inline"`
 }
 
 // XML is a metadata object that allows for more fine-tuned XML model definitions.
diff --git a/openapi/parser/parse_components.go b/openapi/parser/parse_components.go
index a9728b8a..c998fcb7 100644
--- a/openapi/parser/parse_components.go
+++ b/openapi/parser/parse_components.go
@@ -40,34 +40,35 @@ func validateComponentsKeys(p *parser, c *ogen.Components) error {
 	if c == nil {
 		return nil
 	}
-	if err := validateComponentsKey(p, c.Schemas, c.Locator.Field("schemas")); err != nil {
+	locator := c.Common.Locator
+	if err := validateComponentsKey(p, c.Schemas, locator.Field("schemas")); err != nil {
 		return errors.Wrap(err, "schemas")
 	}
-	if err := validateComponentsKey(p, c.Responses, c.Locator.Field("responses")); err != nil {
+	if err := validateComponentsKey(p, c.Responses, locator.Field("responses")); err != nil {
 		return errors.Wrap(err, "responses")
 	}
-	if err := validateComponentsKey(p, c.Parameters, c.Locator.Field("parameters")); err != nil {
+	if err := validateComponentsKey(p, c.Parameters, locator.Field("parameters")); err != nil {
 		return errors.Wrap(err, "parameters")
 	}
-	if err := validateComponentsKey(p, c.Examples, c.Locator.Field("examples")); err != nil {
+	if err := validateComponentsKey(p, c.Examples, locator.Field("examples")); err != nil {
 		return errors.Wrap(err, "examples")
 	}
-	if err := validateComponentsKey(p, c.RequestBodies, c.Locator.Field("requestBodies")); err != nil {
+	if err := validateComponentsKey(p, c.RequestBodies, locator.Field("requestBodies")); err != nil {
 		return errors.Wrap(err, "requestBodies")
 	}
-	if err := validateComponentsKey(p, c.Headers, c.Locator.Field("headers")); err != nil {
+	if err := validateComponentsKey(p, c.Headers, locator.Field("headers")); err != nil {
 		return errors.Wrap(err, "headers")
 	}
-	if err := validateComponentsKey(p, c.SecuritySchemes, c.Locator.Field("securitySchemes")); err != nil {
+	if err := validateComponentsKey(p, c.SecuritySchemes, locator.Field("securitySchemes")); err != nil {
 		return errors.Wrap(err, "securitySchemes")
 	}
-	if err := validateComponentsKey(p, c.Links, c.Locator.Field("links")); err != nil {
+	if err := validateComponentsKey(p, c.Links, locator.Field("links")); err != nil {
 		return errors.Wrap(err, "links")
 	}
-	if err := validateComponentsKey(p, c.Callbacks, c.Locator.Field("callbacks")); err != nil {
+	if err := validateComponentsKey(p, c.Callbacks, locator.Field("callbacks")); err != nil {
 		return errors.Wrap(err, "callbacks")
 	}
-	if err := validateComponentsKey(p, c.PathItems, c.Locator.Field("pathItems")); err != nil {
+	if err := validateComponentsKey(p, c.PathItems, locator.Field("pathItems")); err != nil {
 		return errors.Wrap(err, "pathItems")
 	}
 	return nil
@@ -83,8 +84,9 @@ func (p *parser) parseComponents(c *ogen.Components) (_ *openapi.Components, rer
 			RequestBodies: map[string]*openapi.RequestBody{},
 		}, nil
 	}
+	locator := c.Common.Locator
 	defer func() {
-		rerr = p.wrapLocation("", c.Locator, rerr)
+		rerr = p.wrapLocation("", locator, rerr)
 	}()
 
 	if err := validateComponentsKeys(p, c); err != nil {
@@ -99,9 +101,9 @@ func (p *parser) parseComponents(c *ogen.Components) (_ *openapi.Components, rer
 		RequestBodies: make(map[string]*openapi.RequestBody, len(c.RequestBodies)),
 	}
 	wrapErr := func(component, name string, err error) error {
-		loc := c.Locator.Field(component).Field(name)
+		locator := locator.Field(component).Field(name)
 		err = errors.Wrapf(err, "%s: %q", component, name)
-		return p.wrapLocation("", loc, err)
+		return p.wrapLocation("", locator, err)
 	}
 
 	for name := range c.Schemas {
diff --git a/openapi/parser/parse_example.go b/openapi/parser/parse_example.go
index e7b95de8..0c6c6bc6 100644
--- a/openapi/parser/parse_example.go
+++ b/openapi/parser/parse_example.go
@@ -10,13 +10,14 @@ func (p *parser) parseExample(e *ogen.Example, ctx *jsonpointer.ResolveCtx) (_ *
 	if e == nil {
 		return nil, nil
 	}
+	locator := e.Common.Locator
 	defer func() {
-		rerr = p.wrapLocation(ctx.LastLoc(), e.Locator, rerr)
+		rerr = p.wrapLocation(ctx.LastLoc(), locator, rerr)
 	}()
 	if ref := e.Ref; ref != "" {
 		resolved, err := p.resolveExample(ref, ctx)
 		if err != nil {
-			return nil, p.wrapRef(ctx.LastLoc(), e.Locator, err)
+			return nil, p.wrapRef(ctx.LastLoc(), locator, err)
 		}
 		return resolved, nil
 	}
@@ -26,6 +27,6 @@ func (p *parser) parseExample(e *ogen.Example, ctx *jsonpointer.ResolveCtx) (_ *
 		Description:   e.Description,
 		Value:         e.Value,
 		ExternalValue: e.ExternalValue,
-		Locator:       e.Locator,
+		Locator:       locator,
 	}, nil
 }
diff --git a/openapi/parser/parse_header.go b/openapi/parser/parse_header.go
index cb2864a6..2f726f59 100644
--- a/openapi/parser/parse_header.go
+++ b/openapi/parser/parse_header.go
@@ -28,13 +28,14 @@ func (p *parser) parseHeader(name string, header *ogen.Header, ctx *jsonpointer.
 	if header == nil {
 		return nil, errors.New("header object is empty or null")
 	}
+	locator := header.Common.Locator
 	defer func() {
-		rerr = p.wrapLocation(ctx.LastLoc(), header.Locator, rerr)
+		rerr = p.wrapLocation(ctx.LastLoc(), locator, rerr)
 	}()
 	if ref := header.Ref; ref != "" {
 		resolved, err := p.resolveHeader(name, ref, ctx)
 		if err != nil {
-			return nil, p.wrapRef(ctx.LastLoc(), header.Locator, err)
+			return nil, p.wrapRef(ctx.LastLoc(), locator, err)
 		}
 		return resolved, nil
 	}
@@ -44,7 +45,7 @@ func (p *parser) parseHeader(name string, header *ogen.Header, ctx *jsonpointer.
 			return nil
 		}
 		err := errors.Errorf(`%q MUST NOT be specified, got %q`, name, got)
-		return p.wrapField(name, ctx.LastLoc(), header.Locator, err)
+		return p.wrapField(name, ctx.LastLoc(), locator, err)
 	}
 	if err := mustNotSpecified("in", header.In); err != nil {
 		return nil, err
@@ -61,13 +62,13 @@ func (p *parser) parseHeader(name string, header *ogen.Header, ctx *jsonpointer.
 	schema, err := p.parseSchema(header.Schema, ctx)
 	if err != nil {
 		err := errors.Wrap(err, "schema")
-		return nil, p.wrapField("schema", ctx.LastLoc(), header.Locator, err)
+		return nil, p.wrapField("schema", ctx.LastLoc(), locator, err)
 	}
 
-	content, err := p.parseParameterContent(header.Content, header.Locator.Field("content"), ctx)
+	content, err := p.parseParameterContent(header.Content, locator.Field("content"), ctx)
 	if err != nil {
 		err := errors.Wrap(err, "content")
-		return nil, p.wrapField("content", ctx.LastLoc(), header.Locator, err)
+		return nil, p.wrapField("content", ctx.LastLoc(), locator, err)
 	}
 
 	op := &openapi.Header{
@@ -80,7 +81,7 @@ func (p *parser) parseHeader(name string, header *ogen.Header, ctx *jsonpointer.
 		Style:       inferParamStyle(locatedIn, header.Style),
 		Explode:     inferParamExplode(locatedIn, header.Explode),
 		Required:    header.Required,
-		Locator:     header.Locator,
+		Locator:     locator,
 	}
 
 	// TODO: Validate content?
diff --git a/openapi/parser/parse_mediatype.go b/openapi/parser/parse_mediatype.go
index 2479f2bc..42ca5e34 100644
--- a/openapi/parser/parse_mediatype.go
+++ b/openapi/parser/parse_mediatype.go
@@ -78,8 +78,9 @@ func (p *parser) parseParameterContent(
 }
 
 func (p *parser) parseMediaType(ct string, m ogen.Media, ctx *jsonpointer.ResolveCtx) (_ *openapi.MediaType, rerr error) {
+	locator := m.Common.Locator
 	defer func() {
-		rerr = p.wrapLocation(ctx.LastLoc(), m.Locator, rerr)
+		rerr = p.wrapLocation(ctx.LastLoc(), locator, rerr)
 	}()
 
 	s, err := p.parseSchema(m.Schema, ctx)
@@ -100,8 +101,9 @@ func (p *parser) parseMediaType(ct string, m ogen.Media, ctx *jsonpointer.Resolv
 			}
 
 			parseEncoding := func(name string, prop jsonschema.Property, e ogen.Encoding) (rerr error) {
+				locator := e.Common.Locator
 				defer func() {
-					rerr = p.wrapLocation(ctx.LastLoc(), e.Locator, rerr)
+					rerr = p.wrapLocation(ctx.LastLoc(), locator, rerr)
 				}()
 
 				encoding := &openapi.Encoding{
@@ -110,11 +112,11 @@ func (p *parser) parseMediaType(ct string, m ogen.Media, ctx *jsonpointer.Resolv
 					Style:         inferParamStyle(openapi.LocationQuery, e.Style),
 					Explode:       inferParamExplode(openapi.LocationQuery, e.Explode),
 					AllowReserved: e.AllowReserved,
-					Locator:       e.Locator,
+					Locator:       locator,
 				}
 				encoding.Headers, err = p.parseHeaders(e.Headers, ctx)
 				if err != nil {
-					return p.wrapField("headers", ctx.LastLoc(), e.Locator, err)
+					return p.wrapField("headers", ctx.LastLoc(), locator, err)
 				}
 				encodings[name] = encoding
 
@@ -134,7 +136,7 @@ func (p *parser) parseMediaType(ct string, m ogen.Media, ctx *jsonpointer.Resolv
 				return nil
 			}
 
-			encodingLoc := m.Locator.Field("encoding")
+			encodingLoc := locator.Field("encoding")
 			for name, e := range m.Encoding {
 				prop, ok := names[name]
 				if !ok {
@@ -175,6 +177,6 @@ func (p *parser) parseMediaType(ct string, m ogen.Media, ctx *jsonpointer.Resolv
 		Example:  json.RawMessage(m.Example),
 		Examples: examples,
 		Encoding: encodings,
-		Locator:  m.Locator,
+		Locator:  locator,
 	}, nil
 }
diff --git a/openapi/parser/parse_parameter.go b/openapi/parser/parse_parameter.go
index dff19c1a..1dd44822 100644
--- a/openapi/parser/parse_parameter.go
+++ b/openapi/parser/parse_parameter.go
@@ -69,7 +69,7 @@ func (p *parser) parseParams(
 		}
 		if _, ok := unique[ploc]; ok {
 			err := errors.Errorf("duplicate parameter: %q in %q", param.Name, param.In)
-			return nil, p.wrapLocation(ctx.LastLoc(), spec.Locator, err)
+			return nil, p.wrapLocation(ctx.LastLoc(), spec.Common.Locator, err)
 		}
 
 		unique[ploc] = struct{}{}
@@ -85,16 +85,17 @@ func (p *parser) validateParameter(
 	param *ogen.Parameter,
 	file string,
 ) error {
+	locator := param.Common.Locator
 	switch {
 	case param.Schema != nil && param.Content != nil:
 		err := errors.New("parameter MUST contain either a schema property, or a content property, but not both")
-		return p.wrapField("schema", file, param.Locator, err)
+		return p.wrapField("schema", file, 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
 		err := errors.New("content must have at least one entry")
-		return p.wrapField("content", file, param.Locator, err)
+		return p.wrapField("content", file, locator, err)
 	}
 
 	// Path parameters are always required.
@@ -102,12 +103,12 @@ func (p *parser) validateParameter(
 	case openapi.LocationPath:
 		if !param.Required {
 			err := errors.New("path parameters must be required")
-			return p.wrapField("required", file, param.Locator, err)
+			return p.wrapField("required", file, locator, err)
 		}
 	case openapi.LocationHeader:
 		if !httpguts.ValidHeaderFieldName(name) {
 			err := errors.Errorf("invalid header name %q", name)
-			return p.wrapField("name", file, param.Locator, err)
+			return p.wrapField("name", file, locator, err)
 		}
 	}
 	return nil
@@ -117,13 +118,14 @@ func (p *parser) parseParameter(param *ogen.Parameter, ctx *jsonpointer.ResolveC
 	if param == nil {
 		return nil, errors.New("parameter object is empty or null")
 	}
+	locator := param.Common.Locator
 	defer func() {
-		rerr = p.wrapLocation(ctx.LastLoc(), param.Locator, rerr)
+		rerr = p.wrapLocation(ctx.LastLoc(), locator, rerr)
 	}()
 	if ref := param.Ref; ref != "" {
 		parsed, err := p.resolveParameter(ref, ctx)
 		if err != nil {
-			return nil, p.wrapRef(ctx.LastLoc(), param.Locator, err)
+			return nil, p.wrapRef(ctx.LastLoc(), locator, err)
 		}
 		return parsed, nil
 	}
@@ -138,7 +140,7 @@ func (p *parser) parseParameter(param *ogen.Parameter, ctx *jsonpointer.ResolveC
 	locatedIn, exists := types[strings.ToLower(param.In)]
 	if !exists {
 		err := errors.Errorf("unknown parameter location %q", param.In)
-		return nil, p.wrapField("in", ctx.LastLoc(), param.Locator, err)
+		return nil, p.wrapField("in", ctx.LastLoc(), locator, err)
 	}
 
 	if err := p.validateParameter(param.Name, locatedIn, param, ctx.LastLoc()); err != nil {
@@ -148,13 +150,13 @@ func (p *parser) parseParameter(param *ogen.Parameter, ctx *jsonpointer.ResolveC
 	schema, err := p.parseSchema(param.Schema, ctx)
 	if err != nil {
 		err := errors.Wrap(err, "schema")
-		return nil, p.wrapField("schema", ctx.LastLoc(), param.Locator, err)
+		return nil, p.wrapField("schema", ctx.LastLoc(), locator, err)
 	}
 
-	content, err := p.parseParameterContent(param.Content, param.Locator.Field("content"), ctx)
+	content, err := p.parseParameterContent(param.Content, locator.Field("content"), ctx)
 	if err != nil {
 		err := errors.Wrap(err, "content")
-		return nil, p.wrapField("content", ctx.LastLoc(), param.Locator, err)
+		return nil, p.wrapField("content", ctx.LastLoc(), locator, err)
 	}
 
 	op := &openapi.Parameter{
@@ -168,7 +170,7 @@ func (p *parser) parseParameter(param *ogen.Parameter, ctx *jsonpointer.ResolveC
 		Explode:       inferParamExplode(locatedIn, param.Explode),
 		Required:      param.Required,
 		AllowReserved: param.AllowReserved,
-		Locator:       param.Locator,
+		Locator:       locator,
 	}
 
 	// TODO: Validate content?
diff --git a/openapi/parser/parse_path_item.go b/openapi/parser/parse_path_item.go
index c8767096..1aac1235 100644
--- a/openapi/parser/parse_path_item.go
+++ b/openapi/parser/parse_path_item.go
@@ -20,19 +20,20 @@ func (p *parser) parsePathItem(
 	if item == nil {
 		return nil, errors.New("pathItem object is empty or null")
 	}
+	locator := item.Common.Locator
 	defer func() {
-		rerr = p.wrapLocation(ctx.LastLoc(), item.Locator, rerr)
+		rerr = p.wrapLocation(ctx.LastLoc(), locator, rerr)
 	}()
 
 	if ref := item.Ref; ref != "" {
 		ops, err := p.resolvePathItem(path, ref, operationIDs, ctx)
 		if err != nil {
-			return nil, p.wrapRef(ctx.LastLoc(), item.Locator, err)
+			return nil, p.wrapRef(ctx.LastLoc(), locator, err)
 		}
 		return ops, nil
 	}
 
-	itemParams, err := p.parseParams(item.Parameters, item.Locator.Field("parameters"), ctx)
+	itemParams, err := p.parseParams(item.Parameters, locator.Field("parameters"), ctx)
 	if err != nil {
 		return nil, errors.Wrap(err, "parameters")
 	}
@@ -69,8 +70,9 @@ func (p *parser) parseOp(
 	itemParams []*openapi.Parameter,
 	ctx *jsonpointer.ResolveCtx,
 ) (_ *openapi.Operation, err error) {
+	locator := spec.Common.Locator
 	defer func() {
-		err = p.wrapLocation(ctx.LastLoc(), spec.Locator, err)
+		err = p.wrapLocation(ctx.LastLoc(), locator, err)
 	}()
 
 	op := &openapi.Operation{
@@ -79,10 +81,10 @@ func (p *parser) parseOp(
 		Description: spec.Description,
 		Deprecated:  spec.Deprecated,
 		HTTPMethod:  httpMethod,
-		Locator:     spec.Locator,
+		Locator:     locator,
 	}
 
-	opParams, err := p.parseParams(spec.Parameters, spec.Locator.Field("parameters"), ctx)
+	opParams, err := p.parseParams(spec.Parameters, locator.Field("parameters"), ctx)
 	if err != nil {
 		return nil, errors.Wrap(err, "parameters")
 	}
@@ -103,7 +105,7 @@ func (p *parser) parseOp(
 	}
 
 	{
-		locator := spec.Locator.Field("responses")
+		locator := locator.Field("responses")
 		op.Responses, err = p.parseResponses(spec.Responses, locator, ctx)
 		if err != nil {
 			err := errors.Wrap(err, "responses")
@@ -127,7 +129,7 @@ func (p *parser) parseOp(
 	if spec.Security != nil {
 		// Use operation level security.
 		security = spec.Security
-		securityParent = spec.Locator
+		securityParent = locator
 	}
 	if err := parseSecurity(security, securityParent.Field("security")); err != nil {
 		return nil, err
diff --git a/openapi/parser/parse_request_body.go b/openapi/parser/parse_request_body.go
index d0a90a58..b536def9 100644
--- a/openapi/parser/parse_request_body.go
+++ b/openapi/parser/parse_request_body.go
@@ -12,13 +12,14 @@ func (p *parser) parseRequestBody(body *ogen.RequestBody, ctx *jsonpointer.Resol
 	if body == nil {
 		return nil, errors.New("requestBody object is empty or null")
 	}
+	locator := body.Common.Locator
 	defer func() {
-		rerr = p.wrapLocation(ctx.LastLoc(), body.Locator, rerr)
+		rerr = p.wrapLocation(ctx.LastLoc(), locator, rerr)
 	}()
 	if ref := body.Ref; ref != "" {
 		resolved, err := p.resolveRequestBody(ref, ctx)
 		if err != nil {
-			return nil, p.wrapRef(ctx.LastLoc(), body.Locator, err)
+			return nil, p.wrapRef(ctx.LastLoc(), locator, err)
 		}
 		return resolved, nil
 	}
@@ -29,7 +30,7 @@ func (p *parser) parseRequestBody(body *ogen.RequestBody, ctx *jsonpointer.Resol
 			return nil, errors.New("content must have at least one entry")
 		}
 
-		content, err := p.parseContent(body.Content, body.Locator.Field("content"), ctx)
+		content, err := p.parseContent(body.Content, locator.Field("content"), ctx)
 		if err != nil {
 			return nil, errors.Wrap(err, "parse content")
 		}
@@ -37,13 +38,13 @@ func (p *parser) parseRequestBody(body *ogen.RequestBody, ctx *jsonpointer.Resol
 		return content, nil
 	}()
 	if err != nil {
-		return nil, p.wrapField("content", ctx.LastLoc(), body.Locator, err)
+		return nil, p.wrapField("content", ctx.LastLoc(), locator, err)
 	}
 
 	return &openapi.RequestBody{
 		Description: body.Description,
 		Required:    body.Required,
 		Content:     content,
-		Locator:     body.Locator,
+		Locator:     locator,
 	}, nil
 }
diff --git a/openapi/parser/parse_response.go b/openapi/parser/parse_response.go
index d40596b7..432aa8cf 100644
--- a/openapi/parser/parse_response.go
+++ b/openapi/parser/parse_response.go
@@ -42,34 +42,35 @@ func (p *parser) parseResponse(resp *ogen.Response, ctx *jsonpointer.ResolveCtx)
 	if resp == nil {
 		return nil, errors.New("response object is empty or null")
 	}
+	locator := resp.Common.Locator
 	defer func() {
-		rerr = p.wrapLocation(ctx.LastLoc(), resp.Locator, rerr)
+		rerr = p.wrapLocation(ctx.LastLoc(), locator, rerr)
 	}()
 	if ref := resp.Ref; ref != "" {
 		resolved, err := p.resolveResponse(ref, ctx)
 		if err != nil {
-			return nil, p.wrapRef(ctx.LastLoc(), resp.Locator, err)
+			return nil, p.wrapRef(ctx.LastLoc(), locator, err)
 		}
 		return resolved, nil
 	}
 
-	content, err := p.parseContent(resp.Content, resp.Locator.Field("content"), ctx)
+	content, err := p.parseContent(resp.Content, locator.Field("content"), ctx)
 	if err != nil {
 		err := errors.Wrap(err, "content")
-		return nil, p.wrapField("content", ctx.LastLoc(), resp.Locator, err)
+		return nil, p.wrapField("content", ctx.LastLoc(), locator, err)
 	}
 
 	headers, err := p.parseHeaders(resp.Headers, ctx)
 	if err != nil {
 		err := errors.Wrap(err, "headers")
-		return nil, p.wrapField("headers", ctx.LastLoc(), resp.Locator, err)
+		return nil, p.wrapField("headers", ctx.LastLoc(), locator, err)
 	}
 
 	return &openapi.Response{
 		Description: resp.Description,
 		Headers:     headers,
 		Content:     content,
-		Locator:     resp.Locator,
+		Locator:     locator,
 	}, nil
 }
 
diff --git a/openapi/parser/parse_security.go b/openapi/parser/parse_security.go
index ca9176ff..c723aff1 100644
--- a/openapi/parser/parse_security.go
+++ b/openapi/parser/parse_security.go
@@ -5,6 +5,7 @@ import (
 	"strings"
 
 	"github.com/go-faster/errors"
+	"golang.org/x/exp/maps"
 
 	"github.com/ogen-go/ogen"
 	"github.com/ogen-go/ogen/internal/jsonpointer"
@@ -19,14 +20,15 @@ func (p *parser) parseSecurityScheme(
 	if scheme == nil {
 		return nil, errors.New("securityScheme is empty or null")
 	}
+	locator := scheme.Common.Locator
 	defer func() {
-		rerr = p.wrapLocation(ctx.LastLoc(), scheme.Locator, rerr)
+		rerr = p.wrapLocation(ctx.LastLoc(), locator, rerr)
 	}()
 
 	if ref := scheme.Ref; ref != "" {
 		resolved, err := p.resolveSecurityScheme(ref, ctx)
 		if err != nil {
-			return nil, p.wrapRef(ctx.LastLoc(), scheme.Locator, err)
+			return nil, p.wrapRef(ctx.LastLoc(), locator, err)
 		}
 		return resolved, nil
 	}
@@ -38,11 +40,11 @@ func (p *parser) parseSecurityScheme(
 			case "query", "header", "cookie":
 			default:
 				err := errors.Errorf(`invalid "in": %q`, scheme.In)
-				return p.wrapField("in", ctx.LastLoc(), scheme.Locator, err)
+				return p.wrapField("in", ctx.LastLoc(), locator, err)
 			}
 			if scheme.Name == "" {
 				err := errors.New(`"name" is required and MUST be a non-empty string`)
-				return p.wrapField("name", ctx.LastLoc(), scheme.Locator, err)
+				return p.wrapField("name", ctx.LastLoc(), locator, err)
 			}
 			return nil
 		case "http":
@@ -64,23 +66,23 @@ func (p *parser) parseSecurityScheme(
 				"vapid":
 			default:
 				err := errors.Errorf(`invalid "scheme": %q`, scheme.Scheme)
-				return p.wrapField("scheme", ctx.LastLoc(), scheme.Locator, err)
+				return p.wrapField("scheme", ctx.LastLoc(), locator, err)
 			}
 			return nil
 		case "mutualTLS":
 			return nil
 		case "oauth2":
 			err := p.validateOAuthFlows(scheme.Flows, ctx.LastLoc())
-			return p.wrapField("flows", ctx.LastLoc(), scheme.Locator, err)
+			return p.wrapField("flows", ctx.LastLoc(), locator, err)
 		case "openIdConnect":
 			if _, err := url.ParseRequestURI(scheme.OpenIDConnectURL); err != nil {
 				err := errors.Wrap(err, `"openIdConnectUrl" MUST be in the form of a URL`)
-				return p.wrapField("openIdConnectUrl", ctx.LastLoc(), scheme.Locator, err)
+				return p.wrapField("openIdConnectUrl", ctx.LastLoc(), locator, err)
 			}
 			return nil
 		default:
 			err := errors.Errorf("unknown security scheme type %q", scheme.Type)
-			return p.wrapField("type", ctx.LastLoc(), scheme.Locator, err)
+			return p.wrapField("type", ctx.LastLoc(), locator, err)
 		}
 	}(); err != nil {
 		return nil, errors.Wrap(err, scheme.Type)
@@ -113,16 +115,18 @@ func (p *parser) validateOAuthFlows(flows *ogen.OAuthFlows, loc string) (rerr er
 	if flows == nil {
 		return errors.New("oAuthFlows is empty or null")
 	}
+	locator := flows.Common.Locator
 	defer func() {
-		rerr = p.wrapLocation(loc, flows.Locator, rerr)
+		rerr = p.wrapLocation(loc, locator, rerr)
 	}()
 
 	check := func(flow *ogen.OAuthFlow, authURL, tokenURL bool) (rerr error) {
 		if flow == nil {
 			return nil
 		}
+		locator := flow.Common.Locator
 		defer func() {
-			rerr = p.wrapLocation(loc, flow.Locator, rerr)
+			rerr = p.wrapLocation(loc, locator, rerr)
 		}()
 
 		checkURL := func(name, input string, check bool) error {
@@ -131,7 +135,7 @@ func (p *parser) validateOAuthFlows(flows *ogen.OAuthFlows, loc string) (rerr er
 			}
 			if _, err := url.ParseRequestURI(input); err != nil {
 				err := errors.Wrapf(err, `%q MUST be in the form of a URL`, name)
-				return p.wrapField(name, loc, flow.Locator, err)
+				return p.wrapField(name, loc, locator, err)
 			}
 			return nil
 		}
@@ -156,17 +160,13 @@ func cloneOAuthFlows(flows ogen.OAuthFlows) (r openapi.OAuthFlows) {
 		if flow == nil {
 			return nil
 		}
-		r := &openapi.OAuthFlow{
+		return &openapi.OAuthFlow{
 			AuthorizationURL: flow.AuthorizationURL,
 			TokenURL:         flow.TokenURL,
 			RefreshURL:       flow.RefreshURL,
-			Scopes:           make(map[string]string, len(flow.Scopes)),
-			Locator:          flow.Locator,
+			Scopes:           maps.Clone(flow.Scopes),
+			Locator:          flow.Common.Locator,
 		}
-		for k, v := range flow.Scopes {
-			r.Scopes[k] = v
-		}
-		return r
 	}
 
 	return openapi.OAuthFlows{
@@ -174,7 +174,7 @@ func cloneOAuthFlows(flows ogen.OAuthFlows) (r openapi.OAuthFlows) {
 		Password:          cloneFlow(flows.Password),
 		ClientCredentials: cloneFlow(flows.ClientCredentials),
 		AuthorizationCode: cloneFlow(flows.AuthorizationCode),
-		Locator:           flows.Locator,
+		Locator:           flows.Common.Locator,
 	}
 }
 
@@ -231,7 +231,7 @@ func (p *parser) parseSecurityRequirements(
 					BearerFormat:     spec.BearerFormat,
 					Flows:            cloneOAuthFlows(flows),
 					OpenIDConnectURL: spec.OpenIDConnectURL,
-					Locator:          spec.Locator,
+					Locator:          spec.Common.Locator,
 				},
 			})
 		}
diff --git a/schema.go b/schema.go
index b6e5ae18..2e22c0a4 100644
--- a/schema.go
+++ b/schema.go
@@ -7,6 +7,8 @@ import (
 	"github.com/go-faster/errors"
 	"github.com/go-faster/jx"
 	yaml "github.com/go-faster/yamlx"
+
+	"github.com/ogen-go/ogen/jsonschema"
 )
 
 // The Schema Object allows the definition of input and output data types.
@@ -239,7 +241,7 @@ type Schema struct {
 	// described is not a string.
 	ContentMediaType string `json:"contentMediaType,omitempty" yaml:"contentMediaType,omitempty"`
 
-	Locator Locator `json:"-" yaml:",inline"`
+	Common jsonschema.OpenAPICommon `json:"-" yaml:",inline"`
 }
 
 // Property is item of Properties.
@@ -500,6 +502,8 @@ type Discriminator struct {
 	PropertyName string `json:"propertyName" yaml:"propertyName"`
 	// An object to hold mappings between payload values and schema names or references.
 	Mapping map[string]string `json:"mapping,omitempty" yaml:"mapping,omitempty"`
+
+	Common OpenAPICommon `json:"-" yaml:",inline"`
 }
 
 // XML is a metadata object that allows for more fine-tuned XML model definitions.
@@ -532,4 +536,6 @@ type XML struct {
 	//
 	// Default value is false.
 	Wrapped bool `json:"wrapped,omitempty" yaml:"wrapped,omitempty"`
+
+	Common OpenAPICommon `json:"-" yaml:",inline"`
 }
diff --git a/spec_backcomp.go b/schema_backcomp.go
similarity index 97%
rename from spec_backcomp.go
rename to schema_backcomp.go
index 99823be3..860ecf7e 100644
--- a/spec_backcomp.go
+++ b/schema_backcomp.go
@@ -1,8 +1,6 @@
 package ogen
 
 import (
-	"encoding/json"
-
 	"github.com/ogen-go/ogen/jsonschema"
 )
 
@@ -56,8 +54,7 @@ func (s *Schema) ToJSONSchema() *jsonschema.RawSchema {
 		Discriminator:        s.Discriminator.ToJSONSchema(),
 		XML:                  s.XML.ToJSONSchema(),
 		Example:              s.Example,
-		XAnnotations:         map[string]json.RawMessage{},
-		Locator:              s.Locator,
+		Common:               s.Common,
 	}
 }
 
diff --git a/security_scheme.go b/security_scheme.go
index b21c675b..c6c1c274 100644
--- a/security_scheme.go
+++ b/security_scheme.go
@@ -25,7 +25,7 @@ type SecurityScheme struct {
 	// This MUST be in the form of a URL. The OpenID Connect standard requires the use of TLS.
 	OpenIDConnectURL string `json:"openIdConnectUrl,omitempty" yaml:"openIdConnectUrl,omitempty"`
 
-	Locator Locator `json:"-" yaml:",inline"`
+	Common OpenAPICommon `json:"-" yaml:",inline"`
 }
 
 // OAuthFlows allows configuration of the supported OAuth Flows.
@@ -41,7 +41,7 @@ type OAuthFlows struct {
 	// Configuration for the OAuth Authorization Code flow. Previously called accessCode in OpenAPI 2.0.
 	AuthorizationCode *OAuthFlow `json:"authorizationCode" yaml:"authorizationCode"`
 
-	Locator Locator `json:"-" yaml:",inline"`
+	Common OpenAPICommon `json:"-" yaml:",inline"`
 }
 
 // OAuthFlow is configuration details for a supported OAuth Flow.
@@ -61,7 +61,7 @@ type OAuthFlow struct {
 	// A map between the scope name and a short description for it. The map MAY be empty.
 	Scopes map[string]string `json:"scopes" yaml:"scopes"`
 
-	Locator Locator `json:"-" yaml:",inline"`
+	Common OpenAPICommon `json:"-" yaml:",inline"`
 }
 
 // SecurityRequirements lists the required security schemes to execute this operation.
diff --git a/spec.go b/spec.go
index 83f7ef5b..adc02267 100644
--- a/spec.go
+++ b/spec.go
@@ -21,6 +21,13 @@ type (
 	// RawValue is a raw JSON value.
 	RawValue = jsonschema.RawValue
 
+	// Extensions is a map of OpenAPI extensions.
+	//
+	// See https://spec.openapis.org/oas/v3.1.0#specification-extensions.
+	Extensions = jsonschema.Extensions
+	// OpenAPICommon is a common OpenAPI object fields (extensions and locator).
+	OpenAPICommon = jsonschema.OpenAPICommon
+
 	// Locator stores location of JSON value.
 	Locator = location.Locator
 )
@@ -71,6 +78,9 @@ type Spec struct {
 	// Additional external documentation.
 	ExternalDocs *ExternalDocumentation `json:"externalDocs,omitempty" yaml:"externalDocs,omitempty"`
 
+	// Specification extensions.
+	Extensions Extensions `json:"-" yaml:",inline"`
+
 	// Raw YAML node. Used by '$ref' resolvers.
 	Raw *yaml.Node `json:"-" yaml:"-"`
 }
@@ -138,6 +148,9 @@ type Info struct {
 	License *License `json:"license,omitempty" yaml:"license,omitempty"`
 	// REQUIRED. The version of the OpenAPI document.
 	Version string `json:"version" yaml:"version"`
+
+	// Specification extensions.
+	Extensions Extensions `json:"-" yaml:",inline"`
 }
 
 // Contact information for the exposed API.
@@ -150,6 +163,9 @@ type Contact struct {
 	URL string `json:"url,omitempty" yaml:"url,omitempty"`
 	// The email address of the contact person/organization.
 	Email string `json:"email,omitempty" yaml:"email,omitempty"`
+
+	// Specification extensions.
+	Extensions Extensions `json:"-" yaml:",inline"`
 }
 
 // License information for the exposed API.
@@ -162,6 +178,9 @@ type License struct {
 	Identifier string `json:"identifier,omitempty" yaml:"identifier,omitempty"`
 	// A URL to the license used for the API.
 	URL string `json:"url,omitempty" yaml:"url,omitempty"`
+
+	// Specification extensions.
+	Extensions Extensions `json:"-" yaml:",inline"`
 }
 
 // Server represents a Server.
@@ -177,6 +196,9 @@ type Server struct {
 	Description string `json:"description,omitempty" yaml:"description,omitempty"`
 	// A map between a variable name and its value. The value is used for substitution in the server's URL template.
 	Variables map[string]ServerVariable `json:"variables,omitempty" yaml:"variables,omitempty"`
+
+	// Specification extensions.
+	Extensions Extensions `json:"-" yaml:",inline"`
 }
 
 // ServerVariable describes an object representing a Server Variable for server URL template substitution.
@@ -193,6 +215,9 @@ type ServerVariable struct {
 	Default string `json:"default" yaml:"default"`
 	// An optional description for the server variable. CommonMark syntax MAY be used for rich text representation.
 	Description string `json:"description,omitempty" yaml:"description,omitempty"`
+
+	// Specification extensions.
+	Extensions Extensions `json:"-" yaml:",inline"`
 }
 
 // Components Holds a set of reusable objects for different aspects of the OAS.
@@ -201,18 +226,28 @@ type ServerVariable struct {
 //
 // See https://spec.openapis.org/oas/v3.1.0#components-object.
 type Components struct {
-	Schemas         map[string]*Schema         `json:"schemas,omitempty" yaml:"schemas,omitempty"`
-	Responses       map[string]*Response       `json:"responses,omitempty" yaml:"responses,omitempty"`
-	Parameters      map[string]*Parameter      `json:"parameters,omitempty" yaml:"parameters,omitempty"`
-	Examples        map[string]*Example        `json:"examples,omitempty" yaml:"examples,omitempty"`
-	RequestBodies   map[string]*RequestBody    `json:"requestBodies,omitempty" yaml:"requestBodies,omitempty"`
-	Headers         map[string]*Header         `json:"headers,omitempty" yaml:"headers,omitempty"`
+	// An object to hold reusable Schema Objects.
+	Schemas map[string]*Schema `json:"schemas,omitempty" yaml:"schemas,omitempty"`
+	// An object to hold reusable Response Objects.
+	Responses map[string]*Response `json:"responses,omitempty" yaml:"responses,omitempty"`
+	// An object to hold reusable Parameter Objects.
+	Parameters map[string]*Parameter `json:"parameters,omitempty" yaml:"parameters,omitempty"`
+	// An object to hold reusable Example Objects.
+	Examples map[string]*Example `json:"examples,omitempty" yaml:"examples,omitempty"`
+	// An object to hold reusable Request Body Objects.
+	RequestBodies map[string]*RequestBody `json:"requestBodies,omitempty" yaml:"requestBodies,omitempty"`
+	// An object to hold reusable Header Objects.
+	Headers map[string]*Header `json:"headers,omitempty" yaml:"headers,omitempty"`
+	// An object to hold reusable Security Scheme Objects.
 	SecuritySchemes map[string]*SecurityScheme `json:"securitySchemes,omitempty" yaml:"securitySchemes,omitempty"`
-	Links           map[string]*Link           `json:"links,omitempty" yaml:"links,omitempty"`
-	Callbacks       map[string]*Callback       `json:"callbacks,omitempty" yaml:"callbacks,omitempty"`
-	PathItems       map[string]*PathItem       `json:"pathItems,omitempty" yaml:"pathItems,omitempty"`
+	// An object to hold reusable Link Objects.
+	Links map[string]*Link `json:"links,omitempty" yaml:"links,omitempty"`
+	// An object to hold reusable Callback Objects.
+	Callbacks map[string]*Callback `json:"callbacks,omitempty" yaml:"callbacks,omitempty"`
+	// An object to hold reusable Path Item Objects.
+	PathItems map[string]*PathItem `json:"pathItems,omitempty" yaml:"pathItems,omitempty"`
 
-	Locator Locator `json:"-" yaml:",inline"`
+	Common OpenAPICommon `json:"-" yaml:",inline"`
 }
 
 // Init initializes all fields.
@@ -293,7 +328,7 @@ type PathItem struct {
 	// a combination of a name and location.
 	Parameters []*Parameter `json:"parameters,omitempty" yaml:"parameters,omitempty"`
 
-	Locator Locator `json:"-" yaml:",inline"`
+	Common OpenAPICommon `json:"-" yaml:",inline"`
 }
 
 // Operation describes a single API operation on a path.
@@ -347,7 +382,7 @@ type Operation struct {
 	// it will be overridden by this value.
 	Servers []Server `json:"servers,omitempty" yaml:"servers,omitempty"`
 
-	Locator Locator `json:"-" yaml:",inline"`
+	Common OpenAPICommon `json:"-" yaml:",inline"`
 }
 
 // ExternalDocumentation describes a reference to external resource for extended documentation.
@@ -358,6 +393,9 @@ type ExternalDocumentation struct {
 	Description string `json:"description,omitempty" yaml:"description,omitempty"`
 	// REQUIRED. The URL for the target documentation. This MUST be in the form of a URL.
 	URL string `json:"url" yaml:"url"`
+
+	// Specification extensions.
+	Extensions Extensions `json:"-" yaml:",inline"`
 }
 
 // Parameter describes a single operation parameter.
@@ -414,7 +452,7 @@ type Parameter struct {
 	// The map MUST only contain one entry.
 	Content map[string]Media `json:"content,omitempty" yaml:"content,omitempty"`
 
-	Locator Locator `json:"-" yaml:",inline"`
+	Common OpenAPICommon `json:"-" yaml:",inline"`
 }
 
 // RequestBody describes a single request body.
@@ -438,7 +476,7 @@ type RequestBody struct {
 	// Determines if the request body is required in the request. Defaults to false.
 	Required bool `json:"required,omitempty" yaml:"required,omitempty"`
 
-	Locator Locator `json:"-" yaml:",inline"`
+	Common OpenAPICommon `json:"-" yaml:",inline"`
 }
 
 // Media provides schema and examples for the media type identified by its key.
@@ -460,7 +498,7 @@ type Media struct {
 	// type is multipart or application/x-www-form-urlencoded.
 	Encoding map[string]Encoding `json:"encoding,omitempty" yaml:"encoding,omitempty"`
 
-	Locator Locator `json:"-" yaml:",inline"`
+	Common OpenAPICommon `json:"-" yaml:",inline"`
 }
 
 // Encoding describes single encoding definition applied to a single schema property.
@@ -491,7 +529,7 @@ type Encoding struct {
 	// is not application/x-www-form-urlencoded.
 	AllowReserved bool `json:"allowReserved,omitempty" yaml:"allowReserved,omitempty"`
 
-	Locator Locator `json:"-" yaml:",inline"`
+	Common OpenAPICommon `json:"-" yaml:",inline"`
 }
 
 // Responses is a container for the expected responses of an operation.
@@ -536,7 +574,7 @@ type Response struct {
 	// of the names for Component Objects.
 	Links map[string]*Link `json:"links,omitempty" yaml:"links,omitempty"`
 
-	Locator Locator `json:"-" yaml:",inline"`
+	Common OpenAPICommon `json:"-" yaml:",inline"`
 }
 
 // Callback is a map of possible out-of band callbacks related to the parent operation.
@@ -569,7 +607,7 @@ type Example struct {
 	// A URI that points to the literal example.
 	ExternalValue string `json:"externalValue,omitempty" yaml:"externalValue,omitempty"`
 
-	Locator Locator `json:"-" yaml:",inline"`
+	Common OpenAPICommon `json:"-" yaml:",inline"`
 }
 
 // Link describes a possible design-time link for a response.
@@ -592,6 +630,8 @@ type Link struct {
 	// The key is the parameter name to be used, whereas the value can be a constant or an expression to be
 	// evaluated and passed to the linked operation.
 	Parameters map[string]RawValue `json:"parameters,omitempty" yaml:"parameters,omitempty"`
+
+	Common OpenAPICommon `json:"-" yaml:",inline"`
 }
 
 // Header describes header response.
@@ -615,4 +655,7 @@ type Tag struct {
 	Description string `json:"description,omitempty" yaml:"description,omitempty"`
 	// Additional external documentation for this tag.
 	ExternalDocs *ExternalDocumentation `json:"externalDocs,omitempty" yaml:"externalDocs,omitempty"`
+
+	// Specification extensions.
+	Extensions Extensions `json:"-" yaml:",inline"`
 }


Test patch:
diff --git a/dsl_test.go b/dsl_test.go
index db793ee2..9dc8ceca 100644
--- a/dsl_test.go
+++ b/dsl_test.go
@@ -18,6 +18,11 @@ const (
 )
 
 var (
+	_extensions = ogen.Extensions(nil)
+	_common     = ogen.OpenAPICommon{
+		Extensions: _extensions,
+	}
+
 	// reusable query param
 	_queryParam = ogen.NewParameter().
 			InQuery().
@@ -103,8 +108,8 @@ func TestBuilder(t *testing.T) {
 			Version: "0.1.0",
 		},
 		Servers: []ogen.Server{
-			{"staging", "staging.api.com", nil},
-			{"production", "api.com", nil},
+			{"staging", "staging.api.com", nil, _extensions},
+			{"production", "api.com", nil, _extensions},
 		},
 		Paths: map[string]*ogen.PathItem{
 			pathWithID: {
@@ -212,6 +217,7 @@ func TestBuilder(t *testing.T) {
 				},
 			},
 		},
+		Extensions: _extensions,
 	}
 	// referenced path
 	path := ogen.NewPathItem().
@@ -328,18 +334,22 @@ func TestBuilder(t *testing.T) {
 		SetHead(ogen.NewOperation().SetOperationID("head").SetResponses(ogen.Responses{"resp": ogen.NewResponse()})).
 		SetPatch(ogen.NewOperation().SetOperationID("patch").AddParameters(ogen.NewParameter().InHeader().SetDeprecated(true))).
 		SetTrace(ogen.NewOperation().SetOperationID("trace")).
-		SetServers([]ogen.Server{{"url1", "desc1", nil}}).
+		SetServers([]ogen.Server{{"url1", "desc1", nil, nil}}).
 		AddServers(ogen.NewServer().SetDescription("desc2").SetURL("url2")).
 		SetParameters([]*ogen.Parameter{_queryParam.Parameter})
 	assert.Equal(t, &ogen.PathItem{
-		Put:        &ogen.Operation{OperationID: "put", Tags: []string{"tag1", "tag2"}},
-		Delete:     &ogen.Operation{OperationID: "delete", Summary: "summary"},
-		Options:    &ogen.Operation{OperationID: "options", Parameters: []*ogen.Parameter{_cookieParam.Parameter}},
-		Head:       &ogen.Operation{OperationID: "head", Responses: ogen.Responses{"resp": &ogen.Response{}}},
-		Patch:      &ogen.Operation{OperationID: "patch", Parameters: []*ogen.Parameter{{In: "header", Deprecated: true}}},
-		Trace:      &ogen.Operation{OperationID: "trace"},
-		Servers:    []ogen.Server{{"url1", "desc1", nil}, {"url2", "desc2", nil}},
+		Put:     &ogen.Operation{OperationID: "put", Tags: []string{"tag1", "tag2"}},
+		Delete:  &ogen.Operation{OperationID: "delete", Summary: "summary"},
+		Options: &ogen.Operation{OperationID: "options", Parameters: []*ogen.Parameter{_cookieParam.Parameter}},
+		Head:    &ogen.Operation{OperationID: "head", Responses: ogen.Responses{"resp": &ogen.Response{}}},
+		Patch:   &ogen.Operation{OperationID: "patch", Parameters: []*ogen.Parameter{{In: "header", Deprecated: true}}},
+		Trace:   &ogen.Operation{OperationID: "trace"},
+		Servers: []ogen.Server{
+			{"url1", "desc1", nil, nil},
+			{"url2", "desc2", nil, nil},
+		},
 		Parameters: []*ogen.Parameter{_queryParam.Parameter},
+		Common:     _common,
 	}, pi)
 
 	mlt := uint64(1)
diff --git a/jsonschema/parser_test.go b/jsonschema/parser_test.go
index 505db4cc..5e001c85 100644
--- a/jsonschema/parser_test.go
+++ b/jsonschema/parser_test.go
@@ -1,14 +1,17 @@
 package jsonschema
 
 import (
+	"fmt"
 	"strconv"
 	"strings"
 	"testing"
 
 	"github.com/go-faster/errors"
+	yaml "github.com/go-faster/yamlx"
 	"github.com/stretchr/testify/require"
 
 	"github.com/ogen-go/ogen/internal/jsonpointer"
+	"github.com/ogen-go/ogen/internal/location"
 )
 
 type components map[string]*RawSchema
@@ -345,6 +348,46 @@ func TestSchemaReferencedArray(t *testing.T) {
 	require.Equal(t, expect, out)
 }
 
+func TestSchemaExtensions(t *testing.T) {
+	tests := []struct {
+		raw       string
+		expect    *Schema
+		expectErr bool
+	}{
+		{
+			`{"type": "string", "x-ogen-name": "foo"}`,
+			&Schema{
+				Type:      String,
+				XOgenName: "foo",
+			},
+			false,
+		},
+		{`{"type": "string", "x-ogen-name": {}}`, nil, true},
+	}
+
+	for i, tt := range tests {
+		tt := tt
+		t.Run(fmt.Sprintf("Test%d", i+1), func(t *testing.T) {
+			a := require.New(t)
+
+			var raw RawSchema
+			a.NoError(yaml.Unmarshal([]byte(tt.raw), &raw))
+
+			out, err := NewParser(Settings{
+				Filename: "test.yaml",
+			}).Parse(&raw)
+			if tt.expectErr {
+				a.Error(err)
+				return
+			}
+			a.NoError(err)
+			// Zero locator to simplify comparison.
+			out.Locator = location.Locator{}
+			a.Equal(tt.expect, out)
+		})
+	}
+}
+
 func TestInvalidMultipleOf(t *testing.T) {
 	values := []int{0, -1, -10}
 	parser := NewParser(Settings{
diff --git a/location_test.go b/location_test.go
index 3d61afc7..b7053b67 100644
--- a/location_test.go
+++ b/location_test.go
@@ -138,26 +138,26 @@ func TestLocation(t *testing.T) {
 			schema = media.Schema
 		)
 		// Compare PathItem.
-		equalLoc(&foo.Locator, 8, 13)
+		equalLoc(&foo.Common.Locator, 8, 13)
 
 		// Compare post
-		equalLoc(&post.Locator, 9, 15)
+		equalLoc(&post.Common.Locator, 9, 15)
 
 		// Compare Parameters.
-		equalLoc(&post.Parameters[0].Locator, 11, 11)
-		equalLoc(&post.Parameters[1].Locator, 18, 11)
+		equalLoc(&post.Parameters[0].Common.Locator, 11, 11)
+		equalLoc(&post.Parameters[1].Common.Locator, 18, 11)
 
 		// Compare RequestBody.
-		equalLoc(&body.Locator, 26, 24)
-		equalLoc(&media.Locator, 28, 33)
-		equalLoc(&schema.Locator, 29, 25)
+		equalLoc(&body.Common.Locator, 26, 24)
+		equalLoc(&media.Common.Locator, 28, 33)
+		equalLoc(&schema.Common.Locator, 29, 25)
 
 		// Compare get.
-		equalLoc(&get.Locator, 48, 14)
+		equalLoc(&get.Common.Locator, 48, 14)
 
 		var user = locationSpec.Components.Schemas["User"]
-		equalLoc(&user.Locator, 59, 15)
-		equalLoc(&user.Properties[0].Schema.Locator, 62, 19)
+		equalLoc(&user.Common.Locator, 59, 15)
+		equalLoc(&user.Properties[0].Schema.Common.Locator, 62, 19)
 	})
 	t.Run("YAML", func(t *testing.T) {
 		a := assert.New(t)
@@ -175,20 +175,20 @@ func TestLocation(t *testing.T) {
 		)
 		// FIXME(tdakkota): parser sets map/seq location to the first element.
 		// Compare PathItem and Operation.
-		equalLoc(&foo.Locator, 7, 5)
-		equalLoc(&post.Locator, 8, 7)
+		equalLoc(&foo.Common.Locator, 7, 5)
+		equalLoc(&post.Common.Locator, 8, 7)
 
 		// Compare Parameters.
-		equalLoc(&post.Parameters[0].Locator, 9, 11)
-		equalLoc(&post.Parameters[1].Locator, 13, 11)
+		equalLoc(&post.Parameters[0].Common.Locator, 9, 11)
+		equalLoc(&post.Parameters[1].Common.Locator, 13, 11)
 
 		// Compare RequestBody.
-		equalLoc(&body.Locator, 18, 9)
-		equalLoc(&requestMedia.Locator, 20, 13)
-		equalLoc(&requestSchema.Locator, 21, 15)
+		equalLoc(&body.Common.Locator, 18, 9)
+		equalLoc(&requestMedia.Common.Locator, 20, 13)
+		equalLoc(&requestSchema.Common.Locator, 21, 15)
 
 		var user = locationSpec.Components.Schemas["User"]
-		equalLoc(&user.Locator, 36, 7)
-		equalLoc(&user.Properties[0].Schema.Locator, 39, 11)
+		equalLoc(&user.Common.Locator, 36, 7)
+		equalLoc(&user.Properties[0].Schema.Common.Locator, 39, 11)
 	})
 }
diff --git a/spec_test.go b/spec_test.go
new file mode 100644
index 00000000..bd15bb71
--- /dev/null
+++ b/spec_test.go
@@ -0,0 +1,43 @@
+package ogen_test
+
+import (
+	"testing"
+
+	yaml "github.com/go-faster/yamlx"
+	"github.com/stretchr/testify/require"
+
+	"github.com/ogen-go/ogen"
+)
+
+func encodeDecode[T any](a *require.Assertions, input T) (result T) {
+	data, err := yaml.Marshal(input)
+	a.NoError(err)
+
+	a.NoError(yaml.Unmarshal(data, &result))
+	return result
+}
+
+func TestExtensionParsing(t *testing.T) {
+	a := require.New(t)
+
+	{
+		var (
+			input = `{"url": "/api/v1", "x-ogen-name": "foo"}`
+			s     ogen.Server
+		)
+		a.NoError(yaml.Unmarshal([]byte(input), &s))
+		a.Equal("foo", s.Extensions["x-ogen-name"].Value)
+		s2 := encodeDecode(a, s)
+		a.Equal("foo", s2.Extensions["x-ogen-name"].Value)
+	}
+
+	{
+		var (
+			input = `{"description": "foo", "x-ogen-extension": "bar"}`
+			s     ogen.Response
+		)
+		a.NoError(yaml.Unmarshal([]byte(input), &s))
+		a.Equal("bar", s.Common.Extensions["x-ogen-extension"].Value)
+		// FIXME(tdakkota): encodeDecode doesn't work for this type
+	}
+}