danielgtaylor / danielgtaylor/huma
Support for `oneOf`, `anyOf`, `allOf`, `not` via struct tags
- Dominant language
- Go
- Stars
- 4.4k
- Forks
- 285
- Avg merge
- 40m
- Merged PRs (30d)
- 1
Description
First off, I love this project! Thank you for all of the incredible work you do.
I really enjoy just being able to define request inputs and outputs via struct -- but it seems that currently the only way to use `oneOf`, `anyOf`, `allOf`, or `not` are by passing structs to `huma.Operation.RequestBody` or `huma.Operation.Response`. I think it would be useful to have struct tags to more conveniently define this.
For example, here's how I'm currently building `oneOf` tagged union requests (minus the `oneOf` struct tag). This works well enough, but would love to have a way to do achieve this via struct tags for the input and output structs. I'd love to hear other ideas and feedback, or if there's already an existing way to do this I'm just not aware of.
```go
// Request body DTO
type Example struct {
Type string `json:"type" enum:"foo,bar,baz"`
RawOpts json.RawMessage `json:"opts"`
Opts struct {
Foo Foo
Bar Bar
Baz Baz
} `json:"-" oneOf:"opts"` // oneOf tag value refers to json tag value above
// could be used to generate what would otherwise be defined manually in huma.Schema
}
// It might be more appropriate to do this with huma.Resolve? but this works too
// I chose this approach because the same could potentially be done for
// response bodies via MarshalJSON
func (x *Example) UnmarshalJSON(b []byte) error {
type alias Example
tmp := &struct{ *alias }{alias: (*alias)(x)}
if err := json.Unmarshal(b, &tmp); err != nil {
return err
}
*x = Example(*tmp.alias)
var err error
switch x.Type {
case "foo":
err = json.Unmarshal(x.RawOpts, &x.Opts.Foo)
case "bar":
err = json.Unmarshal(x.RawOpts, &x.Opts.Bar)
case "baz":
err = json.Unmarshal(x.RawOpts, &x.Opts.Baz)
default:
err = errors.New("invalid example type")
}
return err
}
// From what I can tell, all of the code below can be derived from the
// input struct body via tags and reflection
func (h *ApiHandler) RegisterExample(api huma.API) {
registry := api.OpenAPI().Components.Schemas
optsSchema := &huma.Schema{
OneOf: []*huma.Schema{
registry.Schema(reflect.TypeFor[Foo](), true, ""),
registry.Schema(reflect.TypeFor[Bar](), true, ""),
registry.Schema(reflect.TypeFor[Baz](), true, ""),
},
Nullable: false,
}
exampleSchema := registry.SchemaFromRef(registry.Schema(reflect.TypeFor[Example](), true, "").Ref)
exampleSchema.Properties["opts"] = optsSchema
exampleSchema.PrecomputeMessages()
exampleRequestBody := &huma.RequestBody{
Required: true,
Content: map[string]*huma.MediaType{"application/json": {
Schema: exampleSchema,
}},
}
// huma.Register ...
}
```
It would be great to hear your thoughts on this. Thanks again!
Contributor guide
No contributing guide indexed for this repository
Research direction
Start by reading huma.Operation.RequestBody and Response, huma.Resolve, and the registry.Schema and SchemaFromRef paths described in the issue. Clarify how struct tags should represent oneOf, anyOf, allOf, and not for both request and response schemas, then define coverage for the generated OpenAPI schemas; done means the chosen tag design works consistently without manual schema replacement.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- go
- Domain
- api, backend-api-design
- Issue type
- Feature
- Difficulty
- 5/5
- Estimated time
- Over a week
- Activity status
- Stale
- Clarity
- Needs clarification
- Newbie friendliness
- 30/100