From c1173b651c4e710b91649b885488b696fd7d636f Mon Sep 17 00:00:00 2001 From: mbd3v Date: Sun, 29 Mar 2026 15:37:35 -0400 Subject: [PATCH] fixed issue with swag not properly creating form schemas that follow the openapi spec --- operationv3.go | 122 ++++++++++++++++++++++++++++++-------------- operationv3_test.go | 35 +++++++++++-- 2 files changed, 116 insertions(+), 41 deletions(-) diff --git a/operationv3.go b/operationv3.go index afd488d02..3d8911c9b 100644 --- a/operationv3.go +++ b/operationv3.go @@ -173,7 +173,7 @@ func (o *OperationV3) ParseAcceptComment(commentLine string) error { schema := spec.NewSchemaSpec() switch value { - case "application/json", "multipart/form-data", "text/xml": + case "application/json", "multipart/form-data", "text/xml", "application/x-www-form-urlencoded": schema.Spec.Type = &spec.SingleOrArray[string]{OBJECT} case "image/png", "image/jpeg", @@ -424,14 +424,24 @@ func (o *OperationV3) ParseParamComment(commentLine string, astFile *ast.File) e } case "body", "formData": if objectType == PRIMITIVE { - schema := PrimitiveSchemaV3(refType) + var schema *spec.RefOrSpec[spec.Schema] + if paramType == "formData" && refType == "file" { + schema = spec.NewSchemaSpec() + schema.Spec.Type = &spec.SingleOrArray[string]{STRING} + schema.Spec.Format = "binary" + } else { + schema = PrimitiveSchemaV3(refType) + } err := o.parseParamAttributeForBody(commentLine, objectType, refType, schema.Spec) if err != nil { return err } - o.fillRequestBody(name, schema, required, description, true, paramType == "formData") + err = o.fillRequestBody(name, schema, required, description, true, paramType == "formData") + if err != nil { + return err + } return nil @@ -446,7 +456,10 @@ func (o *OperationV3) ParseParamComment(commentLine string, astFile *ast.File) e if err != nil { return err } - o.fillRequestBody(name, schema, required, description, false, paramType == "formData") + err = o.fillRequestBody(name, schema, required, description, false, paramType == "formData") + if err != nil { + return err + } return nil @@ -468,35 +481,50 @@ func (o *OperationV3) ParseParamComment(commentLine string, astFile *ast.File) e return nil } -func (o *OperationV3) fillRequestBody(name string, schema *spec.RefOrSpec[spec.Schema], required bool, description string, primitive, formData bool) { - if o.RequestBody == nil { - o.RequestBody = spec.NewRequestBodySpec() - o.RequestBody.Spec.Spec.Content = make(map[string]*spec.Extendable[spec.MediaType]) +func isBinarySchema(schema *spec.RefOrSpec[spec.Schema]) bool { + if schema == nil || schema.Spec == nil { + return false + } + return schema.Spec.Format == "binary" +} - if primitive && !formData { - o.RequestBody.Spec.Spec.Content["text/plain"] = spec.NewMediaType() - } else if formData { - o.RequestBody.Spec.Spec.Content["application/x-www-form-urlencoded"] = spec.NewMediaType() - } else { - o.RequestBody.Spec.Spec.Content["application/json"] = spec.NewMediaType() +func (o *OperationV3) formDataContentType(schema *spec.RefOrSpec[spec.Schema]) string { + if o.RequestBody != nil && o.RequestBody.Spec != nil && o.RequestBody.Spec.Spec.Content != nil { + content := o.RequestBody.Spec.Spec.Content + + if content["multipart/form-data"] != nil { + return "multipart/form-data" + } + + if content["application/x-www-form-urlencoded"] != nil { + return "application/x-www-form-urlencoded" } } - o.RequestBody.Spec.Spec.Required = required + if isBinarySchema(schema) { + return "multipart/form-data" + } - // Append description to existing description if this is not the first body - if o.RequestBody.Spec.Spec.Description != "" && description != "" { - o.RequestBody.Spec.Spec.Description += " | " + description - } else if description != "" { - o.RequestBody.Spec.Spec.Description = description + return "application/x-www-form-urlencoded" +} + +func (o *OperationV3) fillRequestBody( + name string, + schema *spec.RefOrSpec[spec.Schema], + required bool, + description string, + primitive, formData bool, +) error { + if o.RequestBody == nil { + o.RequestBody = spec.NewRequestBodySpec() + o.RequestBody.Spec.Spec.Content = make(map[string]*spec.Extendable[spec.MediaType]) } - // Handle oneOf merging for request body schemas contentType := "application/json" if primitive && !formData { contentType = "text/plain" } else if formData { - contentType = "application/x-www-form-urlencoded" + contentType = o.formDataContentType(schema) } mediaType := o.RequestBody.Spec.Spec.Content[contentType] @@ -504,24 +532,42 @@ func (o *OperationV3) fillRequestBody(name string, schema *spec.RefOrSpec[spec.S mediaType = spec.NewMediaType() o.RequestBody.Spec.Spec.Content[contentType] = mediaType } - if schema.Ref != nil { - schema.Ref.Summary = name - schema.Ref.Description = description - } - if schema.Spec != nil { - schema.Spec.Title = name + + o.RequestBody.Spec.Spec.Required = required + if description != "" && o.RequestBody.Spec.Spec.Description == "" { + o.RequestBody.Spec.Spec.Description = description } - if mediaType.Spec.Schema == nil { - mediaType.Spec.Schema = schema - } else if mediaType.Spec.Schema.Ref != nil || mediaType.Spec.Schema.Spec.OneOf == nil { - // If there's an existing schema that doesn't have oneOf, create a oneOf schema - oneOfSchema := spec.NewSchemaSpec() - oneOfSchema.Spec.OneOf = []*spec.RefOrSpec[spec.Schema]{mediaType.Spec.Schema, schema} - mediaType.Spec.Schema = oneOfSchema - } else { - // If there's already a oneOf schema, append to it - mediaType.Spec.Schema.Spec.OneOf = append(mediaType.Spec.Schema.Spec.OneOf, schema) + + if formData { + if mediaType.Spec.Schema == nil { + mediaType.Spec.Schema = spec.NewSchemaSpec() + mediaType.Spec.Schema.Spec.Type = &spec.SingleOrArray[string]{OBJECT} + mediaType.Spec.Schema.Spec.Properties = map[string]*spec.RefOrSpec[spec.Schema]{} + } + + if mediaType.Spec.Schema.Ref != nil { + return fmt.Errorf("form request body schema cannot be a ref") + } + + if mediaType.Spec.Schema.Spec.Properties == nil { + mediaType.Spec.Schema.Spec.Properties = map[string]*spec.RefOrSpec[spec.Schema]{} + } + + if schema != nil && schema.Spec != nil && schema.Spec.Description == "" && description != "" { + schema.Spec.Description = description + } + + mediaType.Spec.Schema.Spec.Properties[name] = schema + if required && !findInSlice(mediaType.Spec.Schema.Spec.Required, name) { + mediaType.Spec.Schema.Spec.Required = + append(mediaType.Spec.Schema.Spec.Required, name) + } + + return nil } + + mediaType.Spec.Schema = schema + return nil } func (o *OperationV3) parseParamAttribute(comment, objectType, schemaType string, param *spec.Parameter) error { diff --git a/operationv3_test.go b/operationv3_test.go index 0f9713329..7a6600302 100644 --- a/operationv3_test.go +++ b/operationv3_test.go @@ -1204,11 +1204,33 @@ func TestParseParamCommentByFormDataTypeV3(t *testing.T) { requestBody := operation.RequestBody assert.True(t, requestBody.Spec.Spec.Required) assert.Equal(t, "this is a test file", requestBody.Spec.Spec.Description) - assert.NotNil(t, requestBody) requestBodySpec := requestBody.Spec.Spec assert.NotNil(t, requestBodySpec) - assert.Equal(t, &typeFile, requestBodySpec.Content["application/x-www-form-urlencoded"].Spec.Schema.Spec.Type) + + media := requestBodySpec.Content["multipart/form-data"] + if assert.NotNil(t, media) { + if assert.NotNil(t, media.Spec.Schema) && assert.NotNil(t, media.Spec.Schema.Spec) { + assert.Equal( + t, + &spec.SingleOrArray[string]{OBJECT}, + media.Spec.Schema.Spec.Type, + ) + + prop := media.Spec.Schema.Spec.Properties["file"] + if assert.NotNil(t, prop) && assert.NotNil(t, prop.Spec) { + assert.Equal( + t, + &spec.SingleOrArray[string]{STRING}, + prop.Spec.Type, + ) + assert.Equal(t, "binary", prop.Spec.Format) + assert.Equal(t, "this is a test file", prop.Spec.Description) + } + + assert.Contains(t, media.Spec.Schema.Spec.Required, "file") + } + } } func TestParseParamCommentByFormDataTypeUint64V3(t *testing.T) { @@ -1228,7 +1250,14 @@ func TestParseParamCommentByFormDataTypeUint64V3(t *testing.T) { requestBodySpec := requestBody.Spec.Spec.Content["application/x-www-form-urlencoded"].Spec assert.NotNil(t, requestBodySpec) - assert.Equal(t, &typeInteger, requestBodySpec.Schema.Spec.Type) + assert.Equal(t, &typeObject, requestBodySpec.Schema.Spec.Type) + + prop := requestBodySpec.Schema.Spec.Properties["file"] + if assert.NotNil(t, prop) && assert.NotNil(t, prop.Spec) { + assert.Equal(t, &typeInteger, prop.Spec.Type) + } + + assert.Contains(t, requestBodySpec.Schema.Spec.Required, "file") } func TestParseParamCommentByNotSupportedTypeV3(t *testing.T) {