Repository: swaggest/openapi-go
Pull request #135 (2025-02-14 22:49:47)

Add contentType field tag for bodies

github-actions[bot]: ### Lines Of Code

| Language  | Files |   Lines    |    Code    | Comments |  Blanks   | Complexity |     Bytes      |
|-----------|-------|------------|------------|----------|-----------|------------|----------------|
| Go        | 21    | 4086 (+54) | 3033 (+42) | 227      | 826 (+12) | 827 (+8)   | 94.2K (+1.5K)  |
| Go (test) | 22    | 6639 (+94) | 5100 (+80) | 660      | 879 (+14) | 61         | 170.3K (+1.8K) |
| Markdown  | 4     | 241 (+1)   | 210 (+1)   | 0        | 31        | 0          | 6.8K (+48B)    |
<!-- Sticky Pull Request CommentLOC -->
github-actions[bot]: ### Go API Changes

<pre>
# summary
Inferred base version: v0.2.54
Suggested version: v0.2.55
</pre>
<!-- Sticky Pull Request Commentgorelease -->
github-actions[bot]: ### Unit Test Coverage
total:									(statements)				47.5%
changed lines: (statements) 91.7%
<details><summary>Coverage of changed lines</summary>

|           File           |       Function       | Coverage |
|--------------------------|----------------------|----------|
| Total                    |                      | 91.7%    |
| openapi3/reflect.go      |                      | 91.7%    |
| openapi3/reflect.go:271  | setupRequest         | 100.0%   |
| openapi3/reflect.go:634  | parseRawResponseBody | 100.0%   |
| openapi3/reflect.go:646  | setupResponse        | 100.0%   |
| openapi3/reflect.go:351  | parseRawRequestBody  | 80.0%    |
| openapi31/reflect.go     |                      | 91.7%    |
| openapi31/reflect.go:218 | setupRequest         | 100.0%   |
| openapi31/reflect.go:585 | parseRawResponseBody | 100.0%   |
| openapi31/reflect.go:597 | setupResponse        | 100.0%   |
| openapi31/reflect.go:298 | parseRawRequestBody  | 80.0%    |

</details>

<details><summary>Coverage diff with base branch</summary>

|         File         |       Function       | Base Coverage | Current Coverage |
|----------------------|----------------------|---------------|------------------|
| Total                |                      | 47.4%         | 47.5% (+0.1%)    |
| openapi3/reflect.go  | parseRawRequestBody  | no function   | 80.0%            |
| openapi3/reflect.go  | parseRawResponseBody | no function   | 100.0%           |
| openapi31/reflect.go | parseRawRequestBody  | no function   | 80.0%            |
| openapi31/reflect.go | parseRawResponseBody | no function   | 100.0%           |

</details>
<!-- Sticky Pull Request Commentunit-test -->

Patch:
diff --git a/README.md b/README.md
index 3a64351..23d334c 100644
--- a/README.md
+++ b/README.md
@@ -22,6 +22,7 @@ For automated HTTP REST service framework built with this library please check [
     * `query`, `path` for parameters in URL
     * `header`, `cookie`, `formData`, `file` for other parameters
     * `form` acts as `query` and `formData`
+    * `contentType` indicates body content type
     * [field tags](https://github.com/swaggest/jsonschema-go#field-tags) named after JSON Schema/OpenAPI 3 Schema constraints
     * `collectionFormat` to unpack slices from string
         * `csv` comma-separated values,
diff --git a/openapi3/reflect.go b/openapi3/reflect.go
index c55c268..c422c74 100644
--- a/openapi3/reflect.go
+++ b/openapi3/reflect.go
@@ -279,6 +279,8 @@ func (r *Reflector) setupRequest(o *Operation, oc openapi.OperationContext) erro
 			); err != nil {
 				return err
 			}
+
+			r.parseRawRequestBody(o, cu)
 		case mimeJSON:
 			if err := joinErrors(
 				r.parseParameters(o, oc, cu),
@@ -314,6 +316,7 @@ const (
 	tagFormData        = "formData"
 	tagForm            = "form"
 	tagHeader          = "header"
+	tagContentType     = "contentType"
 	mimeJSON           = "application/json"
 	mimeFormUrlencoded = "application/x-www-form-urlencoded"
 	mimeMultipart      = "multipart/form-data"
@@ -346,6 +349,16 @@ func (r *Reflector) stringRequestBody(
 	o.RequestBodyEns().RequestBodyEns().WithContentItem(mime, mediaType(format))
 }
 
+func (r *Reflector) parseRawRequestBody(o *Operation, cu openapi.ContentUnit) {
+	if cu.Structure == nil {
+		return
+	}
+
+	refl.WalkTaggedFields(reflect.ValueOf(cu.Structure), func(_ reflect.Value, _ reflect.StructField, tag string) {
+		r.stringRequestBody(o, tag, "")
+	}, tagContentType)
+}
+
 func (r *Reflector) parseRequestBody(
 	o *Operation,
 	oc openapi.OperationContext,
@@ -617,6 +630,16 @@ func (r *Reflector) parseResponseHeader(resp *Response, oc openapi.OperationCont
 	return nil
 }
 
+func (r *Reflector) parseRawResponseBody(resp *Response, cu openapi.ContentUnit) {
+	if cu.Structure == nil {
+		return
+	}
+
+	refl.WalkTaggedFields(reflect.ValueOf(cu.Structure), func(_ reflect.Value, _ reflect.StructField, tag string) {
+		r.ensureResponseContentType(resp, tag, "")
+	}, tagContentType)
+}
+
 func (r *Reflector) setupResponse(o *Operation, oc openapi.OperationContext) error {
 	for _, cu := range oc.Response() {
 		if cu.HTTPStatus == 0 && !cu.IsDefault {
@@ -654,6 +677,8 @@ func (r *Reflector) setupResponse(o *Operation, oc openapi.OperationContext) err
 				return err
 			}
 
+			r.parseRawResponseBody(resp, cu)
+
 			if cu.ContentType != "" {
 				r.ensureResponseContentType(resp, cu.ContentType, cu.Format)
 			}
diff --git a/openapi31/reflect.go b/openapi31/reflect.go
index f37a3e1..3b95933 100644
--- a/openapi31/reflect.go
+++ b/openapi31/reflect.go
@@ -226,6 +226,8 @@ func (r *Reflector) setupRequest(o *Operation, oc openapi.OperationContext) erro
 			); err != nil {
 				return err
 			}
+
+			r.parseRawRequestBody(o, cu)
 		case mimeJSON:
 			if err := joinErrors(
 				r.parseParameters(o, oc, cu),
@@ -260,6 +262,7 @@ const (
 	tagJSON            = "json"
 	tagFormData        = "formData"
 	tagForm            = "form"
+	tagContentType     = "contentType"
 	mimeJSON           = "application/json"
 	mimeFormUrlencoded = "application/x-www-form-urlencoded"
 	mimeMultipart      = "multipart/form-data"
@@ -293,6 +296,16 @@ func (r *Reflector) stringRequestBody(
 	o.RequestBodyEns().RequestBodyEns().WithContentItem(mime, mediaType(format))
 }
 
+func (r *Reflector) parseRawRequestBody(o *Operation, cu openapi.ContentUnit) {
+	if cu.Structure == nil {
+		return
+	}
+
+	refl.WalkTaggedFields(reflect.ValueOf(cu.Structure), func(_ reflect.Value, _ reflect.StructField, tag string) {
+		r.stringRequestBody(o, tag, "")
+	}, tagContentType)
+}
+
 func (r *Reflector) parseRequestBody(
 	o *Operation,
 	oc openapi.OperationContext,
@@ -568,6 +581,16 @@ func (r *Reflector) parseResponseHeader(resp *Response, oc openapi.OperationCont
 	return nil
 }
 
+func (r *Reflector) parseRawResponseBody(resp *Response, cu openapi.ContentUnit) {
+	if cu.Structure == nil {
+		return
+	}
+
+	refl.WalkTaggedFields(reflect.ValueOf(cu.Structure), func(_ reflect.Value, _ reflect.StructField, tag string) {
+		resp.WithContentItem(tag, mediaType(""))
+	}, tagContentType)
+}
+
 func (r *Reflector) setupResponse(o *Operation, oc openapi.OperationContext) error {
 	for _, cu := range oc.Response() {
 		if cu.HTTPStatus == 0 && !cu.IsDefault {
@@ -605,6 +628,8 @@ func (r *Reflector) setupResponse(o *Operation, oc openapi.OperationContext) err
 				return err
 			}
 
+			r.parseRawResponseBody(resp, cu)
+
 			if cu.ContentType != "" {
 				r.ensureResponseContentType(resp, cu.ContentType, cu.Format)
 			}


Test patch:
diff --git a/openapi3/reflect_test.go b/openapi3/reflect_test.go
index 2c8de30..7d21156 100644
--- a/openapi3/reflect_test.go
+++ b/openapi3/reflect_test.go
@@ -1355,3 +1355,50 @@ func TestWithCustomize(t *testing.T) {
 	  }
 	}`, r.SpecSchema())
 }
+
+func TestRawBody(t *testing.T) {
+	r := openapi3.NewReflector()
+
+	oc, err := r.NewOperationContext(http.MethodPost, "/")
+	require.NoError(t, err)
+
+	type req struct {
+		TextBody string `contentType:"text/plain"`
+		CSVBody  string `contentType:"text/csv"`
+	}
+
+	type resp struct {
+		TextBody string `contentType:"text/plain"`
+		CSVBody  string `contentType:"text/csv"`
+	}
+
+	oc.AddReqStructure(req{})
+	oc.AddRespStructure(resp{})
+
+	require.NoError(t, r.AddOperation(oc))
+
+	assertjson.EqMarshal(t, `{
+	  "openapi":"3.0.3","info":{"title":"","version":""},
+	  "paths":{
+		"/":{
+		  "post":{
+			"requestBody":{
+			  "content":{
+				"text/csv":{"schema":{"type":"string"}},
+				"text/plain":{"schema":{"type":"string"}}
+			  }
+			},
+			"responses":{
+			  "200":{
+				"description":"OK",
+				"content":{
+				  "text/csv":{"schema":{"type":"string"}},
+				  "text/plain":{"schema":{"type":"string"}}
+				}
+			  }
+			}
+		  }
+		}
+	  }
+	}`, r.SpecSchema())
+}
diff --git a/openapi31/reflect_test.go b/openapi31/reflect_test.go
index b3f3217..ff69e90 100644
--- a/openapi31/reflect_test.go
+++ b/openapi31/reflect_test.go
@@ -1483,3 +1483,50 @@ func TestWithCustomize(t *testing.T) {
 	  }
 	}`, r.SpecSchema())
 }
+
+func TestRawBody(t *testing.T) {
+	r := openapi31.NewReflector()
+
+	oc, err := r.NewOperationContext(http.MethodPost, "/")
+	require.NoError(t, err)
+
+	type req struct {
+		TextBody string `contentType:"text/plain"`
+		CSVBody  string `contentType:"text/csv"`
+	}
+
+	type resp struct {
+		TextBody string `contentType:"text/plain"`
+		CSVBody  string `contentType:"text/csv"`
+	}
+
+	oc.AddReqStructure(req{})
+	oc.AddRespStructure(resp{})
+
+	require.NoError(t, r.AddOperation(oc))
+
+	assertjson.EqMarshal(t, `{
+	  "openapi":"3.1.0","info":{"title":"","version":""},
+	  "paths":{
+		"/":{
+		  "post":{
+			"requestBody":{
+			  "content":{
+				"text/csv":{"schema":{"type":"string"}},
+				"text/plain":{"schema":{"type":"string"}}
+			  }
+			},
+			"responses":{
+			  "200":{
+				"description":"OK",
+				"content":{
+				  "text/csv":{"schema":{"type":"string"}},
+				  "text/plain":{"schema":{"type":"string"}}
+				}
+			  }
+			}
+		  }
+		}
+	  }
+	}`, r.SpecSchema())
+}