danielgtaylor / danielgtaylor/huma

Support for `oneOf`, `anyOf`, `allOf`, `not` via struct tags

Open
#761 2 comments 4 reactions 0 assignees View on GitHub
enhancement
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

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.