Repository: swaggest/openapi-go (go)

Issue:
setupRequest doesn't work with jsonschema.Struct
**Describe the bug**
When a `jsonschema.Struct` is passed to `setupRequest` it doesn't work. For example, when used with `oc.AddReqStructure`. As far as I can tell, it happens because `refl.HasTaggedFields` (called from `ReflectRequestBody`) works on the `Struct`'s fields instead of the `Field` elements.

**To Reproduce**
[Here](https://go.dev/play/p/ARwi9ydGLc5) is an example for `HasTaggedFields` returning an incorrect answer.

**Expected behavior**
`setupRequest` should work on `jsonschema.Struct` the same way `setupResponse` does.

**Additional context**
I'm trying to add an OpenAPI schema to an existing APIs. I don't want to rewrite all the input structs, but wanted to extend them to include the URL params. Unfortunately, this didn't work due the aforementioned issue.


Pull request:
Fix support for jsonschema.Struct in requests
Fixes #109.

Patch:
diff --git a/internal/json_schema.go b/internal/json_schema.go
index a7166c9..f19d55f 100644
--- a/internal/json_schema.go
+++ b/internal/json_schema.go
@@ -66,6 +66,14 @@ func ReflectRequestBody(
 		hasTaggedFields = refl.HasTaggedFields(input, t)
 	}
 
+	hasJSONSchemaStruct := false
+
+	refl.WalkFieldsRecursively(reflect.ValueOf(input), func(v reflect.Value, _ reflect.StructField, _ []reflect.StructField) {
+		if v.Type() == reflect.TypeOf(jsonschema.Struct{}) {
+			hasJSONSchemaStruct = true
+		}
+	})
+
 	// Form data can not have map or array as body.
 	if !hasTaggedFields && len(mapping) == 0 && tag != tagJSON {
 		return nil, false, nil
@@ -74,7 +82,7 @@ func ReflectRequestBody(
 	// If `formData` is defined on a request body `json` is ignored.
 	if tag == tagJSON &&
 		(refl.HasTaggedFields(input, tagFormData) || refl.HasTaggedFields(input, tagForm)) &&
-		!forceJSONRequestBody {
+		!forceJSONRequestBody && !hasJSONSchemaStruct {
 		return nil, false, nil
 	}
 
@@ -89,7 +97,7 @@ func ReflectRequestBody(
 	}
 
 	// JSON can be a map or array without field tags.
-	if !hasTaggedFields && len(mapping) == 0 && !refl.IsSliceOrMap(input) &&
+	if !hasTaggedFields && !hasJSONSchemaStruct && len(mapping) == 0 && !refl.IsSliceOrMap(input) &&
 		refl.FindEmbeddedSliceOrMap(input) == nil && !isProcessWithoutTags {
 		return nil, false, nil
 	}


Test patch:
diff --git a/openapi3/reflect_test.go b/openapi3/reflect_test.go
index 6d0dc1e..6b30525 100644
--- a/openapi3/reflect_test.go
+++ b/openapi3/reflect_test.go
@@ -1123,3 +1123,89 @@ func TestReflector_AddOperation_defName(t *testing.T) {
 	  }
 	}`, r.Spec)
 }
+
+func TestReflector_AddOperation_jsonschemaStruct(t *testing.T) {
+	r := openapi3.NewReflector()
+
+	oc, err := r.NewOperationContext(http.MethodPost, "/foo/{id}")
+	require.NoError(t, err)
+
+	type Req struct {
+		ID int `path:"id"`
+		jsonschema.Struct
+	}
+
+	req := Req{}
+	req.DefName = "FooStruct"
+	req.Fields = append(req.Fields, jsonschema.Field{
+		Name:  "Foo",
+		Value: "abc",
+		Tag:   `json:"foo" minLength:"3"`,
+	})
+
+	type Resp struct {
+		ID int `json:"id"`
+		jsonschema.Struct
+		Nested jsonschema.Struct `json:"nested"`
+	}
+
+	resp := Resp{}
+	resp.DefName = "BarStruct"
+	resp.Fields = append(resp.Fields, jsonschema.Field{
+		Name:  "Bar",
+		Value: "cba",
+		Tag:   `json:"bar" maxLength:"3"`,
+	})
+	resp.Nested.DefName = "BazStruct"
+	resp.Nested.Fields = append(resp.Nested.Fields, jsonschema.Field{
+		Name:  "Baz",
+		Value: "def",
+		Tag:   `json:"baz" maxLength:"5"`,
+	})
+
+	oc.AddReqStructure(req)
+	oc.AddRespStructure(resp)
+
+	require.NoError(t, r.AddOperation(oc))
+
+	assertjson.EqMarshal(t, `{
+	  "openapi":"3.0.3","info":{"title":"","version":""},
+	  "paths":{
+		"/foo/{id}":{
+		  "post":{
+			"parameters":[
+			  {
+				"name":"id","in":"path","required":true,"schema":{"type":"integer"}
+			  }
+			],
+			"requestBody":{
+			  "content":{
+				"application/json":{"schema":{"$ref":"#/components/schemas/FooStruct"}}
+			  }
+			},
+			"responses":{
+			  "200":{
+				"description":"OK",
+				"content":{
+				  "application/json":{"schema":{"$ref":"#/components/schemas/BarStruct"}}
+				}
+			  }
+			}
+		  }
+		}
+	  },
+	  "components":{
+		"schemas":{
+		  "BarStruct":{
+			"properties":{
+			  "bar":{"maxLength":3,"type":"string"},"id":{"type":"integer"},
+			  "nested":{"$ref":"#/components/schemas/BazStruct"}
+			},
+			"type":"object"
+		  },
+		  "BazStruct":{"properties":{"baz":{"maxLength":5,"type":"string"}},"type":"object"},
+		  "FooStruct":{"properties":{"foo":{"minLength":3,"type":"string"}},"type":"object"}
+		}
+	  }
+	}`, r.SpecSchema())
+}
diff --git a/openapi31/reflect_test.go b/openapi31/reflect_test.go
index 3cc40f5..524e4df 100644
--- a/openapi31/reflect_test.go
+++ b/openapi31/reflect_test.go
@@ -1251,3 +1251,89 @@ func TestReflector_AddOperation_rawSchema(t *testing.T) {
 	  }
 	}`, r.SpecSchema())
 }
+
+func TestReflector_AddOperation_jsonschemaStruct(t *testing.T) {
+	r := openapi31.NewReflector()
+
+	oc, err := r.NewOperationContext(http.MethodPost, "/foo/{id}")
+	require.NoError(t, err)
+
+	type Req struct {
+		ID int `path:"id"`
+		jsonschema.Struct
+	}
+
+	req := Req{}
+	req.DefName = "FooStruct"
+	req.Fields = append(req.Fields, jsonschema.Field{
+		Name:  "Foo",
+		Value: "abc",
+		Tag:   `json:"foo" minLength:"3"`,
+	})
+
+	type Resp struct {
+		ID int `json:"id"`
+		jsonschema.Struct
+		Nested jsonschema.Struct `json:"nested"`
+	}
+
+	resp := Resp{}
+	resp.DefName = "BarStruct"
+	resp.Fields = append(resp.Fields, jsonschema.Field{
+		Name:  "Bar",
+		Value: "cba",
+		Tag:   `json:"bar" maxLength:"3"`,
+	})
+	resp.Nested.DefName = "BazStruct"
+	resp.Nested.Fields = append(resp.Nested.Fields, jsonschema.Field{
+		Name:  "Baz",
+		Value: "def",
+		Tag:   `json:"baz" maxLength:"5"`,
+	})
+
+	oc.AddReqStructure(req)
+	oc.AddRespStructure(resp)
+
+	require.NoError(t, r.AddOperation(oc))
+
+	assertjson.EqMarshal(t, `{
+	  "openapi":"3.1.0","info":{"title":"","version":""},
+	  "paths":{
+		"/foo/{id}":{
+		  "post":{
+			"parameters":[
+			  {
+				"name":"id","in":"path","required":true,"schema":{"type":"integer"}
+			  }
+			],
+			"requestBody":{
+			  "content":{
+				"application/json":{"schema":{"$ref":"#/components/schemas/FooStruct"}}
+			  }
+			},
+			"responses":{
+			  "200":{
+				"description":"OK",
+				"content":{
+				  "application/json":{"schema":{"$ref":"#/components/schemas/BarStruct"}}
+				}
+			  }
+			}
+		  }
+		}
+	  },
+	  "components":{
+		"schemas":{
+		  "BarStruct":{
+			"properties":{
+			  "bar":{"maxLength":3,"type":"string"},"id":{"type":"integer"},
+			  "nested":{"$ref":"#/components/schemas/BazStruct"}
+			},
+			"type":"object"
+		  },
+		  "BazStruct":{"properties":{"baz":{"maxLength":5,"type":"string"}},"type":"object"},
+		  "FooStruct":{"properties":{"foo":{"minLength":3,"type":"string"}},"type":"object"}
+		}
+	  }
+	}`, r.SpecSchema())
+}