From f07545c5183935b2b230cea80c39f1f60f792a7c Mon Sep 17 00:00:00 2001 From: Christian Wygoda Date: Fri, 2 Jan 2026 15:10:42 +0100 Subject: [PATCH] feat: add ExtraParams support for declaring path params not in input models MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add support for declaring extra path parameters at the operation level. This is useful when path parameters come from parent routes (e.g., /v1/:contract_id/...) and are not defined in handler input models. - Add ExtraParams field to OperationInfo - Add ExtraParam struct for parameter metadata - Add PathParam and QueryParam OperationOption functions - Update generator to include extra params before validation 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- fizz.go | 29 ++++++++++++++++++++++ openapi/generator.go | 59 ++++++++++++++++++++++++++++++++++++++------ openapi/operation.go | 11 +++++++++ 3 files changed, 92 insertions(+), 7 deletions(-) diff --git a/fizz.go b/fizz.go index 08b76cb..a17d3a6 100644 --- a/fizz.go +++ b/fizz.go @@ -401,6 +401,35 @@ func XInternal() func(*openapi.OperationInfo) { } } +// PathParam adds an extra path parameter to the operation. +// This is useful when the path parameter is part of a parent route +// and not defined in the handler's input model. +// For example, when routes are defined under /v1/:contract_id/..., +// the contract_id parameter needs to be declared in each operation. +func PathParam(name, description string) func(*openapi.OperationInfo) { + return func(o *openapi.OperationInfo) { + o.ExtraParams = append(o.ExtraParams, &openapi.ExtraParam{ + Name: name, + In: "path", + Description: description, + Required: true, + }) + } +} + +// QueryParam adds an extra query parameter to the operation. +// This is useful for query parameters not defined in the handler's input model. +func QueryParam(name, description string, required bool) func(*openapi.OperationInfo) { + return func(o *openapi.OperationInfo) { + o.ExtraParams = append(o.ExtraParams, &openapi.ExtraParam{ + Name: name, + In: "query", + Description: description, + Required: required, + }) + } +} + // OperationFromContext returns the OpenAPI operation from // the given Gin context or an error if none is found. func OperationFromContext(ctx context.Context) (*openapi.Operation, error) { diff --git a/openapi/generator.go b/openapi/generator.go index 99f6833..d9eabc1 100644 --- a/openapi/generator.go +++ b/openapi/generator.go @@ -275,6 +275,12 @@ func (g *Generator) AddOperation(path, method, tag string, in, out reflect.Type, method != http.MethodHead && method != http.MethodDelete + // Collect extra params from info (if any) + var extraParams []*ExtraParam + if info != nil { + extraParams = info.ExtraParams + } + if in != nil { if in.Kind() == reflect.Ptr { in = in.Elem() @@ -282,7 +288,12 @@ func (g *Generator) AddOperation(path, method, tag string, in, out reflect.Type, if in.Kind() != reflect.Struct { return nil, errors.New("input type is not a struct") } - if err := g.setOperationParams(op, in, in, allowBody, path); err != nil { + if err := g.setOperationParams(op, in, in, allowBody, path, extraParams); err != nil { + return nil, err + } + } else if len(extraParams) > 0 { + // Even without an input type, we may have extra params to add + if err := g.setOperationParams(op, nil, nil, allowBody, path, extraParams); err != nil { return nil, err } } @@ -437,13 +448,47 @@ func (g *Generator) setOperationResponse(op *Operation, t reflect.Type, code, mt } // setOperationParams adds the fields of the struct type t -// to the given operation. -func (g *Generator) setOperationParams(op *Operation, t, parent reflect.Type, allowBody bool, path string) error { - if t.Kind() != reflect.Struct { - return errors.New("input type is not a struct") +// to the given operation, along with any extra params provided. +func (g *Generator) setOperationParams(op *Operation, t, parent reflect.Type, allowBody bool, path string, extraParams []*ExtraParam) error { + if t != nil { + if t.Kind() != reflect.Struct { + return errors.New("input type is not a struct") + } + if err := g.buildParamsRecursive(op, t, parent, allowBody); err != nil { + return err + } } - if err := g.buildParamsRecursive(op, t, parent, allowBody); err != nil { - return err + + // Add extra params (e.g., path params from parent routes) + for _, ep := range extraParams { + if ep == nil { + continue + } + // Check if a parameter with same name/location already exists + exists := false + for _, p := range op.Parameters { + if p != nil && p.Name == ep.Name && p.In == ep.In { + exists = true + break + } + } + if exists { + continue // Skip duplicate + } + // Path params are always required + required := ep.Required + if ep.In == "path" { + required = true + } + op.Parameters = append(op.Parameters, &ParameterOrRef{ + Parameter: &Parameter{ + Name: ep.Name, + In: ep.In, + Description: ep.Description, + Required: required, + Schema: &SchemaOrRef{Schema: &Schema{Type: "string"}}, + }, + }) } // Input fields that are neither path- nor query-bound // have been extracted into the operation's RequestBody diff --git a/openapi/operation.go b/openapi/operation.go index 306900f..e557be0 100644 --- a/openapi/operation.go +++ b/openapi/operation.go @@ -15,6 +15,17 @@ type OperationInfo struct { Security []*SecurityRequirement XCodeSamples []*XCodeSample XInternal bool + ExtraParams []*ExtraParam // Additional parameters not derived from InputModel +} + +// ExtraParam represents an additional parameter to include in the operation. +// This is useful for path parameters that come from parent routes (e.g., /v1/:contract_id/...) +// where the contract_id is not part of the handler's input model. +type ExtraParam struct { + Name string // Parameter name (e.g., "contract_id") + In string // Parameter location: "path", "query", "header", "cookie" + Description string // Optional description + Required bool // Whether the parameter is required (always true for path params) } // ResponseHeader represents a single header that