diff --git a/tools/goctl/api/swagger/properties.go b/tools/goctl/api/swagger/properties.go index e5bf6d204..bf263fb5a 100644 --- a/tools/goctl/api/swagger/properties.go +++ b/tools/goctl/api/swagger/properties.go @@ -70,15 +70,40 @@ func propertiesFromType(ctx Context, tp apiSpec.Type) (spec.SchemaProperties, [] switch sampleTypeFromGoType(ctx, member.Type) { case swaggerTypeArray: schema.Items = itemsFromGoType(ctx, member.Type) + // Special handling for arrays with useDefinitions + if ctx.UseDefinitions { + // For arrays, check if the array element (not the array itself) contains a struct + if arrayType, ok := member.Type.(apiSpec.ArrayType); ok { + if structName, containsStruct := containsStruct(arrayType.Value); containsStruct { + // Set the $ref inside the items, not at the schema level + schema.Items = &spec.SchemaOrArray{ + Schema: &spec.Schema{ + SchemaProps: spec.SchemaProps{ + Ref: spec.MustCreateRef(getRefName(structName)), + }, + }, + } + } + } + } case swaggerTypeObject: p, r := propertiesFromType(ctx, member.Type) schema.Properties = p schema.Required = r - } - if ctx.UseDefinitions { - structName, containsStruct := containsStruct(member.Type) - if containsStruct { - schema.SchemaProps.Ref = spec.MustCreateRef(getRefName(structName)) + // For objects with useDefinitions, set $ref at schema level + if ctx.UseDefinitions { + structName, containsStruct := containsStruct(member.Type) + if containsStruct { + schema.SchemaProps.Ref = spec.MustCreateRef(getRefName(structName)) + } + } + default: + // For non-array, non-object types, apply useDefinitions logic + if ctx.UseDefinitions { + structName, containsStruct := containsStruct(member.Type) + if containsStruct { + schema.SchemaProps.Ref = spec.MustCreateRef(getRefName(structName)) + } } } diff --git a/tools/goctl/api/swagger/swagger_test.go b/tools/goctl/api/swagger/swagger_test.go index d9fa47a95..2493f175d 100644 --- a/tools/goctl/api/swagger/swagger_test.go +++ b/tools/goctl/api/swagger/swagger_test.go @@ -3,6 +3,7 @@ package swagger import ( "testing" + "github.com/zeromicro/go-zero/tools/goctl/api/spec" "github.com/stretchr/testify/assert" ) @@ -23,3 +24,117 @@ func Test_pathVariable2SwaggerVariable(t *testing.T) { assert.Equal(t, tc.expected, result) } } + +func TestArrayDefinitionsBug(t *testing.T) { + // Test case for the bug where array of structs with useDefinitions + // generates incorrect swagger JSON structure + + // Context with useDefinitions enabled + ctx := Context{ + UseDefinitions: true, + } + + // Create a test struct containing an array of structs + testStruct := spec.DefineStruct{ + RawName: "TestStruct", + Members: []spec.Member{ + { + Name: "ArrayField", + Type: spec.ArrayType{ + Value: spec.DefineStruct{ + RawName: "ItemStruct", + Members: []spec.Member{ + { + Name: "ItemName", + Type: spec.PrimitiveType{RawName: "string"}, + Tag: `json:"itemName"`, + }, + }, + }, + }, + Tag: `json:"arrayField"`, + }, + }, + } + + // Get properties from the struct + properties, _ := propertiesFromType(ctx, testStruct) + + // Check that we have the array field + assert.Contains(t, properties, "arrayField") + arrayField := properties["arrayField"] + + // Verify the array field has correct structure + assert.Equal(t, "array", arrayField.Type[0]) + + // Check that we have items + assert.NotNil(t, arrayField.Items, "Array should have items defined") + assert.NotNil(t, arrayField.Items.Schema, "Array items should have schema") + + // The FIX: $ref should be inside items, not at schema level + hasRef := arrayField.Ref.String() != "" + assert.False(t, hasRef, "Schema level should NOT have $ref") + + // The $ref should be in the items + hasItemsRef := arrayField.Items.Schema.Ref.String() != "" + assert.True(t, hasItemsRef, "Items should have $ref") + assert.Equal(t, "#/definitions/ItemStruct", arrayField.Items.Schema.Ref.String()) + + // Verify there are no other properties in the items when using $ref + assert.Nil(t, arrayField.Items.Schema.Properties, "Items with $ref should not have properties") + assert.Empty(t, arrayField.Items.Schema.Required, "Items with $ref should not have required") + assert.Empty(t, arrayField.Items.Schema.Type, "Items with $ref should not have type") +} + +func TestArrayWithoutDefinitions(t *testing.T) { + // Test that arrays work correctly when useDefinitions is false + ctx := Context{ + UseDefinitions: false, // This is the default + } + + // Create the same test struct + testStruct := spec.DefineStruct{ + RawName: "TestStruct", + Members: []spec.Member{ + { + Name: "ArrayField", + Type: spec.ArrayType{ + Value: spec.DefineStruct{ + RawName: "ItemStruct", + Members: []spec.Member{ + { + Name: "ItemName", + Type: spec.PrimitiveType{RawName: "string"}, + Tag: `json:"itemName"`, + }, + }, + }, + }, + Tag: `json:"arrayField"`, + }, + }, + } + + properties, _ := propertiesFromType(ctx, testStruct) + + assert.Contains(t, properties, "arrayField") + arrayField := properties["arrayField"] + + // Should be array type + assert.Equal(t, "array", arrayField.Type[0]) + + // Should have items with full schema, no $ref + assert.NotNil(t, arrayField.Items) + assert.NotNil(t, arrayField.Items.Schema) + + // Should NOT have $ref at schema level + assert.Empty(t, arrayField.Ref.String(), "Schema should not have $ref when useDefinitions is false") + + // Should NOT have $ref in items either + assert.Empty(t, arrayField.Items.Schema.Ref.String(), "Items should not have $ref when useDefinitions is false") + + // Should have full schema properties in items + assert.Equal(t, "object", arrayField.Items.Schema.Type[0]) + assert.Contains(t, arrayField.Items.Schema.Properties, "itemName") + assert.Equal(t, []string{"itemName"}, arrayField.Items.Schema.Required) +}