Repository: getkin/kin-openapi
Pull request #1027 (2024-10-30 11:38:06)

openapi3: fail to load spec because of schema names in mapping 
This PR consists of a unit test demonstrating a problem with the latest mapping PR: https://github.com/getkin/kin-openapi/pull/1022
The problem is due to the new logic not handling schema names anymore.
The example in the unit test is taken from here: https://spec.openapis.org/oas/v3.0.0#fixed-fields-20
The table in this page states that mapping is "An object to hold mappings between payload values and **schema names** or references."


Patch:
diff --git a/.github/docs/openapi3.txt b/.github/docs/openapi3.txt
index b63a994..a1631df 100644
--- a/.github/docs/openapi3.txt
+++ b/.github/docs/openapi3.txt
@@ -411,8 +411,8 @@ type Discriminator struct {
 	Extensions map[string]any `json:"-" yaml:"-"`
 	Origin     *Origin        `json:"origin,omitempty" yaml:"origin,omitempty"`
 
-	PropertyName string                `json:"propertyName" yaml:"propertyName"` // required
-	Mapping      StringMap[MappingRef] `json:"mapping,omitempty" yaml:"mapping,omitempty"`
+	PropertyName string    `json:"propertyName" yaml:"propertyName"` // required
+	Mapping      StringMap `json:"mapping,omitempty" yaml:"mapping,omitempty"`
 }
     Discriminator is specified by OpenAPI/Swagger standard version 3. See
     https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.0.3.md#discriminator-object
@@ -831,15 +831,6 @@ type Location struct {
 }
     Location is a struct that contains the location of a field.
 
-type MappingRef SchemaRef
-    MappingRef is a ref to a Schema objects. Unlike SchemaRefs it is serialised
-    as a plain string instead of an object with a $ref key, as such it also does
-    not support extensions.
-
-func (mr MappingRef) MarshalText() ([]byte, error)
-
-func (mr *MappingRef) UnmarshalText(data []byte) error
-
 type MediaType struct {
 	Extensions map[string]any `json:"-" yaml:"-"`
 	Origin     *Origin        `json:"origin,omitempty" yaml:"origin,omitempty"`
@@ -922,10 +913,10 @@ type OAuthFlow struct {
 	Extensions map[string]any `json:"-" yaml:"-"`
 	Origin     *Origin        `json:"origin,omitempty" yaml:"origin,omitempty"`
 
-	AuthorizationURL string            `json:"authorizationUrl,omitempty" yaml:"authorizationUrl,omitempty"`
-	TokenURL         string            `json:"tokenUrl,omitempty" yaml:"tokenUrl,omitempty"`
-	RefreshURL       string            `json:"refreshUrl,omitempty" yaml:"refreshUrl,omitempty"`
-	Scopes           StringMap[string] `json:"scopes" yaml:"scopes"` // required
+	AuthorizationURL string    `json:"authorizationUrl,omitempty" yaml:"authorizationUrl,omitempty"`
+	TokenURL         string    `json:"tokenUrl,omitempty" yaml:"tokenUrl,omitempty"`
+	RefreshURL       string    `json:"refreshUrl,omitempty" yaml:"refreshUrl,omitempty"`
+	Scopes           StringMap `json:"scopes" yaml:"scopes"` // required
 }
     OAuthFlow is specified by OpenAPI/Swagger standard version 3. See
     https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.0.3.md#oauth-flow-object
@@ -2076,11 +2067,11 @@ func NewRegexpFormatValidator(pattern string) StringFormatValidator
     NewRegexpFormatValidator creates a new FormatValidator that uses a regular
     expression to validate the value.
 
-type StringMap[V any] map[string]V
+type StringMap map[string]string
     StringMap is a map[string]string that ignores the origin in the underlying
     json representation.
 
-func (stringMap *StringMap[V]) UnmarshalJSON(data []byte) (err error)
+func (stringMap *StringMap) UnmarshalJSON(data []byte) (err error)
     UnmarshalJSON sets StringMap to a copy of data.
 
 type T struct {
diff --git a/openapi3/discriminator.go b/openapi3/discriminator.go
index 8d0d942..a8ab07b 100644
--- a/openapi3/discriminator.go
+++ b/openapi3/discriminator.go
@@ -11,22 +11,8 @@ type Discriminator struct {
 	Extensions map[string]any `json:"-" yaml:"-"`
 	Origin     *Origin        `json:"origin,omitempty" yaml:"origin,omitempty"`
 
-	PropertyName string                `json:"propertyName" yaml:"propertyName"` // required
-	Mapping      StringMap[MappingRef] `json:"mapping,omitempty" yaml:"mapping,omitempty"`
-}
-
-// MappingRef is a ref to a Schema objects. Unlike SchemaRefs it is serialised
-// as a plain string instead of an object with a $ref key, as such it also does
-// not support extensions.
-type MappingRef SchemaRef
-
-func (mr *MappingRef) UnmarshalText(data []byte) error {
-	mr.Ref = string(data)
-	return nil
-}
-
-func (mr MappingRef) MarshalText() ([]byte, error) {
-	return []byte(mr.Ref), nil
+	PropertyName string    `json:"propertyName" yaml:"propertyName"` // required
+	Mapping      StringMap `json:"mapping,omitempty" yaml:"mapping,omitempty"`
 }
 
 // MarshalJSON returns the JSON encoding of Discriminator.
diff --git a/openapi3/internalize_refs.go b/openapi3/internalize_refs.go
index da33dac..01f5dad 100644
--- a/openapi3/internalize_refs.go
+++ b/openapi3/internalize_refs.go
@@ -351,16 +351,6 @@ func (doc *T) derefSchema(s *Schema, refNameResolver RefNameResolver, parentIsEx
 			}
 		}
 	}
-	// Discriminator mapping values are special cases since they are not full
-	// ref objects but are string references to schema objects.
-	if s.Discriminator != nil && s.Discriminator.Mapping != nil {
-		for k, mapRef := range s.Discriminator.Mapping {
-			s2 := (*SchemaRef)(&mapRef)
-			isExternal := doc.addSchemaToSpec(s2, refNameResolver, parentIsExternal)
-			doc.derefSchema(s2.Value, refNameResolver, isExternal || parentIsExternal)
-			s.Discriminator.Mapping[k] = mapRef
-		}
-	}
 
 	for _, name := range componentNames(s.Properties) {
 		s2 := s.Properties[name]
diff --git a/openapi3/loader.go b/openapi3/loader.go
index 4a82886..0f3b8cb 100644
--- a/openapi3/loader.go
+++ b/openapi3/loader.go
@@ -953,16 +953,6 @@ func (loader *Loader) resolveSchemaRef(doc *T, component *SchemaRef, documentPat
 			return err
 		}
 	}
-	// Discriminator mapping refs are a special case since they are not full
-	// ref objects but are plain strings that reference schema objects.
-	if value.Discriminator != nil && value.Discriminator.Mapping != nil {
-		for k, v := range value.Discriminator.Mapping {
-			if err := loader.resolveSchemaRef(doc, (*SchemaRef)(&v), documentPath, visited); err != nil {
-				return err
-			}
-			value.Discriminator.Mapping[k] = v
-		}
-	}
 	return nil
 }
 
diff --git a/openapi3/schema.go b/openapi3/schema.go
index c1c7d23..10a00ea 100644
--- a/openapi3/schema.go
+++ b/openapi3/schema.go
@@ -1298,7 +1298,7 @@ func (schema *Schema) visitNotOperation(settings *schemaValidationSettings, valu
 func (schema *Schema) visitXOFOperations(settings *schemaValidationSettings, value any) (err error, run bool) {
 	var visitedOneOf, visitedAnyOf, visitedAllOf bool
 	if v := schema.OneOf; len(v) > 0 {
-		var discriminatorRef MappingRef
+		var discriminatorRef string
 		if schema.Discriminator != nil {
 			pn := schema.Discriminator.PropertyName
 			if valuemap, okcheck := value.(map[string]any); okcheck {
@@ -1344,7 +1344,7 @@ func (schema *Schema) visitXOFOperations(settings *schemaValidationSettings, val
 				return foundUnresolvedRef(item.Ref), false
 			}
 
-			if discriminatorRef.Ref != "" && discriminatorRef.Ref != item.Ref {
+			if discriminatorRef != "" && discriminatorRef != item.Ref {
 				continue
 			}
 
diff --git a/openapi3/security_scheme.go b/openapi3/security_scheme.go
index 21674df..7bc889d 100644
--- a/openapi3/security_scheme.go
+++ b/openapi3/security_scheme.go
@@ -322,10 +322,10 @@ type OAuthFlow struct {
 	Extensions map[string]any `json:"-" yaml:"-"`
 	Origin     *Origin        `json:"origin,omitempty" yaml:"origin,omitempty"`
 
-	AuthorizationURL string            `json:"authorizationUrl,omitempty" yaml:"authorizationUrl,omitempty"`
-	TokenURL         string            `json:"tokenUrl,omitempty" yaml:"tokenUrl,omitempty"`
-	RefreshURL       string            `json:"refreshUrl,omitempty" yaml:"refreshUrl,omitempty"`
-	Scopes           StringMap[string] `json:"scopes" yaml:"scopes"` // required
+	AuthorizationURL string    `json:"authorizationUrl,omitempty" yaml:"authorizationUrl,omitempty"`
+	TokenURL         string    `json:"tokenUrl,omitempty" yaml:"tokenUrl,omitempty"`
+	RefreshURL       string    `json:"refreshUrl,omitempty" yaml:"refreshUrl,omitempty"`
+	Scopes           StringMap `json:"scopes" yaml:"scopes"` // required
 }
 
 // MarshalJSON returns the JSON encoding of OAuthFlow.
diff --git a/openapi3/stringmap.go b/openapi3/stringmap.go
index b1987c0..354a4c8 100644
--- a/openapi3/stringmap.go
+++ b/openapi3/stringmap.go
@@ -3,11 +3,11 @@ package openapi3
 import "encoding/json"
 
 // StringMap is a map[string]string that ignores the origin in the underlying json representation.
-type StringMap[V any] map[string]V
+type StringMap map[string]string
 
 // UnmarshalJSON sets StringMap to a copy of data.
-func (stringMap *StringMap[V]) UnmarshalJSON(data []byte) (err error) {
-	*stringMap, _, err = unmarshalStringMap[V](data)
+func (stringMap *StringMap) UnmarshalJSON(data []byte) (err error) {
+	*stringMap, _, err = unmarshalStringMap[string](data)
 	return
 }
 
diff --git a/openapi3filter/req_resp_decoder.go b/openapi3filter/req_resp_decoder.go
index e151c9e..19e3609 100644
--- a/openapi3filter/req_resp_decoder.go
+++ b/openapi3filter/req_resp_decoder.go
@@ -1264,6 +1264,9 @@ func decodeBody(body io.Reader, header http.Header, schema *openapi3.SchemaRef,
 func init() {
 	RegisterBodyDecoder("application/json", JSONBodyDecoder)
 	RegisterBodyDecoder("application/json-patch+json", JSONBodyDecoder)
+	RegisterBodyDecoder("application/ld+json", JSONBodyDecoder)
+	RegisterBodyDecoder("application/hal+json", JSONBodyDecoder)
+	RegisterBodyDecoder("application/vnd.api+json", JSONBodyDecoder)
 	RegisterBodyDecoder("application/octet-stream", FileBodyDecoder)
 	RegisterBodyDecoder("application/problem+json", JSONBodyDecoder)
 	RegisterBodyDecoder("application/x-www-form-urlencoded", urlencodedBodyDecoder)


Test patch:
diff --git a/openapi3/internalize_refs_test.go b/openapi3/internalize_refs_test.go
index 967ebca..6e9853a 100644
--- a/openapi3/internalize_refs_test.go
+++ b/openapi3/internalize_refs_test.go
@@ -25,7 +25,6 @@ func TestInternalizeRefs(t *testing.T) {
 		{"testdata/issue831/testref.internalizepath.openapi.yml"},
 		{"testdata/issue959/openapi.yml"},
 		{"testdata/interalizationNameCollision/api.yml"},
-		{"testdata/discriminator.yml"},
 	}
 
 	for _, test := range tests {
diff --git a/openapi3/mapping_test.go b/openapi3/mapping_test.go
new file mode 100644
index 0000000..0c1c199
--- /dev/null
+++ b/openapi3/mapping_test.go
@@ -0,0 +1,58 @@
+package openapi3
+
+import (
+	"testing"
+
+	"github.com/stretchr/testify/require"
+)
+
+func TestMapping(t *testing.T) {
+
+	schema := `
+openapi: 3.0.0
+info:
+  title: ACME
+  version: 1.0.0
+components:
+  schemas:
+    Pet:
+      type: object
+      required:
+      - petType
+      properties:
+        petType:
+          type: string
+      discriminator:
+        propertyName: petType
+        mapping:
+          dog: Dog
+    Cat:
+      allOf:
+      - $ref: '#/components/schemas/Pet'
+      - type: object
+        # all other properties specific to a Cat
+        properties:
+          name:
+            type: string
+    Dog:
+      allOf:
+      - $ref: '#/components/schemas/Pet'
+      - type: object
+        # all other properties specific to a Dog
+        properties:
+          bark:
+            type: string
+    Lizard:
+      allOf:
+      - $ref: '#/components/schemas/Pet'
+      - type: object
+        # all other properties specific to a Lizard
+        properties:
+          lovesRocks:
+            type: boolean
+`
+	loader := NewLoader()
+	loader.IsExternalRefsAllowed = true
+	_, err := loader.LoadFromData([]byte(schema))
+	require.NoError(t, err)
+}
diff --git a/openapi3/testdata/discriminator.yml b/openapi3/testdata/discriminator.yml
deleted file mode 100644
index f9ff884..0000000
--- a/openapi3/testdata/discriminator.yml
+++ /dev/null
@@ -1,24 +0,0 @@
-openapi: 3.1.0
-info:
-  title: foo
-  version: 1.0.0
-paths:
-  /:
-    get:
-      operationId: list
-      responses:
-        "200":
-          description: list
-          content:
-            application/json:
-              schema:
-                type: array
-                items:
-                  oneOf:
-                    - $ref: "./ext.yml#/schemas/Foo"
-                    - $ref: "./ext.yml#/schemas/Bar"
-                  discriminator:
-                    propertyName: cat
-                    mapping:
-                      foo: "./ext.yml#/schemas/Foo"
-                      bar: "./ext.yml#/schemas/Bar"
diff --git a/openapi3/testdata/discriminator.yml.internalized.yml b/openapi3/testdata/discriminator.yml.internalized.yml
deleted file mode 100644
index ecd6e4e..0000000
--- a/openapi3/testdata/discriminator.yml.internalized.yml
+++ /dev/null
@@ -1,75 +0,0 @@
-{
-    "openapi": "3.1.0",
-    "info": {
-        "title": "foo",
-        "version": "1.0.0"
-    },
-    "paths": {
-        "/": {
-            "get": {
-                "operationId": "list",
-                "responses": {
-                    "200": {
-                        "description": "list",
-                        "content": {
-                            "application/json": {
-                                "schema": {
-                                    "type": "array",
-                                    "items": {
-                                        "oneOf": [
-                                            {
-                                                "$ref": "#/components/schemas/ext_schemas_Foo"
-                                            },
-                                            {
-                                                "$ref": "#/components/schemas/ext_schemas_Bar"
-                                            }
-                                        ],
-                                        "discriminator": {
-                                            "propertyName": "cat",
-                                            "mapping": {
-                                                "foo": "#/components/schemas/ext_schemas_Foo",
-                                                "bar": "#/components/schemas/ext_schemas_Bar"
-                                            }
-                                        }
-                                    }
-                                }
-                            }
-                        }
-                    }
-                }
-            }
-        }
-    },
-    "components": {
-        "schemas": {
-            "ext_schemas_Foo": {
-                "type": "object",
-                "properties": {
-                    "cat": {
-                        "type": "string",
-                        "enum": [
-                            "foo"
-                        ]
-                    },
-                    "name": {
-                        "type": "string"
-                    }
-                }
-            },
-            "ext_schemas_Bar": {
-                "type": "object",
-                "properties": {
-                    "cat": {
-                        "type": "string",
-                        "enum": [
-                            "bar"
-                        ]
-                    },
-                    "other": {
-                        "type": "string"
-                    }
-                }
-            }
-        }
-    }
-}
diff --git a/openapi3/testdata/ext.yml b/openapi3/testdata/ext.yml
deleted file mode 100644
index 21a7a55..0000000
--- a/openapi3/testdata/ext.yml
+++ /dev/null
@@ -1,17 +0,0 @@
-schemas:
-  Foo:
-    type: object
-    properties:
-      cat:
-        type: string
-        enum: [ "foo" ]
-      name:
-        type: string
-  Bar:
-    type: object
-    properties:
-      cat:
-        type: string
-        enum: [ "bar" ]
-      other:
-        type: string