Description
When a path parameter struct field has required:"false" in its tag, huma v2.37.0+ omits the "required" field entirely from the generated OpenAPI spec. Per the OpenAPI 3.x specification, path parameters MUST have required: true:
If the parameter location is "path", this property is REQUIRED and its value MUST be true.
This is a regression from v2.35.0, where required:"false" on path parameters was silently ignored and required: true was always emitted.
Reproduction
package main
import (
"context"
"fmt"
"net/http"
"github.com/danielgtaylor/huma/v2"
"github.com/danielgtaylor/huma/v2/humatest"
)
func main() {
_, api := humatest.New(nil, huma.DefaultConfig("Test", "1.0.0"))
huma.Register(api, huma.Operation{
OperationID: "trigger-error",
Method: http.MethodPost,
Path: "/debug/error/{code}",
Summary: "Trigger error",
}, func(ctx context.Context, input *struct {
Code int `path:"code" required:"false" default:"403"`
}) (*struct{}, error) {
return nil, nil
})
b, _ := api.OpenAPI().MarshalJSON()
fmt.Println(string(b))
}
Expected output (parameter object):
{
"in": "path",
"name": "code",
"required": true,
"schema": { "default": 403, "type": "integer" }
}
Actual output (v2.37.3):
{
"in": "path",
"name": "code",
"schema": { "default": 403, "type": "integer" }
}
The "required" field is missing entirely. This causes downstream OpenAPI consumers (in our case, orval) to reject the spec as invalid.
Root Cause
this probably s due to to interaction between two code paths in huma.go:
Step 1 — parseParamLocation() correctly sets Required = true for path params (huma.go:149):
case f.Tag.Get("path") != "":
pfi.Loc = "path"
pfi.Name = f.Tag.Get("path")
pfi.Required = true
Step 2 — Back in findParams(), the required tag override runs unconditionally after step 1 (huma.go:245-247):
if _, ok = f.Tag.Lookup("required"); ok {
pfi.Required = boolTag(f, "required", false)
}
This overwrites Required back to false for any path parameter tagged required:"false".
Step 3 — Param.MarshalJSON() uses omitEmpty for the Required field (openapi.go:561):
{"required", p.Required, omitEmpty},
Since false is the zero value for bool, it gets omitted entirely from the output.
Why this changed
In v2.35.0, the required tag handling was one-directional — it could only promote a parameter to required:
if r := f.Tag.Get("required"); r == "true" {
pfi.Required = true
}
PR #925 (v2.37.0) changed this to bidirectional to support the new FieldsOptionalByDefault config option. This inadvertently allowed required:"false" to override the mandatory required: true for path parameters.
Notably, in #863, @wolveix explicitly stated:
"according to the OpenAPI specification, the path parameter is a strict requirement. Furthermore, supporting optional path parameters would generate non-compliant specs for OpenAPI consumers"
So I believe the current behavior is unintentional.
Environment
- huma version: v2.37.3 (regression introduced in v2.37.0)
- Go version: 1.24
- Last working version: v2.35.0
P.S. this might be a possible fix:
adding a guard after the tag override in findParams()... something like:
if _, ok = f.Tag.Lookup("required"); ok {
pfi.Required = boolTag(f, "required", false)
}
// Path parameters must always be required per OpenAPI spec
if pfi.Loc == "path" {
pfi.Required = true
}
but there may be a more appropriate place to enforce this invariant.
Description
When a path parameter struct field has
required:"false"in its tag, huma v2.37.0+ omits the"required"field entirely from the generated OpenAPI spec. Per the OpenAPI 3.x specification, path parameters MUST haverequired: true:This is a regression from v2.35.0, where
required:"false"on path parameters was silently ignored andrequired: truewas always emitted.Reproduction
Expected output (parameter object):
{ "in": "path", "name": "code", "required": true, "schema": { "default": 403, "type": "integer" } }Actual output (v2.37.3):
{ "in": "path", "name": "code", "schema": { "default": 403, "type": "integer" } }The
"required"field is missing entirely. This causes downstream OpenAPI consumers (in our case, orval) to reject the spec as invalid.Root Cause
this probably s due to to interaction between two code paths in
huma.go:Step 1 —
parseParamLocation()correctly setsRequired = truefor path params (huma.go:149):Step 2 — Back in
findParams(), therequiredtag override runs unconditionally after step 1 (huma.go:245-247):This overwrites
Requiredback tofalsefor any path parameter taggedrequired:"false".Step 3 —
Param.MarshalJSON()usesomitEmptyfor theRequiredfield (openapi.go:561):{"required", p.Required, omitEmpty},Since
falseis the zero value forbool, it gets omitted entirely from the output.Why this changed
In v2.35.0, the
requiredtag handling was one-directional — it could only promote a parameter to required:PR #925 (v2.37.0) changed this to bidirectional to support the new
FieldsOptionalByDefaultconfig option. This inadvertently allowedrequired:"false"to override the mandatoryrequired: truefor path parameters.Notably, in #863, @wolveix explicitly stated:
So I believe the current behavior is unintentional.
Environment
P.S. this might be a possible fix:
adding a guard after the tag override in
findParams()... something like:but there may be a more appropriate place to enforce this invariant.