Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

OneOf enlightenment needed #730

Open
burgesQ opened this issue Feb 17, 2025 · 5 comments
Open

OneOf enlightenment needed #730

burgesQ opened this issue Feb 17, 2025 · 5 comments
Labels
question Further information is requested

Comments

@burgesQ
Copy link

burgesQ commented Feb 17, 2025

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:

  1. 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!"
}
2) the swagger UI poorly support the `oneOf` schema: 2.a) response example isn't dynamically reloaded on schema change 2.b) schema selection doesn't allow nicer documentation

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, referencing schOld and schNew as schema "as is" to a referenced huma.MediaType enable the magic that inject the $schema field 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

Image

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:\

@burgesQ
Copy link
Author

burgesQ commented Feb 17, 2025

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

Image

swagger UI "raw" response example

Image

@danielgtaylor danielgtaylor added the question Further information is requested label Feb 18, 2025
@danielgtaylor
Copy link
Owner

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.

@burgesQ
Copy link
Author

burgesQ commented Feb 20, 2025

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.

Image

huma wise, I now only needs to understand how to add the $schema entry to my payloads and I'll be good to go !

@danielgtaylor
Copy link
Owner

danielgtaylor commented Feb 20, 2025

BTW I took a quick look at the schema generation code again and it currently has two constraints that prevent your example from working:

  1. It assumes an operation's request/response schema will be a $ref to #/components/schemas/...
  2. Once the ref is resolved, it will only add a $schema field if the type is object. For a oneOf there 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.

@burgesQ
Copy link
Author

burgesQ commented Feb 25, 2025

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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
question Further information is requested
Projects
None yet
Development

No branches or pull requests

2 participants