From 61f08bc3b5a3327c7965270302cd84aa9c91d3dd Mon Sep 17 00:00:00 2001 From: "a.gorbunov" Date: Sun, 18 Jan 2026 23:26:46 +0200 Subject: [PATCH] feat: add explode() annotation for OpenAPI 3.x parameters Add support for the explode() annotation to explicitly set the explode property on query/path/header parameters in OpenAPI 3.x. Usage: // @Param ids query []string true "ID List" explode(false) // @Param tags query []string true "Tag List" explode(true) This allows users to control whether array/object parameters are serialized as: - explode(true): ?ids=a&ids=b&ids=c (repeated params) - explode(false): ?ids=a,b,c (comma-separated) Changes: - operation.go: Add explodeTag constant and regex pattern - operationv3.go: Add setExplodeParamV3 function and case in switch - operationv3_test.go: Add tests for explode(true/false/invalid) Closes: #2134 --- operation.go | 3 +++ operationv3.go | 14 ++++++++++++++ operationv3_test.go | 44 ++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 61 insertions(+) diff --git a/operation.go b/operation.go index 5e5eff668..e61b50685 100644 --- a/operation.go +++ b/operation.go @@ -434,6 +434,7 @@ const ( readOnlyTag = "readonly" extensionsTag = "extensions" collectionFormatTag = "collectionFormat" + explodeTag = "explode" patternTag = "pattern" oneOfTag = "oneOf" ) @@ -457,6 +458,8 @@ var regexAttributes = map[string]*regexp.Regexp{ extensionsTag: regexp.MustCompile(`(?i)\s+extensions\(.*\)`), // for collectionFormat(csv) collectionFormatTag: regexp.MustCompile(`(?i)\s+collectionFormat\(.*\)`), + // for explode(true) or explode(false) + explodeTag: regexp.MustCompile(`(?i)\s+explode\(.*\)`), // example(0) exampleTag: regexp.MustCompile(`(?i)\s+example\(.*\)`), // schemaExample(0) diff --git a/operationv3.go b/operationv3.go index afd488d02..9bb5b84e1 100644 --- a/operationv3.go +++ b/operationv3.go @@ -561,6 +561,8 @@ func (o *OperationV3) parseParamAttribute(comment, objectType, schemaType string param.Schema.Spec.Extensions = setExtensionParam(attr) case collectionFormatTag: err = setCollectionFormatParamV3(param, attrKey, objectType, attr, comment) + case explodeTag: + err = setExplodeParamV3(param, attr, comment) } if err != nil { @@ -616,6 +618,18 @@ func setCollectionFormatParamV3(param *spec.Parameter, name, schemaType, attr, c return fmt.Errorf("%s is attribute to set to an array. comment=%s got=%s", name, commentLine, schemaType) } +func setExplodeParamV3(param *spec.Parameter, attr, commentLine string) error { + switch strings.ToLower(attr) { + case "true": + param.Explode = true + case "false": + param.Explode = false + default: + return fmt.Errorf("explode must be 'true' or 'false', got '%s' in comment: %s", attr, commentLine) + } + return nil +} + func setSchemaExampleV3(param *spec.Schema, schemaType string, value string) error { val, err := defineType(schemaType, value) if err != nil { diff --git a/operationv3_test.go b/operationv3_test.go index 0f9713329..fbdcf179c 100644 --- a/operationv3_test.go +++ b/operationv3_test.go @@ -1029,6 +1029,50 @@ func TestParseParamCommentQueryArrayFormatV3(t *testing.T) { } +func TestParseParamCommentExplodeV3(t *testing.T) { + t.Parallel() + + t.Run("explode true", func(t *testing.T) { + comment := `@Param ids query []string true "ID List" explode(true)` + operation := NewOperationV3(New()) + err := operation.ParseComment(comment, nil) + assert.NoError(t, err) + + parameters := operation.Operation.Parameters + assert.NotNil(t, parameters) + + parameterSpec := parameters[0].Spec.Spec + assert.NotNil(t, parameterSpec) + assert.Equal(t, "ids", parameterSpec.Name) + assert.Equal(t, "query", parameterSpec.In) + assert.Equal(t, true, parameterSpec.Explode) + }) + + t.Run("explode false", func(t *testing.T) { + comment := `@Param ids query []string true "ID List" explode(false)` + operation := NewOperationV3(New()) + err := operation.ParseComment(comment, nil) + assert.NoError(t, err) + + parameters := operation.Operation.Parameters + assert.NotNil(t, parameters) + + parameterSpec := parameters[0].Spec.Spec + assert.NotNil(t, parameterSpec) + assert.Equal(t, "ids", parameterSpec.Name) + assert.Equal(t, "query", parameterSpec.In) + assert.Equal(t, false, parameterSpec.Explode) + }) + + t.Run("explode invalid value", func(t *testing.T) { + comment := `@Param ids query []string true "ID List" explode(invalid)` + operation := NewOperationV3(New()) + err := operation.ParseComment(comment, nil) + assert.Error(t, err) + assert.Contains(t, err.Error(), "explode must be 'true' or 'false'") + }) +} + func TestParseParamCommentByIDV3(t *testing.T) { t.Parallel()