Skip to content

fix: pass along type_use annotations to swagger(-core)#3300

Open
tthornton3-chwy wants to merge 2 commits into
springdoc:mainfrom
tthornton3-chwy:bugfix/pass-along-type-use-annotations-to-swagger-core
Open

fix: pass along type_use annotations to swagger(-core)#3300
tthornton3-chwy wants to merge 2 commits into
springdoc:mainfrom
tthornton3-chwy:bugfix/pass-along-type-use-annotations-to-swagger-core

Conversation

@tthornton3-chwy

Copy link
Copy Markdown

Summary

Preserve top-level TYPE_USE annotations when Springdoc flattens @ParameterObject fields into query parameters.

Previously, GenericParameterService only forwarded annotations from parameterized type arguments. That meant annotations like JSpecify-style @nullable on an array type, e.g. Status @nullable [] status, were dropped before schema extraction. Swagger-core therefore never saw the nullable signal and generated only type: "array".
This change forwards both:

  • annotations on the field’s AnnotatedType itself
  • annotations on parameterized type arguments

That allows OpenAPI 3.1 nullable array query parameters to generate:
"type": ["array", "null"]

Change Plan

  • Updated GenericParameterService.resolveTypeAndTypeAnnotationsForParameter(...) to use a new helper that collects annotations from the full AnnotatedType.
  • Kept the existing parameterized type-argument annotation behavior intact.
  • Added a WebMVC OpenAPI 3.1 regression test app using a flattened @ParameterObject with a runtime TYPE_USE @nullable annotation on an array query parameter.
  • Added the expected JSON snapshot showing type: ["array", "null"] with enum array items.

Test Plan

Ran the test suite with the new tests.

* A parameter object whose {@code clinicId} field is reused as both an optional, nullable
* query parameter and a (required, non-null) path parameter, depending on the controller.
*/
class SearchCriteria {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What is the reason for wanting reuse this object in different contexts where @Nullable is not always correct? For the case where it is used in a path-parameter context, then the annotation misleads the consumer using the object, since the object says nullable but it actually is required?

Isn't the better approach to have an object specifically for the path scenario, so that it both documents the nullable correctly inwards and outwards?

@tthornton3-chwy tthornton3-chwy Jun 26, 2026

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

An example: we follow the CQRS-pattern, and have types that get deserialized for some queries. There's an "admin" path in, something like /admin/vet which can do whatever it wants (add a resource id as a query param, not add an id, put any id it wants). But there is also a "scoped" path, think of something like /resource/{resourceId}/vet that requires it (and authenticates that it is able to access said resourceId). They do the absolute same thing though, and come back to the same type. Sure, we could have an AdminSearchCriteria vs ScopedSearchCriteria, but now we have two methods in our service and gotta map one probably back to the other so we can stay DRY.
So the case where a PathParameter exists, is valid, then its required no matter what, and this helps us do that! Swagger Core didn't seem the right place to think about this, since that's a layer deeper than it probably should be. And with this I get both:

{
                        "name": "resourceId",
                        "in": "path",
                        "description": "Find something affiliated with something id.",
                        "required": true,
                        "schema": {
                            "type": "string",
                            "description": "Find Something affiliated with this something id.",
                            "example": 11111
                        },
                        "example": 1111
                    }

for the scoped and

                    {
                        "name": "resourceId",
                        "in": "query",
                        "description": "Find something affiliated with this something id.",
                        "required": false,
                        "schema": {
                            "type": [
                                "string",
                                "null"
                            ],
                            "description": "Find Something affiliated with this something id.",
                            "example": 11111
                        },
                        "example": 11111
                    },

which is exactly what we want :)

This use case may be semi-niche, but I don't think the case of forcing a path parameter to be required is niche at all!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants