请求输入#
参数#
请求可以具有参数和/或主体作为处理函数的输入。输入使用带有特殊字段和/或标签的标准 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:
然后可以访问例如 input.Session.Name 或 input.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 Customization 和 Request Validation 中描述的方式覆盖此默认行为,例如为 IPv6 设置自定义 format 标签。
其他主体类型#
有时,您希望绕过正常的主体解析,而是直接读取原始主体内容。这对于非结构化数据、文件上传或其他二进制数据很有用。您可以使用 没有 Body 字段的 RawBody []byte 来访问原始主体字节,而不会应用任何解析/验证。例如,要接受某些 text/plain 输入:
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.Form 的 RawBody 来支持多部分表单数据。这将使用 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 可以为您将多部分表单中的文件和值处理到结构体中。在这种情况下,您应该定义处理后的结构体应该是什么样子:
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。
请求示例#
以下是一个请求输入结构体的示例,其中包含路径参数、查询参数、头部参数以及结构化主体和原始主体字节:
type MyInput struct {
ID string `path:"id"`
Detail bool `query:"detail" doc:"Show full details"`
Auth string `header:"Authorization"`
Body MyBody
RawBody []byte
}
对此类端点的请求可能类似于:
# 通过高级操作:
$ 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 结构体,它们是可组合和可重用的。例如:
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
}
深入了解#
- 教程
- Your First API 包括注册带有路径参数的操作
- 参考
huma.Register注册新操作huma.Operation操作
- 外部链接