huma
huma copied to clipboard
OneOf enlightenment needed
Hi, I'm trying to "fully" support an endpoint that may return a "OneOf" equivalent as response body.
I've looked a bit around the github, found some issue and a nice example - but they're a few limitations I've faced afterward:
- the response payload miss the
"$schema"field
response example
╰─ 16:10:01 ❯ http :8888/greeting/oneof ─╯
HTTP/1.1 200 OK
Content-Length: 28
Content-Type: application/json
Date: Mon, 17 Feb 2025 15:10:01 GMT
{
"message": "Hello, oneof!"
}
I've found some weird ways of "fixing" my issues, and I'd like help to get from "weird ways" to "expected ways" 🙈
I ended with the following piece of code to fix my issue 1 and 2.b:
diff to the example
--- example.go 2025-02-17 16:14:55.593638808 +0100
+++ mod_1.go 2025-02-17 16:14:38.690559026 +0100
@@ -57,10 +57,12 @@
// Create a schema for the output body.
registry := api.OpenAPI().Components.Schemas
+
+ schNew := registry.Schema(reflect.TypeOf(GreetingBody{}), true, "")
+ schOld := registry.Schema(reflect.TypeOf(GreetingBodyOld{}), true, "")
schema := &huma.Schema{
OneOf: []*huma.Schema{
- registry.Schema(reflect.TypeOf(GreetingBody{}), true, ""),
- registry.Schema(reflect.TypeOf(GreetingBodyOld{}), true, ""),
+ schNew, schOld,
},
}
@@ -76,6 +78,8 @@
"application/json": {
Schema: schema,
},
+ "old": {Schema: schOld},
+ "new": {Schema: schNew},
},
},
},
1: for some reason, referencingschOldandschNewas schema "as is" to a referencedhuma.MediaTypeenable the magic that inject the$schemafield in response of these struct
new response example
╰─ 16:36:46 ✘ 1 ❯ http :8888/greeting/oneof ─╯
HTTP/1.1 200 OK
Content-Length: 88
Content-Type: application/json
Date: Mon, 17 Feb 2025 15:36:46 GMT
Link: </schemas/GreetingBody.json>; rel="describedBy"
{
"$schema": "http://localhost:8888/schemas/GreetingBody.json",
"message": "Hello, oneof!"
}
2.b: the"old"and"new"key now exist in the UI, allowing me to inject more "context as doc"
Swagger UI screenshot
Little detours, If I now select the "old" or "new" content type in the swagger UI, the example is now cleared. As a "fix" I'm currently setting my MediaType.Example to some matching, json.RawMessage, but I would prefer for it to be auto-generated like usual response schema.
Regarding 2.a; I still haven't found a solution, so currently I have to access docs#/schemas/GreetingBodyOld to obtain a full doc of my response.
nb:\
- issue
1isn't that much of an issue, just my PTSD.\ - issue
2.ashall probably be opened on the swagger UI repo aka https://github.com/stoplightio/elements.\ - issue
2.bprobably has no solution as of today ?
I've also noticed another "issue" with schemas reached as 2.b.
The response example is only populated with example injected via the "example" annotation.\ As such, an array of object won't be populated with the object example !!
diff to examples/oneOf.go
--- example.go 2025-02-17 16:47:14.226102040 +0100
+++ mod_1.go 2025-02-17 17:29:04.292782002 +0100
@@ -38,9 +38,20 @@
Body any
}
+type Some struct {
+ SomeInt int `json:"some_int" example:"42"`
+ SomeString string `json:"some_string" example:"forty two"`
+ Nested []Child `json:"child"`
+}
+
+type Child struct {
+ ID int `json:"id" example:"420"`
+}
+
// GreetingBody is the body of the response for the latest version of the API.
type GreetingBody struct {
Message string `json:"message"`
+ Objects Some `json:"objects"`
}
// GreetingBodyOld is the body of the response for the old version of the API.
@@ -57,10 +68,12 @@
// Create a schema for the output body.
registry := api.OpenAPI().Components.Schemas
+
+ schNew := registry.Schema(reflect.TypeOf(GreetingBody{}), true, "")
+ schOld := registry.Schema(reflect.TypeOf(GreetingBodyOld{}), true, "")
schema := &huma.Schema{
OneOf: []*huma.Schema{
- registry.Schema(reflect.TypeOf(GreetingBody{}), true, ""),
- registry.Schema(reflect.TypeOf(GreetingBodyOld{}), true, ""),
+ schNew, schOld,
},
}
@@ -76,6 +89,8 @@
"application/json": {
Schema: schema,
},
+ "old": {Schema: schOld},
+ "new": {Schema: schNew},
},
},
},
swagger UI endpoint response example
swagger UI "raw" response example
You should definitely open an issue on the Stoplight Elements repo for the UI-related concerns. I don't have much control over that unfortunately!
As for the missing $schema that is something I can look into when I get a chance! In general though I would recommend avoiding complex oneOf schema situations if you can as it makes everything more difficult.
You should definitely open an issue on the Stoplight Elements repo for the UI-related concerns. I don't have much control over that unfortunately!
Seems issues related to oneOf are left as is on spotlight side..
redoc gets the job done, but it's pretty ugly and feature-less.
A good alternative would be scalar. And development seems fast on their side !
OneOf are nicely integrated; I can get ride of that "old/new as content-type" hack thanks to their UI.
Seems that multiple example are not supported (yet); I'll open an issue on their end.
huma wise, I now only needs to understand how to add the $schema entry to my payloads and I'll be good to go !
BTW I took a quick look at the schema generation code again and it currently has two constraints that prevent your example from working:
- It assumes an operation's request/response schema will be a
$refto#/components/schemas/... - Once the ref is resolved, it will only add a
$schemafield if the type isobject. For aoneOfthere is no type as it could be multiple types.
Even if I set a $ref to fix the first issue the second one remains for now.
The transformer could probably be modified to support oneOf by treating the response as two distinct types and adding $schema to each one (if they are object types) and then differentiating between the two when doing the response transform.
Edit: also yes Scalar rocks! I just can't change the default docs without potentially breaking a lot of people so for now you have to manually set it. I honestly thought Stoplight would be better supported long-term but since being bought I haven't seen a ton of progress on Stoplight Elements.
Cool thanks for looking after it ! I think I can live with it If I know there isn't a proper solution yet / never.
Feel free to close the issue