huma
huma copied to clipboard
Support for `oneOf`, `anyOf`, `allOf`, `not` via struct tags
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.
// 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!
@dennisgsmith thanks! This looks a lot like an OpenAPI 3 discriminated union: https://swagger.io/docs/specification/v3_0/data-models/inheritance-and-polymorphism/#discriminator. I'm not opposed to adding such a feature (we already support manually creating such a schema today, but no struct tags). I've shied away from the complexity of supporting this via field tags or custom types but am open to ideas for how it should work.
Is there an example of how to have a union as an output?
Ok, it's probably https://github.com/danielgtaylor/huma/tree/main/examples/oneof-response