Skip to content

Path parameters with required:"false" struct tag omit required field from OpenAPI spec (regression since v2.37.0) #1009

@ritikrishu

Description

@ritikrishu

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 1parseParamLocation() 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 3Param.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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions