Defining a oneOf/union type #828
Replies: 3 comments
-
Beta Was this translation helpful? Give feedback.
-
|
I was attempting something somewhat similar to what I think you're trying to do here. If I remember correctly, it seemed like a schema for every type that I was using had already been created by the time my own code got run. Because of this, creating a schema with I can't remember the details, but I managed to mostly sort things out by creating the schema like that and then using its Here I've approximately adapted my approach to your situation and hopefully that'll get you in the right direction. // Before setting any of my routes
registry := api.OpenAPI().Components.Schemas
...
// Building the "parent" schema so that we can get its Ref
var detailSpecSchema = registry.Schema(reflect.TypeOf(DetailSpec{}), true, "")
// Building the schemas for each option so that we can get their Refs
var dvsFreeSchema = registry.Schema(reflect.TypeOf(DetailValueSpecFree{}), true, "")
var dvsResourceSchema = registry.Schema(reflect.TypeOf(DetailValueSpecResource{}), true, "")
// Setting pretty titles for the selection menu :)
registry.SchemaFromRef(dvsFreeSchema.Ref).Title = "Detail Value Spec (Free)"
registry.SchemaFromRef(dvsResourceSchema.Ref).Title = "Detail Value Spec (Resource)"
// Setting our discriminated union as the type for the parent's "valueSpecs" property
registry.SchemaFromRef(detailSpecSchema.Ref).Properties["valueSpecs"] = &huma.Schema{
OneOf: []*huma.Schema{
registry.SchemaFromRef(dvsFreeSchema.Ref),
registry.SchemaFromRef(dvsResourceSchema.Ref),
},
Discriminator: &huma.Discriminator{
PropertyName: "kind",
Mapping: map[string]string{
"free": dvsFreeSchema.Ref,
"resource": dvsResourceSchema.Ref,
},
},
}Now to be honest, I couldn't get the So that would be something like type DetailValueSpecFree struct{
Kind string `json:"contentType" enum:"free"`
}
type DetailValueSpecResource struct {
Kind string `json:"contentType" enum:"resource"`
AllowedResourceKinds []ResourceKind `json:"allowedResourceKinds"`
MustExist bool `json:"mustExist"`
}Anyway, if you manage to do it right, you'll get a dropdown to choose from in the OpenAPI doc. Mine looks like this: I've only been coding in Go for like 2 weeks (of which almost an entire day was wasted on exactly this) and it's 3AM right now so take everything I've said here with a grain of salt, but hopefully that helps! |
Beta Was this translation helpful? Give feedback.
-
|
Ok, so I stumbled upon this issue as well and managed to solve in to a satisfactory level, but it is by no means perfect. Multiple issues that prevent this from being solvable pretty:
Going by OpenAPI Spec the minimum requirements for using a discriminator to work are as follows: discriminator:
mapping:
foo: "#/components/schemas/Foo"
bar: "#/components/schemas/Bar"
propertyName: type
oneOf:
- $ref: "#/components/schemas/Foo"
- $ref: "#/components/schemas/Bar"Assume we are working with these types: type FooData struct {
Foo string `json:"foo"`
}
type BarData struct {
Bar string `json:"bar"`
}
type TypeData struct {
Type string `json:"type"`
*FooData
*BarData
}I needed a struct that I could embed, so I cannot use func (t TypeData) TransformSchema(r huma.Registry, s *huma.Schema) *huma.Schema {
ref := r.Schema(reflect.TypeOf(t), true, "").Ref
return &huma.Schema{
Discriminator: &huma.Discriminator{
PropertyName: "type",
Mapping: map[string]string{
"foo": ref,
"bar": ref,
},
},
OneOf: []*huma.Schema{
{Ref: ref},
},
}
}So first of all the fact that func (t TypeData) TransformSchema(r huma.Registry, s *huma.Schema) *huma.Schema {
baseRef := r.Schema(reflect.TypeOf(t), true, "").Ref
fooSchemaRef := deriveDiscriminatedSchema(r, *s, baseRef, "Foo", func(property string) bool {
return property != "bar"
})
barSchemaRef := deriveDiscriminatedSchema(r, *s, baseRef, "Bar", func(property string) bool {
return property != "foo"
})
return &huma.Schema{
Discriminator: &huma.Discriminator{
PropertyName: "type",
Mapping: map[string]string{
"foo": fooSchemaRef,
"bar": barSchemaRef,
},
},
OneOf: []*huma.Schema{
{Ref: fooSchemaRef},
{Ref: barSchemaRef},
},
}
}
func deriveDiscriminatedSchema(
registry huma.Registry,
schema huma.Schema,
baseRef string,
title string,
isIncluded func(property string) bool,
) string {
schema.Title = title
schema.Properties = copyMapKeyFiltered(schema.Properties, isIncluded)
schema.Required = copySliceFiltered(schema.Required, isIncluded)
schema.PrecomputeMessages()
ref := baseRef + "-" + title
registry.Map()[path.Base(ref)] = &schema
return ref
}
func copySliceFiltered[S interface{ ~[]E }, E any](source S, filter func(E) bool) S {
copied := make(S, 0, len(source))
for _, v := range source {
if filter(v) {
copied = append(copied, v)
}
}
return copied
}
func copyMapKeyFiltered[M interface{ ~map[K]V }, K comparable, V any](source M, filter func(K) bool) M {
copied := make(M, len(source))
for k, v := range source {
if filter(k) {
copied[k] = v
}
}
return copied
}Lastly, to make it possible to embed the type TypeData[T any] struct {
Type string `json:"type"`
*FooData
*BarData
}
func (t TypeData[T]) TransformSchema(r huma.Registry, s *huma.Schema) *huma.Schema {
baseRef := r.Schema(reflect.TypeFor[T](), true, "").Ref
fooSchemaRef := deriveDiscriminatedSchema(r, *s, baseRef, "Foo", func(property string) bool {
return property != "bar"
})
barSchemaRef := deriveDiscriminatedSchema(r, *s, baseRef, "Bar", func(property string) bool {
return property != "foo"
})
return &huma.Schema{
Discriminator: &huma.Discriminator{
PropertyName: "type",
Mapping: map[string]string{
"foo": fooSchemaRef,
"bar": barSchemaRef,
},
},
OneOf: []*huma.Schema{
{Ref: fooSchemaRef},
{Ref: barSchemaRef},
},
}
}Thus we can pass the top-level struct's type to this embedded struct, even multiple embeds deep: type FooBarBody struct {
FooBarModel[FooBarBody]
}
type FooBarModel[T any] struct {
TypeData[T]
}This would generate the following OpenAPI spec: discriminator:
mapping:
foo: "#/components/schemas/FooBarBody-Foo"
bar: "#/components/schemas/FooBarBody-Bar"
propertyName: type
oneOf:
- $ref: "#/components/schemas/FooBarBody-Foo"
- $ref: "#/components/schemas/FooBarBody-Bar"Assuming that Note that this solution might break other structs implementing Complete & Final Example Codeimport (
"path"
"reflect"
"github.com/danielgtaylor/huma/v2"
)
type FooBarBody struct {
FooBarModel[FooBarBody]
}
type FooBarModel[T any] struct {
TypeData[T]
}
type FooData struct {
Foo string `json:"foo"`
}
type BarData struct {
Bar string `json:"bar"`
}
type TypeData[T any] struct {
Type string `json:"type"`
*FooData
*BarData
}
func (t TypeData[T]) TransformSchema(r huma.Registry, s *huma.Schema) *huma.Schema {
baseRef := r.Schema(reflect.TypeFor[T](), true, "").Ref
fooSchemaRef := deriveDiscriminatedSchema(r, *s, baseRef, "Foo", func(property string) bool {
return property != "bar"
})
barSchemaRef := deriveDiscriminatedSchema(r, *s, baseRef, "Bar", func(property string) bool {
return property != "foo"
})
return &huma.Schema{
Discriminator: &huma.Discriminator{
PropertyName: "type",
Mapping: map[string]string{
"foo": fooSchemaRef,
"bar": barSchemaRef,
},
},
OneOf: []*huma.Schema{
{Ref: fooSchemaRef},
{Ref: barSchemaRef},
},
}
}
func deriveDiscriminatedSchema(
registry huma.Registry,
schema huma.Schema,
baseRef string,
title string,
isIncluded func(property string) bool,
) string {
schema.Title = title
schema.Properties = copyMapKeyFiltered(schema.Properties, isIncluded)
schema.Required = copySliceFiltered(schema.Required, isIncluded)
schema.PrecomputeMessages()
ref := baseRef + "-" + title
registry.Map()[path.Base(ref)] = &schema
return ref
}
func copySliceFiltered[S interface{ ~[]E }, E any](source S, filter func(E) bool) S {
copied := make(S, 0, len(source))
for _, v := range source {
if filter(v) {
copied = append(copied, v)
}
}
return copied
}
func copyMapKeyFiltered[M interface{ ~map[K]V }, K comparable, V any](source M, filter func(K) bool) M {
copied := make(M, len(source))
for k, v := range source {
if filter(k) {
copied[k] = v
}
}
return copied
} |
Beta Was this translation helpful? Give feedback.



Uh oh!
There was an error while loading. Please reload this page.
-
Hey!
I'm trying to define a
oneOfschema and I'm looking for advice on how best to proceed.Currently, in I have a
DetailValueSpecmarker interface that 2 types implement:In my app, I just type-switch on the implementations (first class unions in Golang when?!).
The concrete types (
DetailValueSpecFreeandDetailValueSpecResource) have a customMarshalJSON&UnmarshalJSONimplementation that looks for / injects akindproperty, so the JSON representation for them are:In terms of schema, I suppose the 'correct' way would be something like...
And then also defining a custom schema on the concrete types (to include the
kind)?I've quickly tested this, but because my
DetailValueSpecis part of another type, that type isn't resolving the custom schema.My other, simpler option, was to have the API accept in input type that encompasses both and just validate that only one is present.
But I feel like this still needs some kind of schema hint to show that only one is allowed, and should I be validating at the schema level (Transformer?) or just in my API?
Any suggestions would be great!
Beta Was this translation helpful? Give feedback.
All reactions