Repository: goadesign/goa
Pull request #3561 (2024-07-18 17:55:51)

Handle Any type as a typeless schema in OpenAPI
https://github.com/goadesign/goa/issues/3560#issuecomment-2234991792

raphael: This is great thank you! Just a note that this is a potential breaking change. It is the more correct approach so seems worth merging but let's keep an eye out for feedback.

Patch:
diff --git a/http/codegen/openapi/json_schema.go b/http/codegen/openapi/json_schema.go
index 72c55675..8c431ed2 100644
--- a/http/codegen/openapi/json_schema.go
+++ b/http/codegen/openapi/json_schema.go
@@ -230,7 +230,7 @@ func ResultTypeRef(api *expr.APIExpr, mt *expr.ResultTypeExpr, view string) stri
 
 // ResultTypeRefWithPrefix produces the JSON reference to the media type definition with
 // the given view and adds the provided prefix to the type name
-func ResultTypeRefWithPrefix(api *expr.APIExpr, mt *expr.ResultTypeExpr, view string, prefix string) string {
+func ResultTypeRefWithPrefix(api *expr.APIExpr, mt *expr.ResultTypeExpr, view, prefix string) string {
 	projected, err := expr.Project(mt, view)
 	if err != nil {
 		panic(fmt.Sprintf("failed to project media type %#v: %s", mt.Identifier, err)) // bug
@@ -318,8 +318,9 @@ func TypeSchemaWithPrefix(api *expr.APIExpr, t expr.DataType, prefix string) *Sc
 		s.Type = Type(actual.Name())
 		switch actual.Kind() {
 		case expr.AnyKind:
-			s.Type = Type("string")
-			s.Format = "binary"
+			// A schema without a type matches any data type.
+			// See https://swagger.io/docs/specification/data-models/data-types/#any.
+			s.Type = Type("")
 		case expr.IntKind, expr.Int64Kind,
 			expr.UIntKind, expr.UInt64Kind:
 			// Use int64 format for IntKind and UIntKind because the OpenAPI
diff --git a/http/codegen/openapi/v3/types.go b/http/codegen/openapi/v3/types.go
index 29df754d..8a4fe51f 100644
--- a/http/codegen/openapi/v3/types.go
+++ b/http/codegen/openapi/v3/types.go
@@ -166,7 +166,7 @@ func (sf *schemafier) schemafy(attr *expr.AttributeExpr, noref ...bool) *openapi
 		case expr.Float64Kind:
 			s.Type = openapi.Type("number")
 			s.Format = "double"
-		case expr.BytesKind, expr.AnyKind:
+		case expr.BytesKind:
 			if bases := attr.Bases; len(bases) > 0 {
 				for _, b := range bases {
 					// Union type
@@ -177,6 +177,10 @@ func (sf *schemafier) schemafy(attr *expr.AttributeExpr, noref ...bool) *openapi
 				s.Type = openapi.Type("string")
 				s.Format = "binary"
 			}
+		case expr.AnyKind:
+			// A schema without a type matches any data type.
+			// See https://swagger.io/docs/specification/data-models/data-types/#any.
+			s.Type = openapi.Type("")
 		default:
 			s.Type = openapi.Type(t.Name())
 		}


Test patch:
diff --git a/http/codegen/openapi/v2/files_test.go b/http/codegen/openapi/v2/files_test.go
index f77f28c4..7e9ecab5 100644
--- a/http/codegen/openapi/v2/files_test.go
+++ b/http/codegen/openapi/v2/files_test.go
@@ -39,6 +39,7 @@ func TestSections(t *testing.T) {
 		{"server-host-with-variables", testdata.ServerHostWithVariablesDSL},
 		{"with-spaces", testdata.WithSpacesDSL},
 		{"with-map", testdata.WithMapDSL},
+		{"with-any", testdata.WithAnyDSL},
 		{"path-with-wildcards", testdata.PathWithWildcardDSL},
 		{"path-with-multiple-wildcards", testdata.PathWithMultipleWildcardDSL},
 		{"path-with-multiple-explicit-wildcards", testdata.PathWithMultipleExplicitWildcardDSL},
diff --git a/http/codegen/openapi/v2/testdata/TestSections/with-any_file0.golden b/http/codegen/openapi/v2/testdata/TestSections/with-any_file0.golden
new file mode 100644
index 00000000..d29609f3
--- /dev/null
+++ b/http/codegen/openapi/v2/testdata/TestSections/with-any_file0.golden
@@ -0,0 +1,1 @@
+{"swagger":"2.0","info":{"title":"","version":"0.0.1"},"host":"localhost:80","consumes":["application/json","application/xml","application/gob"],"produces":["application/json","application/xml","application/gob"],"paths":{"/":{"post":{"tags":["testService"],"summary":"testEndpoint testService","operationId":"testService#testEndpoint","parameters":[{"name":"TestEndpointRequestBody","in":"body","required":true,"schema":{"$ref":"#/definitions/TestServiceTestEndpointRequestBody"}}],"responses":{"200":{"description":"OK response.","schema":{"$ref":"#/definitions/TestServiceTestEndpointResponseBody"}}},"schemes":["http"]}}},"definitions":{"TestServiceTestEndpointRequestBody":{"title":"TestServiceTestEndpointRequestBody","type":"object","properties":{"any":{"example":""},"any_array":{"type":"array","items":{"example":""},"example":["","","",""]},"any_map":{"type":"object","example":{"":""},"additionalProperties":true}},"example":{"any":"","any_array":["","",""],"any_map":{"":""}}},"TestServiceTestEndpointResponseBody":{"title":"TestServiceTestEndpointResponseBody","type":"object","properties":{"any":{"example":""},"any_array":{"type":"array","items":{"example":""},"example":["","","",""]},"any_map":{"type":"object","example":{"":""},"additionalProperties":true}},"example":{"any":"","any_array":["",""],"any_map":{"":""}}}}}
\ No newline at end of file
diff --git a/http/codegen/openapi/v2/testdata/TestSections/with-any_file1.golden b/http/codegen/openapi/v2/testdata/TestSections/with-any_file1.golden
new file mode 100644
index 00000000..5ed077f4
--- /dev/null
+++ b/http/codegen/openapi/v2/testdata/TestSections/with-any_file1.golden
@@ -0,0 +1,89 @@
+swagger: "2.0"
+info:
+    title: ""
+    version: 0.0.1
+host: localhost:80
+consumes:
+    - application/json
+    - application/xml
+    - application/gob
+produces:
+    - application/json
+    - application/xml
+    - application/gob
+paths:
+    /:
+        post:
+            tags:
+                - testService
+            summary: testEndpoint testService
+            operationId: testService#testEndpoint
+            parameters:
+                - name: TestEndpointRequestBody
+                  in: body
+                  required: true
+                  schema:
+                    $ref: '#/definitions/TestServiceTestEndpointRequestBody'
+            responses:
+                "200":
+                    description: OK response.
+                    schema:
+                        $ref: '#/definitions/TestServiceTestEndpointResponseBody'
+            schemes:
+                - http
+definitions:
+    TestServiceTestEndpointRequestBody:
+        title: TestServiceTestEndpointRequestBody
+        type: object
+        properties:
+            any:
+                example: ""
+            any_array:
+                type: array
+                items:
+                    example: ""
+                example:
+                    - ""
+                    - ""
+                    - ""
+                    - ""
+            any_map:
+                type: object
+                example:
+                    "": ""
+                additionalProperties: true
+        example:
+            any: ""
+            any_array:
+                - ""
+                - ""
+                - ""
+            any_map:
+                "": ""
+    TestServiceTestEndpointResponseBody:
+        title: TestServiceTestEndpointResponseBody
+        type: object
+        properties:
+            any:
+                example: ""
+            any_array:
+                type: array
+                items:
+                    example: ""
+                example:
+                    - ""
+                    - ""
+                    - ""
+                    - ""
+            any_map:
+                type: object
+                example:
+                    "": ""
+                additionalProperties: true
+        example:
+            any: ""
+            any_array:
+                - ""
+                - ""
+            any_map:
+                "": ""
diff --git a/http/codegen/openapi/v3/files_test.go b/http/codegen/openapi/v3/files_test.go
index f68bbd3c..59350af7 100644
--- a/http/codegen/openapi/v3/files_test.go
+++ b/http/codegen/openapi/v3/files_test.go
@@ -41,6 +41,7 @@ func TestFiles(t *testing.T) {
 		{"server-host-with-variables", testdata.ServerHostWithVariablesDSL},
 		{"with-spaces", testdata.WithSpacesDSL},
 		{"with-map", testdata.WithMapDSL},
+		{"with-any", testdata.WithAnyDSL},
 		{"path-with-wildcards", testdata.PathWithWildcardDSL},
 		{"path-with-multiple-wildcards", testdata.PathWithMultipleWildcardDSL},
 		{"path-with-multiple-explicit-wildcards", testdata.PathWithMultipleExplicitWildcardDSL},
diff --git a/http/codegen/openapi/v3/testdata/dsls/types.go b/http/codegen/openapi/v3/testdata/dsls/types.go
index 54eff0f8..8e13029b 100644
--- a/http/codegen/openapi/v3/testdata/dsls/types.go
+++ b/http/codegen/openapi/v3/testdata/dsls/types.go
@@ -82,6 +82,7 @@ func ObjectResponseBodyDSL(svcName, metName string) func() {
 				Result(func() {
 					Attribute("name")
 					Attribute("age", Int)
+					Attribute("misc", Any)
 				})
 				HTTP(func() {
 					POST("/")
diff --git a/http/codegen/openapi/v3/testdata/golden/with-any_file0.golden b/http/codegen/openapi/v3/testdata/golden/with-any_file0.golden
new file mode 100644
index 00000000..5811ba86
--- /dev/null
+++ b/http/codegen/openapi/v3/testdata/golden/with-any_file0.golden
@@ -0,0 +1,1 @@
+{"openapi":"3.0.3","info":{"title":"Goa API","version":"0.0.1"},"servers":[{"url":"http://localhost:80","description":"Default server for test api"}],"paths":{"/":{"post":{"tags":["testService"],"summary":"testEndpoint testService","operationId":"testService#testEndpoint","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/TestEndpointRequestBody"},"example":{"any":"","any_array":["","","",""],"any_map":{"":""}}}}},"responses":{"200":{"description":"OK response.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/TestEndpointRequestBody"},"example":{"any":"","any_array":["","",""],"any_map":{"":""}}}}}}}}},"components":{"schemas":{"TestEndpointRequestBody":{"type":"object","properties":{"any":{"example":""},"any_array":{"type":"array","items":{"example":""},"example":["","","",""]},"any_map":{"type":"object","example":{"":""},"additionalProperties":true}},"example":{"any":"","any_array":["",""],"any_map":{"":""}}}}},"tags":[{"name":"testService"}]}
\ No newline at end of file
diff --git a/http/codegen/openapi/v3/testdata/golden/with-any_file1.golden b/http/codegen/openapi/v3/testdata/golden/with-any_file1.golden
new file mode 100644
index 00000000..6417a5c6
--- /dev/null
+++ b/http/codegen/openapi/v3/testdata/golden/with-any_file1.golden
@@ -0,0 +1,74 @@
+openapi: 3.0.3
+info:
+    title: Goa API
+    version: 0.0.1
+servers:
+    - url: http://localhost:80
+      description: Default server for test api
+paths:
+    /:
+        post:
+            tags:
+                - testService
+            summary: testEndpoint testService
+            operationId: testService#testEndpoint
+            requestBody:
+                required: true
+                content:
+                    application/json:
+                        schema:
+                            $ref: '#/components/schemas/TestEndpointRequestBody'
+                        example:
+                            any: ""
+                            any_array:
+                                - ""
+                                - ""
+                                - ""
+                                - ""
+                            any_map:
+                                "": ""
+            responses:
+                "200":
+                    description: OK response.
+                    content:
+                        application/json:
+                            schema:
+                                $ref: '#/components/schemas/TestEndpointRequestBody'
+                            example:
+                                any: ""
+                                any_array:
+                                    - ""
+                                    - ""
+                                    - ""
+                                any_map:
+                                    "": ""
+components:
+    schemas:
+        TestEndpointRequestBody:
+            type: object
+            properties:
+                any:
+                    example: ""
+                any_array:
+                    type: array
+                    items:
+                        example: ""
+                    example:
+                        - ""
+                        - ""
+                        - ""
+                        - ""
+                any_map:
+                    type: object
+                    example:
+                        "": ""
+                    additionalProperties: true
+            example:
+                any: ""
+                any_array:
+                    - ""
+                    - ""
+                any_map:
+                    "": ""
+tags:
+    - name: testService
diff --git a/http/codegen/openapi/v3/types_test.go b/http/codegen/openapi/v3/types_test.go
index f61bffc8..3d641256 100644
--- a/http/codegen/openapi/v3/types_test.go
+++ b/http/codegen/openapi/v3/types_test.go
@@ -119,7 +119,7 @@ func TestBuildBodyTypes(t *testing.T) {
 		DSL:  dsls.ObjectResponseBodyDSL(svcName, "object_response_body"),
 
 		ExpectedType:          tempty,
-		ExpectedResponseTypes: rt{200: tobj("name", tstring, "age", tint)},
+		ExpectedResponseTypes: rt{200: tobj("name", tstring, "age", tint, "misc", tempty)},
 	}, {
 		Name: "multi_cookie_response_body",
 		DSL:  dsls.MultiCookieResponseBodyDSL(svcName, "multi_cookie_response_body"),
@@ -137,7 +137,7 @@ func TestBuildBodyTypes(t *testing.T) {
 		DSL:  dsls.ObjectResponseBodyDSL(svcName, "object_streaming_response_body"),
 
 		ExpectedType:          tempty,
-		ExpectedResponseTypes: rt{200: tobj("name", tstring, "age", tint)},
+		ExpectedResponseTypes: rt{200: tobj("name", tstring, "age", tint, "misc", tempty)},
 	}, {
 		Name: "string_error_response",
 		DSL:  dsls.StringErrorResponseBodyDSL(svcName, "string_error_response"),
diff --git a/http/codegen/testdata/openapi_dsls.go b/http/codegen/testdata/openapi_dsls.go
index 3619a09c..10507141 100644
--- a/http/codegen/testdata/openapi_dsls.go
+++ b/http/codegen/testdata/openapi_dsls.go
@@ -522,6 +522,40 @@ var WithMapDSL = func() {
 	})
 }
 
+var WithAnyDSL = func() {
+	Service("testService", func() {
+		Method("testEndpoint", func() {
+			Payload(func() {
+				Attribute("any", Any, func() {
+					Example("")
+				})
+				Attribute("any_array", ArrayOf(Any, func() {
+					Example("")
+				}))
+				Attribute("any_map", MapOf(String, Any), func() {
+					Key(func() { Example("") })
+					Elem(func() { Example("") })
+				})
+			})
+			Result(func() {
+				Attribute("any", Any, func() {
+					Example("")
+				})
+				Attribute("any_array", ArrayOf(Any, func() {
+					Example("")
+				}))
+				Attribute("any_map", MapOf(String, Any), func() {
+					Key(func() { Example("") })
+					Elem(func() { Example("") })
+				})
+			})
+			HTTP(func() {
+				POST("/")
+			})
+		})
+	})
+}
+
 var PathWithWildcardDSL = func() {
 	Service("test service", func() {
 		Method("test endpoint", func() {