Repository: swaggest/rest
Pull request #91 (2022-10-19 14:28:52)

Tolerate form-data for request bodies with json tags
This PR fixes behavior of handling structures that have both `formData` and `json` field tags.

codecov[bot]: # [Codecov](https://codecov.io/gh/swaggest/rest/pull/91?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=swaggest) Report
> Merging [#91](https://codecov.io/gh/swaggest/rest/pull/91?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=swaggest) (1e5229f) into [master](https://codecov.io/gh/swaggest/rest/commit/7f12e41b72c618ed5b7541693991aa03c0addffc?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=swaggest) (7f12e41) will **decrease** coverage by `0.11%`.
> The diff coverage is `66.66%`.

```diff
@@            Coverage Diff             @@
##           master      #91      +/-   ##
==========================================
- Coverage   77.52%   77.40%   -0.12%     
==========================================
  Files          27       27              
  Lines        1428     1434       +6     
==========================================
+ Hits         1107     1110       +3     
- Misses        206      208       +2     
- Partials      115      116       +1     
```

| Flag | Coverage Δ | |
|---|---|---|
| unittests | `77.40% <66.66%> (-0.12%)` | :arrow_down: |

Flags with carried forward coverage won't be shown. [Click here](https://docs.codecov.io/docs/carryforward-flags?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=swaggest#carryforward-flags-in-the-pull-request-comment) to find out more.

| [Impacted Files](https://codecov.io/gh/swaggest/rest/pull/91?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=swaggest) | Coverage Δ | |
|---|---|---|
| [request/factory.go](https://codecov.io/gh/swaggest/rest/pull/91/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=swaggest#diff-cmVxdWVzdC9mYWN0b3J5Lmdv) | `86.72% <66.66%> (+0.11%)` | :arrow_up: |
| [request/jsonbody.go](https://codecov.io/gh/swaggest/rest/pull/91/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=swaggest#diff-cmVxdWVzdC9qc29uYm9keS5nbw==) | `91.42% <66.66%> (-8.58%)` | :arrow_down: |

:mega: We’re building smart automated test selection to slash your CI/CD build times. [Learn more](https://about.codecov.io/iterative-testing/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=swaggest)


Patch:
diff --git a/request/factory.go b/request/factory.go
index 3e78143..ca5d6bc 100644
--- a/request/factory.go
+++ b/request/factory.go
@@ -130,13 +130,15 @@ func (df *DecoderFactory) MakeDecoder(
 		return &m
 	}
 
+	hasFormData := refl.HasTaggedFields(input, formDataTag)
+
 	// Checking for body tags.
 	if refl.HasTaggedFields(input, jsonTag) || refl.FindEmbeddedSliceOrMap(input) != nil ||
 		refl.IsSliceOrMap(input) || refl.IsScalar(input) {
 		if df.JSONReader != nil {
-			m.decoders = append(m.decoders, decodeJSONBody(df.JSONReader))
+			m.decoders = append(m.decoders, decodeJSONBody(df.JSONReader, hasFormData))
 		} else {
-			m.decoders = append(m.decoders, decodeJSONBody(readJSON))
+			m.decoders = append(m.decoders, decodeJSONBody(readJSON, hasFormData))
 		}
 
 		m.in = append(m.in, rest.ParamInBody)
diff --git a/request/jsonbody.go b/request/jsonbody.go
index 2669ea5..cf44c11 100644
--- a/request/jsonbody.go
+++ b/request/jsonbody.go
@@ -23,17 +23,16 @@ func readJSON(rd io.Reader, v interface{}) error {
 	return d.Decode(v)
 }
 
-func decodeJSONBody(readJSON func(rd io.Reader, v interface{}) error) valueDecoderFunc {
+func decodeJSONBody(readJSON func(rd io.Reader, v interface{}) error, tolerateFormData bool) valueDecoderFunc {
 	return func(r *http.Request, input interface{}, validator rest.Validator) error {
 		if r.ContentLength == 0 {
 			return ErrMissingRequestBody
 		}
 
-		contentType := r.Header.Get("Content-Type")
-		if contentType != "" {
-			if len(contentType) < 16 || contentType[0:16] != "application/json" { // allow 'application/json;charset=UTF-8'
-				return fmt.Errorf("%w, received: %s", ErrJSONExpected, contentType)
-			}
+		if ret, err := checkJSONBodyContentType(r.Header.Get("Content-Type"), tolerateFormData); err != nil {
+			return err
+		} else if ret {
+			return nil
 		}
 
 		var (
@@ -66,3 +65,19 @@ func decodeJSONBody(readJSON func(rd io.Reader, v interface{}) error) valueDecod
 		return nil
 	}
 }
+
+func checkJSONBodyContentType(contentType string, tolerateFormData bool) (ret bool, err error) {
+	if contentType == "" {
+		return false, nil
+	}
+
+	if len(contentType) < 16 || contentType[0:16] != "application/json" { // allow 'application/json;charset=UTF-8'
+		if tolerateFormData && (contentType == "application/x-www-form-urlencoded" || contentType == "multipart/form-data") {
+			return true, nil
+		}
+
+		return true, fmt.Errorf("%w, received: %s", ErrJSONExpected, contentType)
+	}
+
+	return false, nil
+}


Test patch:
diff --git a/request/jsonbody_test.go b/request/jsonbody_test.go
index a293e61..c8f7aab 100644
--- a/request/jsonbody_test.go
+++ b/request/jsonbody_test.go
@@ -25,7 +25,7 @@ func Test_decodeJSONBody(t *testing.T) {
 	}
 
 	i := Input{}
-	assert.NoError(t, decodeJSONBody(readJSON)(createReq, &i, nil))
+	assert.NoError(t, decodeJSONBody(readJSON, false)(createReq, &i, nil))
 	assert.Equal(t, 123, i.Amount)
 	assert.Equal(t, "248df4b7-aa70-47b8-a036-33ac447e668d", i.CustomerID)
 	assert.Equal(t, "withdraw", i.Type)
@@ -37,7 +37,7 @@ func Test_decodeJSONBody(t *testing.T) {
 	i = Input{}
 	_, err = createBody.Seek(0, io.SeekStart)
 	assert.NoError(t, err)
-	assert.NoError(t, decodeJSONBody(readJSON)(createReq, &i, vl))
+	assert.NoError(t, decodeJSONBody(readJSON, false)(createReq, &i, vl))
 	assert.Equal(t, 123, i.Amount)
 	assert.Equal(t, "248df4b7-aa70-47b8-a036-33ac447e668d", i.CustomerID)
 	assert.Equal(t, "withdraw", i.Type)
@@ -49,7 +49,7 @@ func Test_decodeJSONBody_emptyBody(t *testing.T) {
 
 	var i []int
 
-	err = decodeJSONBody(readJSON)(req, &i, nil)
+	err = decodeJSONBody(readJSON, false)(req, &i, nil)
 	assert.EqualError(t, err, "missing request body")
 }
 
@@ -60,7 +60,7 @@ func Test_decodeJSONBody_badContentType(t *testing.T) {
 
 	var i []int
 
-	err = decodeJSONBody(readJSON)(req, &i, nil)
+	err = decodeJSONBody(readJSON, false)(req, &i, nil)
 	assert.EqualError(t, err, "request with application/json content type expected, received: text/plain")
 }
 
@@ -70,7 +70,7 @@ func Test_decodeJSONBody_decodeFailed(t *testing.T) {
 
 	var i []int
 
-	err = decodeJSONBody(readJSON)(req, &i, nil)
+	err = decodeJSONBody(readJSON, false)(req, &i, nil)
 	assert.Error(t, err)
 }
 
@@ -80,7 +80,7 @@ func Test_decodeJSONBody_unmarshalFailed(t *testing.T) {
 
 	var i []int
 
-	err = decodeJSONBody(readJSON)(req, &i, nil)
+	err = decodeJSONBody(readJSON, false)(req, &i, nil)
 	assert.EqualError(t, err, "failed to decode json: json: cannot unmarshal number into Go value of type []int")
 }
 
@@ -94,6 +94,26 @@ func Test_decodeJSONBody_validateFailed(t *testing.T) {
 		return errors.New("failed")
 	})
 
-	err = decodeJSONBody(readJSON)(req, &i, vl)
+	err = decodeJSONBody(readJSON, false)(req, &i, vl)
 	assert.EqualError(t, err, "failed")
 }
+
+func Test_decodeJSONBody_tolerateFormData(t *testing.T) {
+	createBody := bytes.NewReader(
+		[]byte(`amount=123&customerId=248df4b7-aa70-47b8-a036-33ac447e668d&type=withdraw`))
+	createReq, err := http.NewRequest(http.MethodPost, "/US/order/348df4b7-aa70-47b8-a036-33ac447e668d", createBody)
+	createReq.Header.Set("Content-Type", "application/x-www-form-urlencoded")
+	assert.NoError(t, err)
+
+	type Input struct {
+		Amount     int    `json:"amount" formData:"amount"`
+		CustomerID string `json:"customerId" formData:"customerId"`
+		Type       string `json:"type" formData:"type"`
+	}
+
+	i := Input{}
+	assert.NoError(t, decodeJSONBody(readJSON, true)(createReq, &i, nil))
+	assert.Empty(t, i.Amount)
+	assert.Empty(t, i.CustomerID)
+	assert.Empty(t, i.Type)
+}