Repository: ogen-go/ogen
Pull request #679 (2022-11-16 06:34:20)

fix(jsonschema): parse `discriminator` properly

codecov[bot]: # [Codecov](https://codecov.io/gh/ogen-go/ogen/pull/679?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=ogen-go) Report
Base: **73.70**% // Head: **73.81**% // Increases project coverage by **`+0.10%`** :tada:
> Coverage data is based on head [(`172e634`)](https://codecov.io/gh/ogen-go/ogen/pull/679?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=ogen-go) compared to base [(`56c1a52`)](https://codecov.io/gh/ogen-go/ogen/commit/56c1a52b88fce2a81eddb063fd845a1c350e3732?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=ogen-go).
> Patch coverage: 88.40% of modified lines in pull request are covered.

<details><summary>Additional details and impacted files</summary>


```diff
@@            Coverage Diff             @@
##             main     #679      +/-   ##
==========================================
+ Coverage   73.70%   73.81%   +0.10%     
==========================================
  Files         172      172              
  Lines       12367    12387      +20     
==========================================
+ Hits         9115     9143      +28     
+ Misses       2770     2764       -6     
+ Partials      482      480       -2     
```


| [Impacted Files](https://codecov.io/gh/ogen-go/ogen/pull/679?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=ogen-go) | Coverage Δ | |
|---|---|---|
| [jsonschema/schema.go](https://codecov.io/gh/ogen-go/ogen/pull/679/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=ogen-go#diff-anNvbnNjaGVtYS9zY2hlbWEuZ28=) | `100.00% <ø> (ø)` | |
| [gen/schema\_gen\_sum.go](https://codecov.io/gh/ogen-go/ogen/pull/679/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=ogen-go#diff-Z2VuL3NjaGVtYV9nZW5fc3VtLmdv) | `72.60% <40.00%> (-0.10%)` | :arrow_down: |
| [jsonschema/parser.go](https://codecov.io/gh/ogen-go/ogen/pull/679/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=ogen-go#diff-anNvbnNjaGVtYS9wYXJzZXIuZ28=) | `84.31% <91.80%> (+0.89%)` | :arrow_up: |
| [schema\_backcomp.go](https://codecov.io/gh/ogen-go/ogen/pull/679/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=ogen-go#diff-c2NoZW1hX2JhY2tjb21wLmdv) | `93.00% <100.00%> (+0.07%)` | :arrow_up: |
| [jsonschema/resolve.go](https://codecov.io/gh/ogen-go/ogen/pull/679/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=ogen-go#diff-anNvbnNjaGVtYS9yZXNvbHZlLmdv) | `67.85% <0.00%> (+5.35%)` | :arrow_up: |
| [jsonschema/errors.go](https://codecov.io/gh/ogen-go/ogen/pull/679/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=ogen-go#diff-anNvbnNjaGVtYS9lcnJvcnMuZ28=) | `89.65% <0.00%> (+10.34%)` | :arrow_up: |
| [jsonschema/parser\_settings.go](https://codecov.io/gh/ogen-go/ogen/pull/679/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=ogen-go#diff-anNvbnNjaGVtYS9wYXJzZXJfc2V0dGluZ3MuZ28=) | `100.00% <0.00%> (+22.22%)` | :arrow_up: |

Help us with your feedback. Take ten seconds to tell us [how you rate us](https://about.codecov.io/nps?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=ogen-go). Have a feature suggestion? [Share it here.](https://app.codecov.io/gh/feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=ogen-go)

</details>

[:umbrella: View full report at Codecov](https://codecov.io/gh/ogen-go/ogen/pull/679?src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=ogen-go).   
:loudspeaker: Do you have feedback about the report comment? [Let us know in this issue](https://about.codecov.io/codecov-pr-comment-feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=ogen-go).


Patch:
diff --git a/gen/schema_gen_sum.go b/gen/schema_gen_sum.go
index fde354e6..b0ca591f 100644
--- a/gen/schema_gen_sum.go
+++ b/gen/schema_gen_sum.go
@@ -5,6 +5,7 @@ import (
 	"fmt"
 	"path"
 	"reflect"
+	"strings"
 
 	"github.com/go-faster/errors"
 	"github.com/go-faster/jx"
@@ -134,6 +135,14 @@ func (g *schemaGen) collectSumVariants(
 	return sum, nil
 }
 
+func schemaName(ref string) (string, bool) {
+	_, after, ok := strings.Cut(ref, "#/")
+	if !ok || after == "" {
+		return "", false
+	}
+	return path.Base(after), true
+}
+
 func (g *schemaGen) anyOf(name string, schema *jsonschema.Schema) (*ir.Type, error) {
 	if err := ensureNoInfiniteRecursion(schema); err != nil {
 		return nil, err
@@ -220,14 +229,12 @@ func (g *schemaGen) oneOf(name string, schema *jsonschema.Schema) (*ir.Type, err
 				if !s.Is(ir.KindStruct, ir.KindMap) {
 					return nil, errors.Wrapf(&ErrNotImplemented{"unsupported sum type variant"}, "%q", s.Kind)
 				}
-				var ref string
-				if s.Schema != nil {
-					ref = s.Schema.Ref
-				} else {
-					ref = schema.OneOf[i].Ref
+				vschema := s.Schema
+				if vschema == nil {
+					vschema = schema.OneOf[i]
 				}
 
-				if ref == v || path.Base(ref) == v {
+				if vschema == v {
 					found = true
 					sum.SumSpec.Mapping = append(sum.SumSpec.Mapping, ir.SumSpecMap{
 						Key:  k,
@@ -241,11 +248,12 @@ func (g *schemaGen) oneOf(name string, schema *jsonschema.Schema) (*ir.Type, err
 				}
 			}
 			if !found {
-				return nil, errors.Errorf("discriminator: unable to map %q to %q", k, v)
+				return nil, errors.Errorf("discriminator: unable to map %q to %q", k, v.Ref)
 			}
 		}
 		if len(sum.SumSpec.Mapping) == 0 {
 			// Implicit mapping, defaults to type name.
+			keys := map[string]struct{}{}
 			for i, s := range sum.SumOf {
 				var ref string
 				if s.Schema != nil {
@@ -254,8 +262,39 @@ func (g *schemaGen) oneOf(name string, schema *jsonschema.Schema) (*ir.Type, err
 					ref = schema.OneOf[i].Ref
 				}
 
+				key, err := func() (string, error) {
+					// Spec says (https://spec.openapis.org/oas/v3.1.0#discriminator-object):
+					//
+					// 	The expectation now is that a property with name petType MUST be present in the response payload,
+					// 	and the value will correspond to the name of a schema defined in the OAS document
+					//
+					// What is name of a schema? Is it the last part of the pointer?
+					// What if pointer part of reference is empty, like `User.json#`?
+					//
+					// As always, OpenAPI is not clear enough.
+					key, ok := schemaName(ref)
+					if !ok {
+						return "", errors.Wrap(
+							&ErrNotImplemented{"complicated reference"},
+							"unable to extract schema name",
+						)
+					}
+
+					if _, ok := keys[key]; ok {
+						return "", errors.Wrapf(
+							&ErrNotImplemented{"duplicate mapping key"},
+							"key %q", key,
+						)
+					}
+					keys[key] = struct{}{}
+					return key, nil
+				}()
+				if err != nil {
+					return nil, errors.Wrapf(err, "mapping %q", ref)
+				}
+
 				sum.SumSpec.Mapping = append(sum.SumSpec.Mapping, ir.SumSpecMap{
-					Key:  path.Base(ref),
+					Key:  key,
 					Type: s,
 				})
 			}
@@ -486,7 +525,7 @@ func mergeSchemes(s1, s2 *jsonschema.Schema) (_ *jsonschema.Schema, err error) {
 		Format:      s1.Format,
 		Enum:        enum,
 		Nullable:    s1.Nullable || s2.Nullable,
-		Description: "Merged schema",
+		Description: "Merged schema", // TODO(tdakkota): handle in a better way.
 	}
 
 	switch {
@@ -507,6 +546,15 @@ func mergeSchemes(s1, s2 *jsonschema.Schema) (_ *jsonschema.Schema, err error) {
 		r.DefaultSet = true
 	}
 
+	switch d1, d2 := s1.Discriminator, s2.Discriminator; {
+	case d1 != nil && d2 != nil:
+		return nil, &ErrNotImplemented{"merge discriminator"} // TODO(tdakkota): implement
+	case d1 != nil:
+		r.Discriminator = d1
+	case d2 != nil:
+		r.Discriminator = d2
+	}
+
 	// Helper functions for comparing validation fields.
 	var (
 		someU64 = func(n1, n2 *uint64, both func(n1, n2 uint64) uint64) *uint64 {
@@ -712,7 +760,7 @@ func mergeProperties(p1, p2 []jsonschema.Property) ([]jsonschema.Property, error
 
 			propmap[p.Name] = jsonschema.Property{
 				Name:        p.Name,
-				Description: "Merged property",
+				Description: "Merged property", // TODO(tdakkota): handle in a better way.
 				Schema:      s,
 				Required:    p.Required || confP.Required,
 			}
diff --git a/jsonschema/parser.go b/jsonschema/parser.go
index f56cffab..93c69a55 100644
--- a/jsonschema/parser.go
+++ b/jsonschema/parser.go
@@ -4,6 +4,7 @@ package jsonschema
 import (
 	"encoding/json"
 	"math/big"
+	"strings"
 
 	"github.com/go-faster/errors"
 	"golang.org/x/exp/slices"
@@ -71,57 +72,67 @@ func (p *Parser) parse(schema *RawSchema, ctx *jsonpointer.ResolveCtx) (_ *Schem
 func (p *Parser) parse1(schema *RawSchema, ctx *jsonpointer.ResolveCtx, hook func(*Schema) *Schema) (*Schema, error) {
 	s, err := p.parseSchema(schema, ctx, hook)
 	if err != nil {
-		return nil, errors.Wrap(err, "parse schema")
+		return nil, err
 	}
 
-	if schema != nil && s != nil {
-		if enum := schema.Enum; len(enum) > 0 {
-			loc := schema.Common.Locator.Field("enum")
-			for i := range enum {
-				for j := range enum {
-					if i == j {
-						continue
-					}
-					a, b := enum[i], enum[j]
-					if ok, _ := ogenjson.Equal(a, b); ok {
-						loc := loc.Index(i)
-						err := errors.Errorf("duplicate enum value: %q", a)
-						return nil, p.wrapLocation(p.file(ctx), loc, err)
-					}
+	if schema == nil || s == nil {
+		return s, nil
+	}
+
+	if rd := schema.Discriminator; rd != nil {
+		d, err := p.parseDiscriminator(rd, ctx)
+		if err != nil {
+			return nil, errors.Wrap(err, "parse discriminator")
+		}
+		s.Discriminator = d
+	}
+
+	if enum := schema.Enum; len(enum) > 0 {
+		loc := schema.Common.Locator.Field("enum")
+		for i := range enum {
+			for j := range enum {
+				if i == j {
+					continue
+				}
+				a, b := enum[i], enum[j]
+				if ok, _ := ogenjson.Equal(a, b); ok {
+					loc := loc.Index(i)
+					err := errors.Errorf("duplicate enum value: %q", a)
+					return nil, p.wrapLocation(p.file(ctx), loc, err)
 				}
 			}
+		}
 
-			values, err := parseEnumValues(s, enum)
+		values, err := parseEnumValues(s, enum)
+		if err != nil {
+			err := errors.Wrap(err, "parse enum values")
+			return nil, p.wrapLocation(p.file(ctx), loc, err)
+		}
+		s.Enum = values
+		handleNullableEnum(s)
+	}
+	if d := schema.Default; len(d) > 0 {
+		if err := func() error {
+			v, err := parseJSONValue(s, json.RawMessage(d))
 			if err != nil {
-				err := errors.Wrap(err, "parse enum values")
-				return nil, p.wrapLocation(p.file(ctx), loc, err)
+				return err
 			}
-			s.Enum = values
-			handleNullableEnum(s)
-		}
-		if d := schema.Default; len(d) > 0 {
-			if err := func() error {
-				v, err := parseJSONValue(s, json.RawMessage(d))
-				if err != nil {
-					return err
-				}
-
-				if v == nil && !s.Nullable {
-					return errors.New("unexpected default \"null\" value")
-				}
 
-				s.Default = v
-				s.DefaultSet = true
-				return nil
-			}(); err != nil {
-				err := errors.Wrap(err, "parse default")
-				return nil, p.wrapField("default", p.file(ctx), schema.Common.Locator, err)
+			if v == nil && !s.Nullable {
+				return errors.New("unexpected default \"null\" value")
 			}
+
+			s.Default = v
+			s.DefaultSet = true
+			return nil
+		}(); err != nil {
+			err := errors.Wrap(err, "parse default")
+			return nil, p.wrapField("default", p.file(ctx), schema.Common.Locator, err)
 		}
-		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")
-			}
+	}
+	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")
 		}
 	}
 
@@ -133,6 +144,9 @@ func (p *Parser) parseSchema(schema *RawSchema, ctx *jsonpointer.ResolveCtx, hoo
 		return nil, nil
 	}
 	wrapField := func(field string, err error) error {
+		if err != nil {
+			err = errors.Wrap(err, field)
+		}
 		return p.wrapField(field, p.file(ctx), schema.Common.Locator, err)
 	}
 
@@ -469,12 +483,6 @@ func (p *Parser) extendInfo(schema *RawSchema, s *Schema, file location.File) *S
 	if len(s.Enum) < 1 {
 		s.Nullable = schema.Nullable
 	}
-	if d := schema.Discriminator; d != nil {
-		s.Discriminator = &Discriminator{
-			PropertyName: d.PropertyName,
-			Mapping:      d.Mapping,
-		}
-	}
 	if x := schema.XML; x != nil {
 		s.XML = &XML{
 			Name:      x.Name,
@@ -488,3 +496,46 @@ func (p *Parser) extendInfo(schema *RawSchema, s *Schema, file location.File) *S
 	s.Pointer = schema.Common.Locator.Pointer(file)
 	return s
 }
+
+func (p *Parser) parseDiscriminator(d *RawDiscriminator, ctx *jsonpointer.ResolveCtx) (_ *Discriminator, rerr error) {
+	locator := d.Common.Locator
+	defer func() {
+		rerr = p.wrapLocation(ctx.File(), locator, rerr)
+	}()
+
+	mapping := make(map[string]*Schema, len(d.Mapping))
+	for value, ref := range d.Mapping {
+		locator := locator.Field("mapping").Field(value)
+
+		// See https://github.com/OAI/OpenAPI-Specification/issues/2520#issuecomment-1139961158.
+		var (
+			s   *Schema
+			err error
+		)
+		switch {
+		case !strings.ContainsRune(ref, '#') && ctx.IsRoot():
+			// JSON Reference usually contains a fragment, e.g. "#/components/schemas/Foo" or
+			// "foo.json#/definitions/Bar", but this looks like a schema name.
+			//
+			// Try to find it in the components, if it is root spec.
+			if s, err = p.resolve("#/components/schemas/"+ref, ctx); err == nil {
+				break
+			}
+			// It seems there is no schema with such name, try to resolve as a plain JSON Reference.
+			fallthrough
+		default:
+			s, err = p.resolve(ref, ctx)
+		}
+		if err != nil {
+			err = errors.Wrap(err, "resolve mapping")
+			return nil, p.wrapLocation(ctx.File(), locator, err)
+		}
+		mapping[value] = s
+	}
+
+	return &Discriminator{
+		PropertyName: d.PropertyName,
+		Mapping:      mapping,
+		Pointer:      locator.Pointer(ctx.File()),
+	}, nil
+}
diff --git a/jsonschema/raw_schema.go b/jsonschema/raw_schema.go
index 564f92c4..4a1d361c 100644
--- a/jsonschema/raw_schema.go
+++ b/jsonschema/raw_schema.go
@@ -35,17 +35,17 @@ type RawSchema struct {
 	ContentEncoding      string                `json:"contentEncoding,omitempty" yaml:"contentEncoding,omitempty"`
 	ContentMediaType     string                `json:"contentMediaType,omitempty" yaml:"contentMediaType,omitempty"`
 
-	Discriminator *Discriminator `json:"discriminator,omitempty" yaml:"discriminator,omitempty"`
-	XML           *XML           `json:"xml,omitempty" yaml:"xml,omitempty"`
-	Example       Example        `json:"example,omitempty" yaml:"example,omitempty"`
+	Discriminator *RawDiscriminator `json:"discriminator,omitempty" yaml:"discriminator,omitempty"`
+	XML           *XML              `json:"xml,omitempty" yaml:"xml,omitempty"`
+	Example       Example           `json:"example,omitempty" yaml:"example,omitempty"`
 
 	Common OpenAPICommon `json:"-" yaml:",inline"`
 }
 
-// Discriminator discriminates types for OneOf, AllOf, AnyOf.
+// RawDiscriminator discriminates types for OneOf, AllOf, AnyOf.
 //
 // See https://spec.openapis.org/oas/v3.1.0#discriminator-object.
-type Discriminator struct {
+type RawDiscriminator struct {
 	// REQUIRED. The name of the property in the payload that will hold the discriminator value.
 	PropertyName string `json:"propertyName" yaml:"propertyName"`
 	// An object to hold mappings between payload values and schema names or references.
diff --git a/jsonschema/resolve.go b/jsonschema/resolve.go
index 2ac49d5b..c1841ec8 100644
--- a/jsonschema/resolve.go
+++ b/jsonschema/resolve.go
@@ -55,7 +55,13 @@ func (p *Parser) getResolver(loc string) (r resolver, rerr error) {
 	return r, nil
 }
 
-func (p *Parser) resolve(ref string, ctx *jsonpointer.ResolveCtx) (*Schema, error) {
+func (p *Parser) resolve(ref string, ctx *jsonpointer.ResolveCtx) (_ *Schema, rerr error) {
+	defer func() {
+		if rerr != nil {
+			rerr = errors.Wrapf(rerr, "resolve %q", ref)
+		}
+	}()
+
 	key, err := ctx.Key(ref)
 	if err != nil {
 		return nil, err
diff --git a/jsonschema/schema.go b/jsonschema/schema.go
index bc53355f..6daf938e 100644
--- a/jsonschema/schema.go
+++ b/jsonschema/schema.go
@@ -106,3 +106,11 @@ type PatternProperty struct {
 	Pattern ogenregex.Regexp
 	Schema  *Schema
 }
+
+// Discriminator discriminates types for OneOf, AllOf, AnyOf.
+type Discriminator struct {
+	PropertyName string
+	Mapping      map[string]*Schema
+
+	location.Pointer `json:"-" yaml:"-"`
+}
diff --git a/openapi/parser/parse_schema.go b/openapi/parser/parse_schema.go
index 3bb07fdc..f85a0fca 100644
--- a/openapi/parser/parse_schema.go
+++ b/openapi/parser/parse_schema.go
@@ -1,11 +1,17 @@
 package parser
 
 import (
+	"github.com/go-faster/errors"
+
 	"github.com/ogen-go/ogen"
 	"github.com/ogen-go/ogen/internal/jsonpointer"
 	"github.com/ogen-go/ogen/jsonschema"
 )
 
 func (p *parser) parseSchema(schema *ogen.Schema, ctx *jsonpointer.ResolveCtx) (*jsonschema.Schema, error) {
-	return p.schemaParser.ParseWithContext(schema.ToJSONSchema(), ctx)
+	s, err := p.schemaParser.ParseWithContext(schema.ToJSONSchema(), ctx)
+	if err != nil {
+		return nil, errors.Wrap(err, "parse schema")
+	}
+	return s, nil
 }
diff --git a/schema_backcomp.go b/schema_backcomp.go
index 860ecf7e..7a93e213 100644
--- a/schema_backcomp.go
+++ b/schema_backcomp.go
@@ -103,15 +103,16 @@ func (p PatternProperties) ToJSONSchema() (result jsonschema.RawPatternPropertie
 	return result
 }
 
-// ToJSONSchema converts Discriminator to jsonschema.Discriminator.
-func (d *Discriminator) ToJSONSchema() *jsonschema.Discriminator {
+// ToJSONSchema converts Discriminator to jsonschema.RawDiscriminator.
+func (d *Discriminator) ToJSONSchema() *jsonschema.RawDiscriminator {
 	if d == nil {
 		return nil
 	}
 
-	return &jsonschema.Discriminator{
+	return &jsonschema.RawDiscriminator{
 		PropertyName: d.PropertyName,
 		Mapping:      d.Mapping,
+		Common:       d.Common,
 	}
 }
 


Test patch:
diff --git a/jsonschema/_testdata/negative/discriminator/unknown_ref.json b/jsonschema/_testdata/negative/discriminator/unknown_ref.json
new file mode 100644
index 00000000..1885ff85
--- /dev/null
+++ b/jsonschema/_testdata/negative/discriminator/unknown_ref.json
@@ -0,0 +1,34 @@
+{
+  "$schema": "http://json-schema.org/draft-04/schema#",
+  "oneOf": [
+    {
+      "type": "object",
+      "properties": {
+        "_type": {
+          "type": "string"
+        },
+        "foo": {
+          "type": "string"
+        }
+      }
+    },
+    {
+      "type": "object",
+      "properties": {
+        "_type": {
+          "type": "string"
+        },
+        "bar": {
+          "type": "string"
+        }
+      }
+    }
+  ],
+  "discriminator": {
+    "propertyName": "_type",
+    "mapping": {
+      "foo": "#/foo",
+      "bar": "#/bar"
+    }
+  }
+}
diff --git a/jsonschema/_testdata/negative/discriminator/unknown_schema_name.json b/jsonschema/_testdata/negative/discriminator/unknown_schema_name.json
new file mode 100644
index 00000000..ec39aab0
--- /dev/null
+++ b/jsonschema/_testdata/negative/discriminator/unknown_schema_name.json
@@ -0,0 +1,34 @@
+{
+  "$schema": "http://json-schema.org/draft-04/schema#",
+  "oneOf": [
+    {
+      "type": "object",
+      "properties": {
+        "_type": {
+          "type": "string"
+        },
+        "foo": {
+          "type": "string"
+        }
+      }
+    },
+    {
+      "type": "object",
+      "properties": {
+        "_type": {
+          "type": "string"
+        },
+        "bar": {
+          "type": "string"
+        }
+      }
+    }
+  ],
+  "discriminator": {
+    "propertyName": "_type",
+    "mapping": {
+      "foo": "Foo",
+      "bar": "Bar"
+    }
+  }
+}
diff --git a/openapi/parser/parse_schema_test.go b/openapi/parser/parse_schema_test.go
new file mode 100644
index 00000000..c93c439e
--- /dev/null
+++ b/openapi/parser/parse_schema_test.go
@@ -0,0 +1,135 @@
+package parser
+
+import (
+	"testing"
+
+	"github.com/stretchr/testify/require"
+
+	"github.com/ogen-go/ogen"
+)
+
+func TestParseDiscriminator(t *testing.T) {
+	root := &ogen.Spec{
+		OpenAPI: "3.0.3",
+		Paths: map[string]*ogen.PathItem{
+			"/get": {
+				Get: &ogen.Operation{
+					OperationID: "testGet",
+					Description: "operation description",
+					Responses: map[string]*ogen.Response{
+						"201": {
+							Content: map[string]ogen.Media{
+								"application/json": {
+									Schema: &ogen.Schema{Ref: "#/components/schemas/TestPet"},
+								},
+							},
+						},
+					},
+				},
+			},
+		},
+		Components: &ogen.Components{
+			Schemas: map[string]*ogen.Schema{
+				"TestPet": {
+					Properties: ogen.Properties{
+						{
+							Name:   "PetRef",
+							Schema: &ogen.Schema{Ref: "#/components/schemas/PetRef"},
+						},
+						{
+							Name:   "PetSchemaName",
+							Schema: &ogen.Schema{Ref: "#/components/schemas/PetSchemaName"},
+						},
+						{
+							Name:   "PetImplicit",
+							Schema: &ogen.Schema{Ref: "#/components/schemas/PetImplicit"},
+						},
+					},
+				},
+				"PetRef": {
+					OneOf: []*ogen.Schema{
+						{Ref: "#/components/schemas/Cat"},
+						{Ref: "#/components/schemas/Dog"},
+						{Ref: "#/components/schemas/Cow"},
+					},
+					Discriminator: &ogen.Discriminator{
+						PropertyName: "petType",
+						Mapping: map[string]string{
+							"cat": "#/components/schemas/Cat",
+							"dog": "#/components/schemas/Dog",
+							"cow": "#/components/schemas/Cow",
+						},
+					},
+				},
+				"PetSchemaName": {
+					OneOf: []*ogen.Schema{
+						{Ref: "#/components/schemas/Cat"},
+						{Ref: "#/components/schemas/Dog"},
+						{Ref: "#/components/schemas/Cow"},
+					},
+					Discriminator: &ogen.Discriminator{
+						PropertyName: "petType",
+						Mapping: map[string]string{
+							"cat": "Cat",
+							"dog": "Dog",
+							"cow": "Cow",
+						},
+					},
+				},
+				"PetImplicit": {
+					OneOf: []*ogen.Schema{
+						{Ref: "#/components/schemas/Cat"},
+						{Ref: "#/components/schemas/Dog"},
+						{Ref: "#/components/schemas/Cow"},
+					},
+					Discriminator: &ogen.Discriminator{
+						PropertyName: "petType",
+					},
+				},
+				"Cat": {
+					Type:     "object",
+					Required: []string{"petType", "meow"},
+					Properties: ogen.Properties{
+						{Name: "petType", Schema: &ogen.Schema{Type: "string"}},
+						{Name: "meow", Schema: &ogen.Schema{Type: "string"}},
+					},
+				},
+				"Dog": {
+					Type:     "object",
+					Required: []string{"petType", "bark"},
+					Properties: ogen.Properties{
+						{Name: "petType", Schema: &ogen.Schema{Type: "string"}},
+						{Name: "bark", Schema: &ogen.Schema{Type: "string"}},
+					},
+				},
+				"Cow": {
+					Type:     "object",
+					Required: []string{"petType", "moo"},
+					Properties: ogen.Properties{
+						{Name: "petType", Schema: &ogen.Schema{Type: "string"}},
+						{Name: "moo", Schema: &ogen.Schema{Type: "string"}},
+					},
+				},
+			},
+		},
+	}
+
+	a := require.New(t)
+
+	spec, err := Parse(root, Settings{})
+	a.NoError(err)
+	{
+		s := spec.Components.Schemas["PetRef"]
+		m := s.Discriminator.Mapping
+		a.Equal(m["cat"].Ref, "#/components/schemas/Cat")
+		a.Equal(m["dog"].Ref, "#/components/schemas/Dog")
+		a.Equal(m["cow"].Ref, "#/components/schemas/Cow")
+	}
+	{
+		s := spec.Components.Schemas["PetSchemaName"]
+		m := s.Discriminator.Mapping
+		a.Equal(m["cat"].Ref, "#/components/schemas/Cat")
+		a.Equal(m["dog"].Ref, "#/components/schemas/Dog")
+		a.Equal(m["cow"].Ref, "#/components/schemas/Cow")
+	}
+}