Skip to content

请求输入#

参数#

请求可以具有参数和/或主体作为处理函数的输入。输入使用带有特殊字段和/或标签的标准 Go 结构体。以下是可用的标签:

Tag Description Example
path 路径参数的名称 path:"thing-id"
query 查询字符串参数的名称 query:"q"
header 头部参数的名称 header:"Authorization"
cookie Cookie 参数的名称 cookie:"session"
required 将查询/头部参数标记为必需 required:"true"

必需

required 标签不鼓励使用,并且仅用于查询/头部参数,这些参数通常应为可选的,供客户端发送。

参数类型#

以下参数类型开箱即用地支持:

Type Example Inputs
bool true, false
[u]int[16/32/64] 1234, 5, -1
float32/64 1.234, 1.0
string hello, t
time.Time 2020-01-01T12:00:00Z
slice, e.g. []int 1,2,3, tag1,tag2

例如,如果参数是查询参数且类型为 []string,则 URI 可能类似于 ?tags=tag1,tag2。查询参数还支持通过设置 explode 标签多次指定相同的参数,例如 query:"tags,explode" 将解析查询字符串如 ?tags=tag1&tags=tag2,而不是逗号分隔的列表。逗号分隔列表更快,大多数用例推荐使用。

对于 Cookie,默认行为是从请求中读取 Cookie 并将其转换为上述类型之一。如果要访问整个 Cookie,可以将类型替换为 http.Cookie

code.go
type MyInput struct {
	Session http.Cookie `cookie:"session"`
}

然后可以访问例如 input.Session.Nameinput.Session.Value

自定义包装类型#

可以通过实现 ParamWrapper 接口将请求参数解析为自定义包装类型,该接口应提供对包装字段的访问,类型为 reflect.Value

可选地,可以实现接口 ParamReactor 来定义在请求参数解析后执行的回调。

使用自定义包装处理空查询参数的示例:

type OptionalParam[T any] struct {
	Value T
	IsSet bool
}

// 定义要使用的包装类型的模式
func (o OptionalParam[T]) Schema(r huma.Registry) *huma.Schema {
	return huma.SchemaFromType(r, reflect.TypeOf(o.Value))
}

// 暴露包装值以接收来自 Huma 的解析值
// 必须使用指针接收器
func (o *OptionalParam[T]) Receiver() reflect.Value {
	return reflect.ValueOf(o).Elem().Field(0)
}

// 响应请求参数被解析以更新内部状态
// 必须使用指针接收器
func (o *OptionalParam[T]) OnParamSet(isSet bool, parsed any) {
	o.IsSet = isSet
}

// 使用包装类型定义请求输入
type MyRequestInput struct {
    MaybeText OptionalParam[string] `query:"text"`
}

请求主体#

特殊结构体字段 Body 将被视为输入请求主体,可以引用任何其他类型,或者可以将结构体或切片内联嵌入。如果主体是指针,则它是可选的。主体允许所有文档和验证标签,此外还有以下标签:

Tag Description Example
contentType 覆盖内容类型 contentType:"application/my-type+json"
required 将主体标记为必需 required:"true"

RawBody []byte 也可以与 Body 一起使用,以提供访问用于验证和解析 Body[]byte

特殊类型#

以下特殊类型开箱即用地支持:

Type Schema Example
time.Time {"type": "string", "format": "date-time"} "2020-01-01T12:00:00Z"
url.URL {"type": "string", "format": "uri"} "https://example.com"
net.IP {"type": "string", "format": "ipv4"} "127.0.0.1"
netip.Addr {"type": "string", "format": "ipv4"} "127.0.0.1"
json.RawMessage {} ["whatever", "you", "want"]

如果需要,可以通过 Schema CustomizationRequest Validation 中描述的方式覆盖此默认行为,例如为 IPv6 设置自定义 format 标签。

其他主体类型#

有时,您希望绕过正常的主体解析,而是直接读取原始主体内容。这对于非结构化数据、文件上传或其他二进制数据很有用。您可以使用 没有 Body 字段的 RawBody []byte 来访问原始主体字节,而不会应用任何解析/验证。例如,要接受某些 text/plain 输入:

code.go
huma.Register(api, huma.Operation{
	OperationID: "post-plain-text",
	Method:      http.MethodPost,
	Path:        "/text",
	Summary:     "示例:发布纯文本输入",
}, func(ctx context.Context, input *struct {
	RawBody []byte `contentType:"text/plain"`
}) (*struct{}, error) {
	fmt.Println("Got input:", input.RawBody)
	return nil, nil
}

这使您能够根据需要对输入进行自己的解析。

多部分表单数据#

通过在输入结构体中使用类型为 multipart.FormRawBody 来支持多部分表单数据。这将使用 Go 标准库的多部分处理实现来解析请求。

例如:

multipart.go
huma.Register(api, huma.Operation{
	OperationID: "upload-files",
    Method:      http.MethodPost,
    Path:        "/upload",
    Summary:     "示例:上传文件",
}, func(ctx context.Context, input *struct {
    RawBody multipart.Form
}) (*struct{}, error) {
    // 在此处处理多部分表单。
	for name, _ := range input.RawBody.File {
	    fmt.Printf("Obtained file with name '%s'", name)
	}
	for name, val := range input.RawBody.Value {
	    fmt.Printf("Obtained value with name '%s' and value '%s'", name, val)
	}
    return nil, nil
})

这对于支持文件上传很有用。此外,Huma 可以为您将多部分表单中的文件和值处理到结构体中。在这种情况下,您应该定义处理后的结构体应该是什么样子:

multipart_form_files.go
huma.Register(api, huma.Operation{
	OperationID: "upload-and-decode-files"
	Method:      http.MethodPost,
	Path:        "/upload",
}, func(ctx context.Context, input *struct {
	RawBody huma.MultipartFormFiles[struct {
		MyFile                    huma.FormFile   `form:"file" contentType:"text/plain" required:"true"`
		SomeOtherFiles            []huma.FormFile `form:"other-files" contentType:"text/plain" required:"true"`
		NoTagBindingFile          huma.FormFile   `contentType:"text/plain"`
		MyGreeting                string          `form:"greeting", minLength:"6"`
		SomeNumbers               []int           `form:"numbers"`
		NonTaggedValuesAreIgnored string  // ignored
	}]
}) (*struct{}, error) {
	// 原始多部分表单主体再次可在 input.RawBody.Form 下访问。
	// 例如 input.RawBody.Form.File("file")
	// 例如 input.RawBody.Form.Value("greeting")

	// 处理后的输入结构体可在 input.RawBody.Data() 下访问。
	formData := input.RawBody.Data()

	// 如果有 "form" 标签,非文件将可用并被验证
	fmt.Println(formData.MyGreeting)
	fmt.Println("These are your numbers:")
	for _, n := range formData.SomeNumbers {
		fmt.Println(n)
	}

	// 没有 "form" 标签的非文件不可用
	if formData.NonTaggedValuesAreIgnored != nil {
		panic("This should not happen")
	}

	// 在此处处理文件。
	b, err := io.ReadAll(formData.MyFile)
	fmt.Println(string(b))

	for _, f := range formData.SomeOtherFiles {
		b, err := io.ReadAll(f)
		fmt.Println(string(b))
	}

	// 用于检查可选文件存在的标志。
	if formData.NoTagBindingFile.IsSet {
		fmt.Println("The form contained a file entry with name 'NoTagBinding'!")
	}
	return nil, nil
})

文件根据指定的 contentType 进行解码。如果未提供 contentType,则默认为 application/octet-stream

请求示例#

以下是一个请求输入结构体的示例,其中包含路径参数、查询参数、头部参数以及结构化主体和原始主体字节:

code.go
type MyInput struct {
	ID      string `path:"id"`
	Detail  bool   `query:"detail" doc:"Show full details"`
	Auth    string `header:"Authorization"`
	Body    MyBody
	RawBody []byte
}

对此类端点的请求可能类似于:

Terminal
# 通过高级操作:
$ restish api my-op 123 --detail=true --authorization=foo <body.json

# 通过 URL:
$ restish api/my-op/123?detail=true -H "Authorization: foo" <body.json

上传

您可以使用没有相应 Body 字段的 RawBody []byte 来支持小文件上传。

输入组合#

由于输入只是 Go 结构体,它们是可组合和可重用的。例如:

code.go
type AuthParam struct {
	Authorization string `header:"Authorization"`
}

type PaginationParams struct {
	Cursor string `query:"cursor"`
	Limit  int    `query:"limit"`
}

// ... 代码后部
huma.Register(api, huma.Operation{
	OperationID: "list-things",
	Method:      http.MethodGet,
	Path:        "/things",
	Summary:     "获取过滤后的事物列表",
}, func(ctx context.Context, input *struct {
	// 嵌入两个结构体来组合您的输入。
	AuthParam
	PaginationParams
}) (*struct{}, error) {
	fmt.Printf("Auth: %s, Cursor: %s, Limit: %d\n", input.Authorization, input.Cursor, input.Limit)
	return nil, nil
}

深入了解#