From 0e87d63b020adfba5d857d47f657275bd2b3918a Mon Sep 17 00:00:00 2001 From: KT-Doan Date: Thu, 9 Apr 2026 15:41:49 +0900 Subject: [PATCH] fix: handle swagger:type array by falling through to underlying type resolution When swagger:type is set to a value not handled by swaggerSchemaForType (e.g., "array"), the function returns an error. Previously this error was silently ignored and the function returned nil, losing all type info. Now, when swaggerSchemaForType returns an error, the code falls through to buildFromType with the underlying type, producing the correct schema. For example, swagger:type array on type StringSlice []string now produces {type: "array", items: {type: "string"}} with the field's description preserved, instead of a $ref that drops the description. * fixes go-openapi/codescan#10 Signed-off-by: Kevin Doan Signed-off-by: KT-Doan --- application_test.go | 2 +- .../classification/models/nomodel.go | 49 ++++++++++++ schema.go | 40 ++++++++-- schema_test.go | 77 +++++++++++++++++++ 4 files changed, 160 insertions(+), 8 deletions(-) diff --git a/application_test.go b/application_test.go index 977c5d2..b895b08 100644 --- a/application_test.go +++ b/application_test.go @@ -51,7 +51,7 @@ func TestMain(m *testing.M) { func TestApplication_LoadCode(t *testing.T) { sctx := loadClassificationPkgsCtx(t) require.NotNil(t, sctx) - require.Len(t, sctx.app.Models, 39) + require.Len(t, sctx.app.Models, 45) require.Len(t, sctx.app.Meta, 1) require.Len(t, sctx.app.Routes, 7) require.Empty(t, sctx.app.Operations) diff --git a/fixtures/goparsing/classification/models/nomodel.go b/fixtures/goparsing/classification/models/nomodel.go index 2750761..d2c169c 100644 --- a/fixtures/goparsing/classification/models/nomodel.go +++ b/fixtures/goparsing/classification/models/nomodel.go @@ -799,3 +799,52 @@ type NamedMapOfStoreOrderSlices GenericMap[string, GenericSlice[StoreOrder]] // // End of models related to named types with type arguments // + +// SomeStringSlice is a named slice type with swagger:type array. +// swagger:type array +type SomeStringSlice []string + +// swagger:model namedWithArrayType +type NamedWithArrayType struct { + // Tags for this item. + Tags SomeStringSlice `json:"tags"` +} + +// SomeFixedArray is a named fixed-length array type with swagger:type array. +// swagger:type array +type SomeFixedArray [5]string + +// swagger:model namedWithFixedArrayType +type NamedWithFixedArrayType struct { + // Labels for this item. + Labels SomeFixedArray `json:"labels"` +} + +// SomeStructWithBadType is a struct type with an unsupported swagger:type value. +// The unsupported value should be ignored and the type should fall through to $ref. +// +// swagger:type badvalue +// swagger:model someStructWithBadType +type SomeStructWithBadType struct { + Name string `json:"name"` +} + +// swagger:model namedWithBadStructType +type NamedWithBadStructType struct { + // The nested struct with an unsupported swagger:type. + Nested SomeStructWithBadType `json:"nested"` +} + +// SomeObjectType is a struct with swagger:type object. +// swagger:type object +// swagger:model someObjectType +type SomeObjectType struct { + Key string `json:"key"` + Value string `json:"value"` +} + +// swagger:model namedWithObjectStructType +type NamedWithObjectStructType struct { + // Headers for this request. + Headers SomeObjectType `json:"headers"` +} diff --git a/schema.go b/schema.go index 83bbfb9..099c1c7 100644 --- a/schema.go +++ b/schema.go @@ -429,10 +429,15 @@ func (s *schemaBuilder) buildNamedType(titpe *types.Named, tgt swaggerTypable) e cmt = new(ast.CommentGroup) } - if typeName, ok := typeName(cmt); ok { - _ = swaggerSchemaForType(typeName, tgt) - - return nil + if tn, ok := typeName(cmt); ok { + if err := swaggerSchemaForType(tn, tgt); err == nil { + return nil + } + // For unsupported swagger:type values (e.g., "array"), fall through + // to underlying type resolution so the full schema (including items + // for slices) is properly built. Build directly from the underlying + // type to bypass the named-type $ref creation. + return s.buildFromType(titpe.Underlying(), tgt) } if s.decl.Spec.Assign.IsValid() { @@ -559,9 +564,12 @@ func (s *schemaBuilder) buildNamedStruct(tio *types.TypeName, cmt *ast.CommentGr return nil } - if typeName, ok := typeName(cmt); ok { - _ = swaggerSchemaForType(typeName, tgt) - return nil + if tn, ok := typeName(cmt); ok { + if err := swaggerSchemaForType(tn, tgt); err == nil { + return nil + } + // For unsupported swagger:type values, fall through to makeRef + // rather than silently returning an empty schema. } return s.makeRef(decl, tgt) @@ -583,6 +591,14 @@ func (s *schemaBuilder) buildNamedArray(tio *types.TypeName, cmt *ast.CommentGro tgt.Items().Typed("string", sfnm) return nil } + // When swagger:type is set to an unsupported value (e.g., "array"), + // skip the $ref and inline the array schema with proper items type. + if tn, ok := typeName(cmt); ok { + if err := swaggerSchemaForType(tn, tgt); err != nil { + return s.buildFromType(elem, tgt.Items()) + } + return nil + } if decl, ok := s.ctx.FindModel(tio.Pkg().Path(), tio.Name()); ok { return s.makeRef(decl, tgt) } @@ -600,6 +616,16 @@ func (s *schemaBuilder) buildNamedSlice(tio *types.TypeName, cmt *ast.CommentGro tgt.Items().Typed("string", sfnm) return nil } + // When swagger:type is set to an unsupported value (e.g., "array"), + // skip the $ref and inline the slice schema with proper items type. + // This preserves the field's description that would be lost with $ref. + if tn, ok := typeName(cmt); ok { + if err := swaggerSchemaForType(tn, tgt); err != nil { + // Unsupported type name (e.g., "array") — build inline from element type. + return s.buildFromType(elem, tgt.Items()) + } + return nil + } if decl, ok := s.ctx.FindModel(tio.Pkg().Path(), tio.Name()); ok { return s.makeRef(decl, tgt) } diff --git a/schema_test.go b/schema_test.go index 4d52589..96a44cd 100644 --- a/schema_test.go +++ b/schema_test.go @@ -2582,3 +2582,80 @@ func TestSetEnumDoesNotPanic(t *testing.T) { require.NoError(t, err) } + +func TestSwaggerTypeNamedArray(t *testing.T) { + sctx := loadClassificationPkgsCtx(t) + decl := getClassificationModel(sctx, "NamedWithArrayType") + require.NotNil(t, decl) + prs := &schemaBuilder{ + ctx: sctx, + decl: decl, + } + models := make(map[string]spec.Schema) + require.NoError(t, prs.Build(models)) + schema := models["namedWithArrayType"] + + // swagger:type array on a named []string type should produce + // an inlined array with string items, not a $ref. + assertArrayProperty(t, &schema, "string", "tags", "", "Tags") +} + +func TestSwaggerTypeNamedFixedArray(t *testing.T) { + sctx := loadClassificationPkgsCtx(t) + decl := getClassificationModel(sctx, "NamedWithFixedArrayType") + require.NotNil(t, decl) + prs := &schemaBuilder{ + ctx: sctx, + decl: decl, + } + models := make(map[string]spec.Schema) + require.NoError(t, prs.Build(models)) + schema := models["namedWithFixedArrayType"] + + // swagger:type array on a named [5]string type should produce + // an inlined array with string items via buildNamedArray. + assertArrayProperty(t, &schema, "string", "labels", "", "Labels") +} + +func TestSwaggerTypeBadValueOnStruct(t *testing.T) { + sctx := loadClassificationPkgsCtx(t) + decl := getClassificationModel(sctx, "NamedWithBadStructType") + require.NotNil(t, decl) + prs := &schemaBuilder{ + ctx: sctx, + decl: decl, + } + models := make(map[string]spec.Schema) + require.NoError(t, prs.Build(models)) + schema := models["namedWithBadStructType"] + + // swagger:type with an unsupported value on a struct should not + // produce an empty schema — it should either inline the struct + // or create a $ref. The key assertion is that the property exists + // and is not empty (i.e., the error was not silently swallowed). + prop := schema.Properties["nested"] + hasType := len(prop.Type) > 0 + hasRef := prop.Ref.String() != "" + hasProps := len(prop.Properties) > 0 + assert.TrueT(t, hasType || hasRef || hasProps, + "expected nested property to have type, $ref, or properties — not an empty schema") +} + +func TestSwaggerTypeObjectOnStruct(t *testing.T) { + sctx := loadClassificationPkgsCtx(t) + decl := getClassificationModel(sctx, "NamedWithObjectStructType") + require.NotNil(t, decl) + prs := &schemaBuilder{ + ctx: sctx, + decl: decl, + } + models := make(map[string]spec.Schema) + require.NoError(t, prs.Build(models)) + schema := models["namedWithObjectStructType"] + + // swagger:type object on a struct should inline as type:object, + // preserving the field's description. + prop := schema.Properties["headers"] + assert.TrueT(t, prop.Type.Contains("object")) + assert.Empty(t, prop.Ref.String(), "should not have $ref when swagger:type object is set") +}