Repository: getkin/kin-openapi
Pull request #870 (2023-11-26 15:34:05)

openapi{2,3}: simplify unmarshal errors


Patch:
diff --git a/openapi2/marsh.go b/openapi2/marsh.go
new file mode 100644
index 0000000..6a9df46
--- /dev/null
+++ b/openapi2/marsh.go
@@ -0,0 +1,26 @@
+package openapi2
+
+import (
+	"encoding/json"
+	"fmt"
+	"strings"
+
+	"github.com/invopop/yaml"
+)
+
+func unmarshalError(jsonUnmarshalErr error) error {
+	if before, after, found := strings.Cut(jsonUnmarshalErr.Error(), "Bis."); found && before != "" && after != "" {
+		before = strings.ReplaceAll(before, " Go struct ", " ")
+		return fmt.Errorf("%s.%s", before, after)
+	}
+	return jsonUnmarshalErr
+}
+
+func unmarshal(data []byte, v interface{}) error {
+	// See https://github.com/getkin/kin-openapi/issues/680
+	if err := json.Unmarshal(data, v); err != nil {
+		// UnmarshalStrict(data, v) TODO: investigate how ymlv3 handles duplicate map keys
+		return yaml.Unmarshal(data, v)
+	}
+	return nil
+}
diff --git a/openapi2/openapi2.go b/openapi2/openapi2.go
index 69f9094..03a72cb 100644
--- a/openapi2/openapi2.go
+++ b/openapi2/openapi2.go
@@ -82,7 +82,7 @@ func (doc *T) UnmarshalJSON(data []byte) error {
 	type TBis T
 	var x TBis
 	if err := json.Unmarshal(data, &x); err != nil {
-		return err
+		return unmarshalError(err)
 	}
 	_ = json.Unmarshal(data, &x.Extensions)
 	delete(x.Extensions, "swagger")
diff --git a/openapi2/operation.go b/openapi2/operation.go
index 0bc3db6..e50f21e 100644
--- a/openapi2/operation.go
+++ b/openapi2/operation.go
@@ -71,7 +71,7 @@ func (operation *Operation) UnmarshalJSON(data []byte) error {
 	type OperationBis Operation
 	var x OperationBis
 	if err := json.Unmarshal(data, &x); err != nil {
-		return err
+		return unmarshalError(err)
 	}
 	_ = json.Unmarshal(data, &x.Extensions)
 	delete(x.Extensions, "summary")
diff --git a/openapi2/parameter.go b/openapi2/parameter.go
index 36bc299..0255098 100644
--- a/openapi2/parameter.go
+++ b/openapi2/parameter.go
@@ -142,7 +142,7 @@ func (parameter *Parameter) UnmarshalJSON(data []byte) error {
 	type ParameterBis Parameter
 	var x ParameterBis
 	if err := json.Unmarshal(data, &x); err != nil {
-		return err
+		return unmarshalError(err)
 	}
 	_ = json.Unmarshal(data, &x.Extensions)
 	delete(x.Extensions, "$ref")
diff --git a/openapi2/path_item.go b/openapi2/path_item.go
index 15be872..a365f0c 100644
--- a/openapi2/path_item.go
+++ b/openapi2/path_item.go
@@ -65,7 +65,7 @@ func (pathItem *PathItem) UnmarshalJSON(data []byte) error {
 	type PathItemBis PathItem
 	var x PathItemBis
 	if err := json.Unmarshal(data, &x); err != nil {
-		return err
+		return unmarshalError(err)
 	}
 	_ = json.Unmarshal(data, &x.Extensions)
 	delete(x.Extensions, "$ref")
diff --git a/openapi2/response.go b/openapi2/response.go
index e6b22e1..b4aa7fe 100644
--- a/openapi2/response.go
+++ b/openapi2/response.go
@@ -47,7 +47,7 @@ func (response *Response) UnmarshalJSON(data []byte) error {
 	type ResponseBis Response
 	var x ResponseBis
 	if err := json.Unmarshal(data, &x); err != nil {
-		return err
+		return unmarshalError(err)
 	}
 	_ = json.Unmarshal(data, &x.Extensions)
 	delete(x.Extensions, "$ref")
diff --git a/openapi2/security_scheme.go b/openapi2/security_scheme.go
index 6ce9bee..d81d8e7 100644
--- a/openapi2/security_scheme.go
+++ b/openapi2/security_scheme.go
@@ -69,7 +69,7 @@ func (securityScheme *SecurityScheme) UnmarshalJSON(data []byte) error {
 	type SecuritySchemeBis SecurityScheme
 	var x SecuritySchemeBis
 	if err := json.Unmarshal(data, &x); err != nil {
-		return err
+		return unmarshalError(err)
 	}
 	_ = json.Unmarshal(data, &x.Extensions)
 	delete(x.Extensions, "$ref")
diff --git a/openapi3/components.go b/openapi3/components.go
index 0678ca7..a645b77 100644
--- a/openapi3/components.go
+++ b/openapi3/components.go
@@ -68,7 +68,7 @@ func (components *Components) UnmarshalJSON(data []byte) error {
 	type ComponentsBis Components
 	var x ComponentsBis
 	if err := json.Unmarshal(data, &x); err != nil {
-		return err
+		return unmarshalError(err)
 	}
 	_ = json.Unmarshal(data, &x.Extensions)
 	delete(x.Extensions, "schemas")
diff --git a/openapi3/contact.go b/openapi3/contact.go
index a025803..e60d281 100644
--- a/openapi3/contact.go
+++ b/openapi3/contact.go
@@ -38,7 +38,7 @@ func (contact *Contact) UnmarshalJSON(data []byte) error {
 	type ContactBis Contact
 	var x ContactBis
 	if err := json.Unmarshal(data, &x); err != nil {
-		return err
+		return unmarshalError(err)
 	}
 	_ = json.Unmarshal(data, &x.Extensions)
 	delete(x.Extensions, "name")
diff --git a/openapi3/discriminator.go b/openapi3/discriminator.go
index d45e28b..abb4807 100644
--- a/openapi3/discriminator.go
+++ b/openapi3/discriminator.go
@@ -32,7 +32,7 @@ func (discriminator *Discriminator) UnmarshalJSON(data []byte) error {
 	type DiscriminatorBis Discriminator
 	var x DiscriminatorBis
 	if err := json.Unmarshal(data, &x); err != nil {
-		return err
+		return unmarshalError(err)
 	}
 	_ = json.Unmarshal(data, &x.Extensions)
 	delete(x.Extensions, "propertyName")
diff --git a/openapi3/encoding.go b/openapi3/encoding.go
index 96c08ea..8e81027 100644
--- a/openapi3/encoding.go
+++ b/openapi3/encoding.go
@@ -68,7 +68,7 @@ func (encoding *Encoding) UnmarshalJSON(data []byte) error {
 	type EncodingBis Encoding
 	var x EncodingBis
 	if err := json.Unmarshal(data, &x); err != nil {
-		return err
+		return unmarshalError(err)
 	}
 	_ = json.Unmarshal(data, &x.Extensions)
 	delete(x.Extensions, "contentType")
diff --git a/openapi3/example.go b/openapi3/example.go
index dc9c46a..67039a6 100644
--- a/openapi3/example.go
+++ b/openapi3/example.go
@@ -67,7 +67,7 @@ func (example *Example) UnmarshalJSON(data []byte) error {
 	type ExampleBis Example
 	var x ExampleBis
 	if err := json.Unmarshal(data, &x); err != nil {
-		return err
+		return unmarshalError(err)
 	}
 	_ = json.Unmarshal(data, &x.Extensions)
 	delete(x.Extensions, "summary")
diff --git a/openapi3/external_docs.go b/openapi3/external_docs.go
index 318892d..7190be4 100644
--- a/openapi3/external_docs.go
+++ b/openapi3/external_docs.go
@@ -37,7 +37,7 @@ func (e *ExternalDocs) UnmarshalJSON(data []byte) error {
 	type ExternalDocsBis ExternalDocs
 	var x ExternalDocsBis
 	if err := json.Unmarshal(data, &x); err != nil {
-		return err
+		return unmarshalError(err)
 	}
 	_ = json.Unmarshal(data, &x.Extensions)
 	delete(x.Extensions, "description")
diff --git a/openapi3/info.go b/openapi3/info.go
index bb3003b..ffcd3b0 100644
--- a/openapi3/info.go
+++ b/openapi3/info.go
@@ -47,7 +47,7 @@ func (info *Info) UnmarshalJSON(data []byte) error {
 	type InfoBis Info
 	var x InfoBis
 	if err := json.Unmarshal(data, &x); err != nil {
-		return err
+		return unmarshalError(err)
 	}
 	_ = json.Unmarshal(data, &x.Extensions)
 	delete(x.Extensions, "title")
diff --git a/openapi3/license.go b/openapi3/license.go
index 32f7587..3d2d2f0 100644
--- a/openapi3/license.go
+++ b/openapi3/license.go
@@ -33,7 +33,7 @@ func (license *License) UnmarshalJSON(data []byte) error {
 	type LicenseBis License
 	var x LicenseBis
 	if err := json.Unmarshal(data, &x); err != nil {
-		return err
+		return unmarshalError(err)
 	}
 	_ = json.Unmarshal(data, &x.Extensions)
 	delete(x.Extensions, "name")
diff --git a/openapi3/link.go b/openapi3/link.go
index c77e238..394afa8 100644
--- a/openapi3/link.go
+++ b/openapi3/link.go
@@ -73,7 +73,7 @@ func (link *Link) UnmarshalJSON(data []byte) error {
 	type LinkBis Link
 	var x LinkBis
 	if err := json.Unmarshal(data, &x); err != nil {
-		return err
+		return unmarshalError(err)
 	}
 	_ = json.Unmarshal(data, &x.Extensions)
 	delete(x.Extensions, "operationRef")
diff --git a/openapi3/loader.go b/openapi3/loader.go
index ffa17e8..7d05954 100644
--- a/openapi3/loader.go
+++ b/openapi3/loader.go
@@ -14,8 +14,6 @@ import (
 	"sort"
 	"strconv"
 	"strings"
-
-	"github.com/invopop/yaml"
 )
 
 var CircularReferenceError = "kin-openapi bug found: circular schema reference not handled"
@@ -178,15 +176,6 @@ func (loader *Loader) loadFromDataWithPathInternal(data []byte, location *url.UR
 	return doc, nil
 }
 
-func unmarshal(data []byte, v interface{}) error {
-	// See https://github.com/getkin/kin-openapi/issues/680
-	if err := json.Unmarshal(data, v); err != nil {
-		// UnmarshalStrict(data, v) TODO: investigate how ymlv3 handles duplicate map keys
-		return yaml.Unmarshal(data, v)
-	}
-	return nil
-}
-
 // ResolveRefsIn expands references if for instance spec was just unmarshaled
 func (loader *Loader) ResolveRefsIn(doc *T, location *url.URL) (err error) {
 	if loader.Context == nil {
diff --git a/openapi3/marsh.go b/openapi3/marsh.go
new file mode 100644
index 0000000..5ef69ae
--- /dev/null
+++ b/openapi3/marsh.go
@@ -0,0 +1,26 @@
+package openapi3
+
+import (
+	"encoding/json"
+	"fmt"
+	"strings"
+
+	"github.com/invopop/yaml"
+)
+
+func unmarshalError(jsonUnmarshalErr error) error {
+	if before, after, found := strings.Cut(jsonUnmarshalErr.Error(), "Bis."); found && before != "" && after != "" {
+		before = strings.ReplaceAll(before, " Go struct ", " ")
+		return fmt.Errorf("%s.%s", before, after)
+	}
+	return jsonUnmarshalErr
+}
+
+func unmarshal(data []byte, v interface{}) error {
+	// See https://github.com/getkin/kin-openapi/issues/680
+	if err := json.Unmarshal(data, v); err != nil {
+		// UnmarshalStrict(data, v) TODO: investigate how ymlv3 handles duplicate map keys
+		return yaml.Unmarshal(data, v)
+	}
+	return nil
+}
diff --git a/openapi3/media_type.go b/openapi3/media_type.go
index 88e61d5..e043a7c 100644
--- a/openapi3/media_type.go
+++ b/openapi3/media_type.go
@@ -89,7 +89,7 @@ func (mediaType *MediaType) UnmarshalJSON(data []byte) error {
 	type MediaTypeBis MediaType
 	var x MediaTypeBis
 	if err := json.Unmarshal(data, &x); err != nil {
-		return err
+		return unmarshalError(err)
 	}
 	_ = json.Unmarshal(data, &x.Extensions)
 	delete(x.Extensions, "schema")
diff --git a/openapi3/openapi3.go b/openapi3/openapi3.go
index d0d99bd..52125b1 100644
--- a/openapi3/openapi3.go
+++ b/openapi3/openapi3.go
@@ -56,7 +56,7 @@ func (doc *T) UnmarshalJSON(data []byte) error {
 	type TBis T
 	var x TBis
 	if err := json.Unmarshal(data, &x); err != nil {
-		return err
+		return unmarshalError(err)
 	}
 	_ = json.Unmarshal(data, &x.Extensions)
 	delete(x.Extensions, "openapi")
diff --git a/openapi3/operation.go b/openapi3/operation.go
index 99c6456..02df2d2 100644
--- a/openapi3/operation.go
+++ b/openapi3/operation.go
@@ -104,7 +104,7 @@ func (operation *Operation) UnmarshalJSON(data []byte) error {
 	type OperationBis Operation
 	var x OperationBis
 	if err := json.Unmarshal(data, &x); err != nil {
-		return err
+		return unmarshalError(err)
 	}
 	_ = json.Unmarshal(data, &x.Extensions)
 	delete(x.Extensions, "tags")
diff --git a/openapi3/parameter.go b/openapi3/parameter.go
index c85fa09..8a75469 100644
--- a/openapi3/parameter.go
+++ b/openapi3/parameter.go
@@ -222,7 +222,7 @@ func (parameter *Parameter) UnmarshalJSON(data []byte) error {
 	type ParameterBis Parameter
 	var x ParameterBis
 	if err := json.Unmarshal(data, &x); err != nil {
-		return err
+		return unmarshalError(err)
 	}
 	_ = json.Unmarshal(data, &x.Extensions)
 
diff --git a/openapi3/path_item.go b/openapi3/path_item.go
index c111228..e5dd0fb 100644
--- a/openapi3/path_item.go
+++ b/openapi3/path_item.go
@@ -86,7 +86,7 @@ func (pathItem *PathItem) UnmarshalJSON(data []byte) error {
 	type PathItemBis PathItem
 	var x PathItemBis
 	if err := json.Unmarshal(data, &x); err != nil {
-		return err
+		return unmarshalError(err)
 	}
 	_ = json.Unmarshal(data, &x.Extensions)
 	delete(x.Extensions, "$ref")
diff --git a/openapi3/request_body.go b/openapi3/request_body.go
index 6ab99b2..8f5c0d6 100644
--- a/openapi3/request_body.go
+++ b/openapi3/request_body.go
@@ -116,7 +116,7 @@ func (requestBody *RequestBody) UnmarshalJSON(data []byte) error {
 	type RequestBodyBis RequestBody
 	var x RequestBodyBis
 	if err := json.Unmarshal(data, &x); err != nil {
-		return err
+		return unmarshalError(err)
 	}
 	_ = json.Unmarshal(data, &x.Extensions)
 	delete(x.Extensions, "description")
diff --git a/openapi3/response.go b/openapi3/response.go
index 4707c6b..61d859e 100644
--- a/openapi3/response.go
+++ b/openapi3/response.go
@@ -149,7 +149,7 @@ func (response *Response) UnmarshalJSON(data []byte) error {
 	type ResponseBis Response
 	var x ResponseBis
 	if err := json.Unmarshal(data, &x); err != nil {
-		return err
+		return unmarshalError(err)
 	}
 	_ = json.Unmarshal(data, &x.Extensions)
 	delete(x.Extensions, "description")
diff --git a/openapi3/schema.go b/openapi3/schema.go
index f941a11..07f5696 100644
--- a/openapi3/schema.go
+++ b/openapi3/schema.go
@@ -178,7 +178,7 @@ func (addProps AdditionalProperties) MarshalJSON() ([]byte, error) {
 func (addProps *AdditionalProperties) UnmarshalJSON(data []byte) error {
 	var x interface{}
 	if err := json.Unmarshal(data, &x); err != nil {
-		return err
+		return unmarshalError(err)
 	}
 	switch y := x.(type) {
 	case nil:
@@ -342,7 +342,7 @@ func (schema *Schema) UnmarshalJSON(data []byte) error {
 	type SchemaBis Schema
 	var x SchemaBis
 	if err := json.Unmarshal(data, &x); err != nil {
-		return err
+		return unmarshalError(err)
 	}
 	_ = json.Unmarshal(data, &x.Extensions)
 
diff --git a/openapi3/security_scheme.go b/openapi3/security_scheme.go
index fe607a1..42df94d 100644
--- a/openapi3/security_scheme.go
+++ b/openapi3/security_scheme.go
@@ -107,7 +107,7 @@ func (ss *SecurityScheme) UnmarshalJSON(data []byte) error {
 	type SecuritySchemeBis SecurityScheme
 	var x SecuritySchemeBis
 	if err := json.Unmarshal(data, &x); err != nil {
-		return err
+		return unmarshalError(err)
 	}
 	_ = json.Unmarshal(data, &x.Extensions)
 	delete(x.Extensions, "type")
@@ -268,7 +268,7 @@ func (flows *OAuthFlows) UnmarshalJSON(data []byte) error {
 	type OAuthFlowsBis OAuthFlows
 	var x OAuthFlowsBis
 	if err := json.Unmarshal(data, &x); err != nil {
-		return err
+		return unmarshalError(err)
 	}
 	_ = json.Unmarshal(data, &x.Extensions)
 	delete(x.Extensions, "implicit")
@@ -348,7 +348,7 @@ func (flow *OAuthFlow) UnmarshalJSON(data []byte) error {
 	type OAuthFlowBis OAuthFlow
 	var x OAuthFlowBis
 	if err := json.Unmarshal(data, &x); err != nil {
-		return err
+		return unmarshalError(err)
 	}
 	_ = json.Unmarshal(data, &x.Extensions)
 	delete(x.Extensions, "authorizationUrl")
diff --git a/openapi3/server.go b/openapi3/server.go
index a10e960..04e233d 100644
--- a/openapi3/server.go
+++ b/openapi3/server.go
@@ -103,7 +103,7 @@ func (server *Server) UnmarshalJSON(data []byte) error {
 	type ServerBis Server
 	var x ServerBis
 	if err := json.Unmarshal(data, &x); err != nil {
-		return err
+		return unmarshalError(err)
 	}
 	_ = json.Unmarshal(data, &x.Extensions)
 	delete(x.Extensions, "url")
@@ -255,7 +255,7 @@ func (serverVariable *ServerVariable) UnmarshalJSON(data []byte) error {
 	type ServerVariableBis ServerVariable
 	var x ServerVariableBis
 	if err := json.Unmarshal(data, &x); err != nil {
-		return err
+		return unmarshalError(err)
 	}
 	_ = json.Unmarshal(data, &x.Extensions)
 	delete(x.Extensions, "enum")
diff --git a/openapi3/tag.go b/openapi3/tag.go
index a2cd509..eea6462 100644
--- a/openapi3/tag.go
+++ b/openapi3/tag.go
@@ -63,7 +63,7 @@ func (t *Tag) UnmarshalJSON(data []byte) error {
 	type TagBis Tag
 	var x TagBis
 	if err := json.Unmarshal(data, &x); err != nil {
-		return err
+		return unmarshalError(err)
 	}
 	_ = json.Unmarshal(data, &x.Extensions)
 	delete(x.Extensions, "name")
diff --git a/openapi3/xml.go b/openapi3/xml.go
index 444117d..604b607 100644
--- a/openapi3/xml.go
+++ b/openapi3/xml.go
@@ -46,7 +46,7 @@ func (xml *XML) UnmarshalJSON(data []byte) error {
 	type XMLBis XML
 	var x XMLBis
 	if err := json.Unmarshal(data, &x); err != nil {
-		return err
+		return unmarshalError(err)
 	}
 	_ = json.Unmarshal(data, &x.Extensions)
 	delete(x.Extensions, "name")


Test patch:
diff --git a/openapi2/marsh_test.go b/openapi2/marsh_test.go
new file mode 100644
index 0000000..9c1500a
--- /dev/null
+++ b/openapi2/marsh_test.go
@@ -0,0 +1,61 @@
+package openapi2
+
+import (
+	"testing"
+
+	"github.com/stretchr/testify/require"
+)
+
+func TestUnmarshalError(t *testing.T) {
+	{
+		v2 := []byte(`
+openapi: '2.0'
+info:
+  version: '1.10'
+  title: title
+paths:
+  "/ping":
+    post:
+      consumes:
+      - multipart/form-data
+      parameters:
+        name: file  # <-- Missing dash
+        in: formData
+        description: file
+        required: true
+        type: file
+      responses:
+        '200':
+          description: OK
+`[1:])
+
+		var doc T
+		err := unmarshal(v2, &doc)
+		require.ErrorContains(t, err, `json: cannot unmarshal object into field Operation.parameters of type openapi2.Parameters`)
+	}
+
+	v2 := []byte(`
+openapi: '2.0'
+info:
+  version: '1.10'
+  title: title
+paths:
+  "/ping":
+    post:
+      consumes:
+      - multipart/form-data
+      parameters:
+      - name: file  # <--
+        in: formData
+        description: file
+        required: true
+        type: file
+      responses:
+        '200':
+          description: OK
+`[1:])
+
+	var doc T
+	err := unmarshal(v2, &doc)
+	require.NoError(t, err)
+}
diff --git a/openapi3/marsh_test.go b/openapi3/marsh_test.go
new file mode 100644
index 0000000..4ddc4fa
--- /dev/null
+++ b/openapi3/marsh_test.go
@@ -0,0 +1,78 @@
+package openapi3
+
+import (
+	"testing"
+
+	"github.com/stretchr/testify/require"
+)
+
+func TestUnmarshalError(t *testing.T) {
+	{
+		spec := []byte(`
+openapi: 3.0.1
+info:
+  version: v1
+  title: Products api
+components:
+  schemas:
+    someSchema:
+      type: object
+    schemaArray:
+      type: array
+      minItems: 1
+      items:
+        $ref: '#/components/schemas/someSchema'
+paths:
+  /categories:
+    get:
+      responses:
+        '200':
+          description: ''
+          content:
+            application/json:
+              schema:
+                allOf:
+                  $ref: '#/components/schemas/schemaArray'  # <- Should have been a list
+`[1:])
+
+		sl := NewLoader()
+
+		_, err := sl.LoadFromData(spec)
+		require.ErrorContains(t, err, `json: cannot unmarshal object into field Schema.allOf of type openapi3.SchemaRefs`)
+	}
+
+	spec := []byte(`
+openapi: 3.0.1
+info:
+  version: v1
+  title: Products api
+components:
+  schemas:
+    someSchema:
+      type: object
+    schemaArray:
+      type: array
+      minItems: 1
+      items:
+        $ref: '#/components/schemas/someSchema'
+paths:
+  /categories:
+    get:
+      responses:
+        '200':
+          description: ''
+          content:
+            application/json:
+              schema:
+                allOf:
+                - $ref: '#/components/schemas/schemaArray'  # <-
+`[1:])
+
+	sl := NewLoader()
+
+	doc, err := sl.LoadFromData(spec)
+	require.NoError(t, err)
+
+	err = doc.Validate(sl.Context)
+	require.NoError(t, err)
+}