Repository: getkin/kin-openapi
Pull request #693 (2022-12-04 22:43:26)

Try decoding as JSON first then YAML, for speed
Fixes https://github.com/getkin/kin-openapi/issues/680

fenollp: @balogal please take a look :)

Patch:
diff --git a/.github/workflows/go.yml b/.github/workflows/go.yml
index d6950eb..8094763 100644
--- a/.github/workflows/go.yml
+++ b/.github/workflows/go.yml
@@ -97,6 +97,11 @@ jobs:
       run: |
         ! grep -IErn '\s$' --exclude-dir={.git,target,pgdata}
 
+    - if: runner.os == 'Linux'
+      name: Ensure use of unmarshal
+      run: |
+        [[ "$(git grep -F yaml. -- openapi3/ | grep -v _test.go | wc -l)" = 1 ]]
+
     - if: runner.os == 'Linux'
       name: Missing specification object link to definition
       run: |
diff --git a/openapi3/loader.go b/openapi3/loader.go
index 138431f..3979e5d 100644
--- a/openapi3/loader.go
+++ b/openapi3/loader.go
@@ -115,7 +115,7 @@ func (loader *Loader) loadSingleElementFromURI(ref string, rootPath *url.URL, el
 	if err != nil {
 		return nil, err
 	}
-	if err := yaml.Unmarshal(data, element); err != nil {
+	if err := unmarshal(data, element); err != nil {
 		return nil, err
 	}
 
@@ -133,7 +133,7 @@ func (loader *Loader) readURL(location *url.URL) ([]byte, error) {
 func (loader *Loader) LoadFromData(data []byte) (*T, error) {
 	loader.resetVisitedPathItemRefs()
 	doc := &T{}
-	if err := yaml.Unmarshal(data, doc); err != nil {
+	if err := unmarshal(data, doc); err != nil {
 		return nil, err
 	}
 	if err := loader.ResolveRefsIn(doc, nil); err != nil {
@@ -162,7 +162,7 @@ func (loader *Loader) loadFromDataWithPathInternal(data []byte, location *url.UR
 	doc := &T{}
 	loader.visitedDocuments[uri] = doc
 
-	if err := yaml.Unmarshal(data, doc); err != nil {
+	if err := unmarshal(data, doc); err != nil {
 		return nil, err
 	}
 	if err := loader.ResolveRefsIn(doc, location); err != nil {
@@ -172,6 +172,14 @@ 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 {
+		return yaml.Unmarshal(data, v)
+	}
+	return nil
+}
+
 // ResolveRefsIn expands references if for instance spec was just unmarshalled
 func (loader *Loader) ResolveRefsIn(doc *T, location *url.URL) (err error) {
 	if loader.Context == nil {
@@ -319,7 +327,7 @@ func (loader *Loader) resolveComponent(
 		if err2 != nil {
 			return nil, err
 		}
-		if err2 = yaml.Unmarshal(data, &cursor); err2 != nil {
+		if err2 = unmarshal(data, &cursor); err2 != nil {
 			return nil, err
 		}
 		if cursor, err2 = drill(cursor); err2 != nil || cursor == nil {
diff --git a/openapi3/schema.go b/openapi3/schema.go
index 9f874d9..67cc7dc 100644
--- a/openapi3/schema.go
+++ b/openapi3/schema.go
@@ -934,10 +934,6 @@ func (schema *Schema) visitSetOperations(settings *schemaValidationSettings, val
 			matchedOneOfIdx  = 0
 			tempValue        = value
 		)
-		// make a deep copy to protect origin value from being injected default value that defined in mismatched oneOf schema
-		if settings.asreq || settings.asrep {
-			tempValue = deepcopy.Copy(value)
-		}
 		for idx, item := range v {
 			v := item.Value
 			if v == nil {
@@ -948,6 +944,11 @@ func (schema *Schema) visitSetOperations(settings *schemaValidationSettings, val
 				continue
 			}
 
+			// make a deep copy to protect origin value from being injected default value that defined in mismatched oneOf schema
+			if settings.asreq || settings.asrep {
+				tempValue = deepcopy.Copy(value)
+			}
+
 			if err := v.visitJSON(settings, tempValue); err != nil {
 				validationErrors = append(validationErrors, err)
 				continue
@@ -990,15 +991,15 @@ func (schema *Schema) visitSetOperations(settings *schemaValidationSettings, val
 			matchedAnyOfIdx = 0
 			tempValue       = value
 		)
-		// make a deep copy to protect origin value from being injected default value that defined in mismatched anyOf schema
-		if settings.asreq || settings.asrep {
-			tempValue = deepcopy.Copy(value)
-		}
 		for idx, item := range v {
 			v := item.Value
 			if v == nil {
 				return foundUnresolvedRef(item.Ref)
 			}
+			// make a deep copy to protect origin value from being injected default value that defined in mismatched anyOf schema
+			if settings.asreq || settings.asrep {
+				tempValue = deepcopy.Copy(value)
+			}
 			if err := v.visitJSON(settings, tempValue); err == nil {
 				ok = true
 				matchedAnyOfIdx = idx


Test patch:
diff --git a/openapi3filter/validate_set_default_test.go b/openapi3filter/validate_set_default_test.go
index 4550b51..731cbbd 100644
--- a/openapi3filter/validate_set_default_test.go
+++ b/openapi3filter/validate_set_default_test.go
@@ -317,6 +317,70 @@ func TestValidateRequestBodyAndSetDefault(t *testing.T) {
                         }
                       }
                     ]
+                  },
+                  "contact": {
+                    "oneOf": [
+                      {
+                        "type": "object",
+                        "required": ["email"],
+                        "properties": {
+                          "email": {
+                            "type": "string"
+                          },
+                          "allow_image": {
+                            "type": "boolean",
+                            "default": true
+                          }
+                        },
+                        "additionalProperties": false
+                      },
+                      {
+                        "type": "object",
+                        "required": ["phone"],
+                        "properties": {
+                          "phone": {
+                            "type": "string"
+                          },
+                          "allow_text": {
+                            "type": "boolean",
+                            "default": false
+                          }
+                        },
+                        "additionalProperties": false
+                      }
+                    ]
+                  },
+                  "contact2": {
+                    "anyOf": [
+                      {
+                        "type": "object",
+                        "required": ["email"],
+                        "properties": {
+                          "email": {
+                            "type": "string"
+                          },
+                          "allow_image": {
+                            "type": "boolean",
+                            "default": true
+                          }
+                        },
+                        "additionalProperties": false
+                      },
+                      {
+                        "type": "object",
+                        "required": ["phone"],
+                        "properties": {
+                          "phone": {
+                            "type": "string"
+                          },
+                          "allow_text": {
+                            "type": "boolean",
+                            "default": false
+                          }
+                        },
+                        "additionalProperties": false
+                      }
+                    ]
                   }
                 }
               }
@@ -358,6 +422,10 @@ func TestValidateRequestBodyAndSetDefault(t *testing.T) {
 		FBLink   string `json:"fb_link,omitempty"`
 		TWLink   string `json:"tw_link,omitempty"`
 	}
+	type contact struct {
+		Email string `json:"email,omitempty"`
+		Phone string `json:"phone,omitempty"`
+	}
 	type body struct {
 		ID             string         `json:"id,omitempty"`
 		Name           string         `json:"name,omitempty"`
@@ -367,6 +435,8 @@ func TestValidateRequestBodyAndSetDefault(t *testing.T) {
 		Filters        []filter       `json:"filters,omitempty"`
 		SocialNetwork  *socialNetwork `json:"social_network,omitempty"`
 		SocialNetwork2 *socialNetwork `json:"social_network_2,omitempty"`
+		Contact        *contact       `json:"contact,omitempty"`
+		Contact2       *contact       `json:"contact2,omitempty"`
 	}
 
 	testCases := []struct {
@@ -656,6 +726,52 @@ func TestValidateRequestBodyAndSetDefault(t *testing.T) {
     "platform": "facebook",
     "fb_link": "www.facebook.com"
   }
+}
+        `, body)
+			},
+		},
+		{
+			name: "contact(oneOf)",
+			body: body{
+				ID: "bt6kdc3d0cvp6u8u3ft0",
+				Contact: &contact{
+					Phone: "123456",
+				},
+			},
+			bodyAssertion: func(t *testing.T, body string) {
+				require.JSONEq(t, `
+{
+  "id": "bt6kdc3d0cvp6u8u3ft0",
+  "name": "default",
+  "code": 123,
+  "all": false,
+  "contact": {
+    "phone": "123456",
+    "allow_text": false
+  }
+}
+        `, body)
+			},
+		},
+		{
+			name: "contact(anyOf)",
+			body: body{
+				ID: "bt6kdc3d0cvp6u8u3ft0",
+				Contact2: &contact{
+					Phone: "123456",
+				},
+			},
+			bodyAssertion: func(t *testing.T, body string) {
+				require.JSONEq(t, `
+{
+  "id": "bt6kdc3d0cvp6u8u3ft0",
+  "name": "default",
+  "code": 123,
+  "all": false,
+  "contact2": {
+    "phone": "123456",
+    "allow_text": false
+  }
 }
         `, body)
 			},