From 596a0b0dfc863ca642dc54f607845a479dcd7f0d Mon Sep 17 00:00:00 2001 From: Peter Zaoral Date: Tue, 16 Jun 2026 17:19:27 +0200 Subject: [PATCH 1/3] @BeanParam enum field losing type and enum when @Schema(defaultValue) is set * merge type, enum and format from the Java field type into the existing partial schema when it has no type set Closes: #2595 Signed-off-by: Peter Zaoral --- .../spi/AbstractParameterProcessor.java | 27 ++++++++++- .../runtime/scanner/ParameterScanTests.java | 25 ++++++++++ .../params.beanparam-enum-default-value.json | 47 +++++++++++++++++++ 3 files changed, 98 insertions(+), 1 deletion(-) create mode 100644 extension-jaxrs/src/test/resources/io/smallrye/openapi/runtime/scanner/params.beanparam-enum-default-value.json diff --git a/core/src/main/java/io/smallrye/openapi/runtime/scanner/spi/AbstractParameterProcessor.java b/core/src/main/java/io/smallrye/openapi/runtime/scanner/spi/AbstractParameterProcessor.java index 04544dd1b..415c376e7 100644 --- a/core/src/main/java/io/smallrye/openapi/runtime/scanner/spi/AbstractParameterProcessor.java +++ b/core/src/main/java/io/smallrye/openapi/runtime/scanner/spi/AbstractParameterProcessor.java @@ -688,7 +688,32 @@ void mapParameterStyle(Parameter param, ParameterContext context) { } void mapParameterSchema(Parameter param, ParameterContext context) { - if (ModelUtil.parameterHasSchema(param) || context.targetType == null) { + if (context.targetType == null) { + return; + } + + if (ModelUtil.parameterHasSchema(param)) { + Schema existingSchema = param.getSchema(); + if (existingSchema != null && existingSchema.getRef() == null && existingSchema.getType() == null) { + Schema typeSchema = SchemaFactory.typeToSchema(scannerContext, context.targetType, null); + if (typeSchema != null) { + if (typeSchema.getRef() != null) { + Schema resolved = ModelUtil.getComponent(scannerContext.getOpenApi(), typeSchema.getRef()); + if (resolved != null) { + typeSchema = resolved; + } + } + if (typeSchema.getType() != null) { + existingSchema.setType(typeSchema.getType()); + } + if (existingSchema.getEnumeration() == null && typeSchema.getEnumeration() != null) { + existingSchema.setEnumeration(typeSchema.getEnumeration()); + } + if (existingSchema.getFormat() == null && typeSchema.getFormat() != null) { + existingSchema.setFormat(typeSchema.getFormat()); + } + } + } return; } diff --git a/extension-jaxrs/src/test/java/io/smallrye/openapi/runtime/scanner/ParameterScanTests.java b/extension-jaxrs/src/test/java/io/smallrye/openapi/runtime/scanner/ParameterScanTests.java index 5d90c6f65..66d290cfc 100644 --- a/extension-jaxrs/src/test/java/io/smallrye/openapi/runtime/scanner/ParameterScanTests.java +++ b/extension-jaxrs/src/test/java/io/smallrye/openapi/runtime/scanner/ParameterScanTests.java @@ -163,6 +163,31 @@ void testJakartaParameterInBeanFromField() throws IOException, JSONException { test.io.smallrye.openapi.runtime.scanner.Widget.class); } + @Test + void testBeanParamEnumDefaultValue() throws IOException, JSONException { + class QueryOptions { + @jakarta.ws.rs.QueryParam("sortOrder") + @Parameter(description = "Sort direction", schema = @Schema(defaultValue = "ASC")) + SortOrder sortOrder; + } + + @jakarta.ws.rs.Path("/beanparam-enum-default") + class ListResource { + @jakarta.ws.rs.GET + @jakarta.ws.rs.Produces(jakarta.ws.rs.core.MediaType.APPLICATION_JSON) + public String list(@jakarta.ws.rs.BeanParam QueryOptions options) { + return "ok"; + } + } + + test("params.beanparam-enum-default-value.json", SortOrder.class, QueryOptions.class, ListResource.class); + } + + enum SortOrder { + ASC, + DESC + } + @Test void testJavaxParameterInBeanFromSetter() throws IOException, JSONException { test("params.parameter-in-bean-from-setter.json", diff --git a/extension-jaxrs/src/test/resources/io/smallrye/openapi/runtime/scanner/params.beanparam-enum-default-value.json b/extension-jaxrs/src/test/resources/io/smallrye/openapi/runtime/scanner/params.beanparam-enum-default-value.json new file mode 100644 index 000000000..386fdb21f --- /dev/null +++ b/extension-jaxrs/src/test/resources/io/smallrye/openapi/runtime/scanner/params.beanparam-enum-default-value.json @@ -0,0 +1,47 @@ +{ + "openapi": "3.1.0", + "paths": { + "/beanparam-enum-default": { + "get": { + "parameters": [ + { + "name": "sortOrder", + "in": "query", + "description": "Sort direction", + "schema": { + "default": "ASC", + "enum": [ + "ASC", + "DESC" + ], + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "type": "string" + } + } + } + } + } + } + } + }, + "components": { + "schemas": { + "SortOrder": { + "enum": [ + "ASC", + "DESC" + ], + "type": "string" + } + } + } +} From f3be32d5c0a88a3dc03ddc1f0fd33482fb4a42d6 Mon Sep 17 00:00:00 2001 From: Peter Zaoral Date: Wed, 1 Jul 2026 13:40:39 +0200 Subject: [PATCH 2/3] @BeanParam enum field losing type and enum when @Schema(defaultValue) is set * merge type, enum and format from the Java field type into the existing partial schema when it has no type set Closes: #2595 Signed-off-by: Peter Zaoral --- .../scanner/spi/AbstractParameterProcessor.java | 16 +++------------- .../params.beanparam-enum-default-value.json | 6 +----- 2 files changed, 4 insertions(+), 18 deletions(-) diff --git a/core/src/main/java/io/smallrye/openapi/runtime/scanner/spi/AbstractParameterProcessor.java b/core/src/main/java/io/smallrye/openapi/runtime/scanner/spi/AbstractParameterProcessor.java index 415c376e7..350aded48 100644 --- a/core/src/main/java/io/smallrye/openapi/runtime/scanner/spi/AbstractParameterProcessor.java +++ b/core/src/main/java/io/smallrye/openapi/runtime/scanner/spi/AbstractParameterProcessor.java @@ -698,19 +698,9 @@ void mapParameterSchema(Parameter param, ParameterContext context) { Schema typeSchema = SchemaFactory.typeToSchema(scannerContext, context.targetType, null); if (typeSchema != null) { if (typeSchema.getRef() != null) { - Schema resolved = ModelUtil.getComponent(scannerContext.getOpenApi(), typeSchema.getRef()); - if (resolved != null) { - typeSchema = resolved; - } - } - if (typeSchema.getType() != null) { - existingSchema.setType(typeSchema.getType()); - } - if (existingSchema.getEnumeration() == null && typeSchema.getEnumeration() != null) { - existingSchema.setEnumeration(typeSchema.getEnumeration()); - } - if (existingSchema.getFormat() == null && typeSchema.getFormat() != null) { - existingSchema.setFormat(typeSchema.getFormat()); + existingSchema.setRef(typeSchema.getRef()); + } else { + mergeObjects(existingSchema, typeSchema); } } } diff --git a/extension-jaxrs/src/test/resources/io/smallrye/openapi/runtime/scanner/params.beanparam-enum-default-value.json b/extension-jaxrs/src/test/resources/io/smallrye/openapi/runtime/scanner/params.beanparam-enum-default-value.json index 386fdb21f..1d24e2612 100644 --- a/extension-jaxrs/src/test/resources/io/smallrye/openapi/runtime/scanner/params.beanparam-enum-default-value.json +++ b/extension-jaxrs/src/test/resources/io/smallrye/openapi/runtime/scanner/params.beanparam-enum-default-value.json @@ -10,11 +10,7 @@ "description": "Sort direction", "schema": { "default": "ASC", - "enum": [ - "ASC", - "DESC" - ], - "type": "string" + "$ref": "#/components/schemas/SortOrder" } } ], From 4ab04787edbe41c00ae7f39507477b76636c431d Mon Sep 17 00:00:00 2001 From: Michael Edgar Date: Sun, 5 Jul 2026 09:53:59 -0400 Subject: [PATCH 3/3] Handle multi-variant parameter schemas, add test scenarios Signed-off-by: Michael Edgar --- .../spi/AbstractParameterProcessor.java | 50 ++++-- .../openapi/runtime/util/ModelUtil.java | 88 +++++++++-- .../runtime/scanner/ParameterScanTests.java | 32 +++- .../params.beanparam-enum-default-value.json | 143 ++++++++++++++---- 4 files changed, 253 insertions(+), 60 deletions(-) diff --git a/core/src/main/java/io/smallrye/openapi/runtime/scanner/spi/AbstractParameterProcessor.java b/core/src/main/java/io/smallrye/openapi/runtime/scanner/spi/AbstractParameterProcessor.java index 350aded48..4fc5d6c7a 100644 --- a/core/src/main/java/io/smallrye/openapi/runtime/scanner/spi/AbstractParameterProcessor.java +++ b/core/src/main/java/io/smallrye/openapi/runtime/scanner/spi/AbstractParameterProcessor.java @@ -49,6 +49,7 @@ import io.smallrye.openapi.api.util.MergeUtil; import io.smallrye.openapi.internal.models.media.SchemaSupport; +import io.smallrye.openapi.model.BaseModel; import io.smallrye.openapi.model.Extensions; import io.smallrye.openapi.runtime.io.Names; import io.smallrye.openapi.runtime.io.schema.SchemaConstant; @@ -693,17 +694,7 @@ void mapParameterSchema(Parameter param, ParameterContext context) { } if (ModelUtil.parameterHasSchema(param)) { - Schema existingSchema = param.getSchema(); - if (existingSchema != null && existingSchema.getRef() == null && existingSchema.getType() == null) { - Schema typeSchema = SchemaFactory.typeToSchema(scannerContext, context.targetType, null); - if (typeSchema != null) { - if (typeSchema.getRef() != null) { - existingSchema.setRef(typeSchema.getRef()); - } else { - mergeObjects(existingSchema, typeSchema); - } - } - } + mergeParameterTypeSchema(param, context); return; } @@ -735,6 +726,43 @@ void mapParameterSchema(Parameter param, ParameterContext context) { ModelUtil.setParameterSchema(param, schema); } + private void mergeParameterTypeSchema(Parameter param, ParameterContext context) { + Map paramSchemas = ModelUtil.getParameterMediaTypeSchemas(param); + Map scannableParamSchemas = new HashMap<>(paramSchemas.size()); + + for (var entry : paramSchemas.entrySet()) { + if (entry.getValue().getRef() == null && entry.getValue().getType() == null) { + scannableParamSchemas.put(entry.getKey(), entry.getValue()); + } + } + + if (!scannableParamSchemas.isEmpty()) { + Schema typeSchema = SchemaFactory.typeToSchema(scannerContext, context.targetType, null); + + if (typeSchema == null) { + // Hidden schema + return; + } + + for (var entry : scannableParamSchemas.entrySet()) { + var paramSchema = entry.getValue(); + + if (typeSchema.getRef() != null) { + paramSchema.setRef(typeSchema.getRef()); + } else { + /* + * The type schema must be cloned before using it as the target of an object merge. + * Merging here will overlay the parameter schema's attributes over the type schema's + * attributes before replacing the schema in the parameter model. + */ + typeSchema = BaseModel.deepCopy(typeSchema, Schema.class); + paramSchema = mergeObjects(typeSchema, paramSchema); + ModelUtil.setParameterMediaTypeSchema(param, entry.getKey(), paramSchema); + } + } + } + } + void mapParameterExtensions(Parameter param, ParameterContext context) { if (param.getExtensions() == null) { param.setExtensions(scannerContext.io().extensionIO().readMap(context.target)); diff --git a/core/src/main/java/io/smallrye/openapi/runtime/util/ModelUtil.java b/core/src/main/java/io/smallrye/openapi/runtime/util/ModelUtil.java index 64bcf6a8a..18b565439 100644 --- a/core/src/main/java/io/smallrye/openapi/runtime/util/ModelUtil.java +++ b/core/src/main/java/io/smallrye/openapi/runtime/util/ModelUtil.java @@ -1,8 +1,9 @@ package io.smallrye.openapi.runtime.util; import java.util.ArrayList; -import java.util.Arrays; import java.util.Collections; +import java.util.HashMap; +import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.function.Supplier; @@ -219,6 +220,59 @@ public static boolean parameterHasSchema(Parameter parameter) { return false; } + /** + * Returns the map of {@link Schema}s defined for the given + * {@link Parameter} with the schema's content type as the key (possibly + * null for the {@linkplain Parameter#getSchema() schema defined directly on + * the parameter}. + * + * A schema can be defined either via the parameter's "schema" property, or + * any "content.*.schema" property. + * + * @param parameter + * Parameter + * @return map of schemas by content type, never null + */ + public static Map getParameterMediaTypeSchemas(Parameter parameter) { + Map mediaTypeSchemas; + + if (parameter.getSchema() != null) { + mediaTypeSchemas = new HashMap<>(1); + mediaTypeSchemas.put(null, parameter.getSchema()); + } else { + Map mediaTypes = getMediaTypesOrEmpty(parameter.getContent()); + mediaTypeSchemas = new LinkedHashMap<>(mediaTypes.size()); + + for (var entry : mediaTypes.entrySet()) { + mediaTypeSchemas.put(entry.getKey(), entry.getValue().getSchema()); + } + } + + return mediaTypeSchemas; + } + + /** + * Set a schema to the parameter for the given content type. When the + * content type is null, the schema is set + * {@linkplain Parameter#setSchema(Schema) directly on the schema}. + * Otherwise, the schema will be set in the parameter's content on a new or + * existing {@link MediaType} using content type as its key. + * + * @param parameter + * The parameter + * @param contentType + * The content type to which the schema applies (null allowed) + * @param schema + * The schema + */ + public static void setParameterMediaTypeSchema(Parameter parameter, String contentType, Schema schema) { + if (contentType == null) { + parameter.setSchema(schema); + } else { + parameter.setContent(putMediaTypeSchema(parameter.getContent(), contentType, schema)); + } + } + /** * Returns the list of {@link Schema}s defined for the given {@link Parameter}. * A schema can be defined either via the parameter's "schema" property, or any @@ -228,20 +282,7 @@ public static boolean parameterHasSchema(Parameter parameter) { * @return list of schemas, never null */ public static List getParameterSchemas(Parameter parameter) { - if (parameter.getSchema() != null) { - return Arrays.asList(parameter.getSchema()); - } - Map mediaTypes = getMediaTypesOrEmpty(parameter.getContent()); - if (!mediaTypes.isEmpty()) { - List schemas = new ArrayList<>(mediaTypes.size()); - - for (MediaType mediaType : mediaTypes.values()) { - if (mediaType.getSchema() != null) { - schemas.add(mediaType.getSchema()); - } - } - } - return Collections.emptyList(); + return new ArrayList<>(getParameterMediaTypeSchemas(parameter).values()); } /** @@ -341,6 +382,23 @@ static Map getMediaTypesOrEmpty(Content content) { return Collections.emptyMap(); } + static Content putMediaTypeSchema(Content content, String contentType, Schema schema) { + if (content == null) { + content = OASFactory.createContent(); + } + + MediaType mediaType = content.getMediaType(contentType); + + if (mediaType == null) { + mediaType = OASFactory.createMediaType(); + content.addMediaType(contentType, mediaType); + } + + mediaType.setSchema(schema); + + return content; + } + /** * Returns the name component of the ref. * diff --git a/extension-jaxrs/src/test/java/io/smallrye/openapi/runtime/scanner/ParameterScanTests.java b/extension-jaxrs/src/test/java/io/smallrye/openapi/runtime/scanner/ParameterScanTests.java index 66d290cfc..2290c09fe 100644 --- a/extension-jaxrs/src/test/java/io/smallrye/openapi/runtime/scanner/ParameterScanTests.java +++ b/extension-jaxrs/src/test/java/io/smallrye/openapi/runtime/scanner/ParameterScanTests.java @@ -171,16 +171,44 @@ class QueryOptions { SortOrder sortOrder; } + @Schema(name = "filter-option-a", ref = "http://example.com/schemas/filter-schema-a.json") + class FilterOptionA { + } + + class FilterOptionB { + } + + @Schema(hidden = true) + class SecretIdentifier { + } + @jakarta.ws.rs.Path("/beanparam-enum-default") class ListResource { @jakarta.ws.rs.GET @jakarta.ws.rs.Produces(jakarta.ws.rs.core.MediaType.APPLICATION_JSON) - public String list(@jakarta.ws.rs.BeanParam QueryOptions options) { + public String list( + @jakarta.ws.rs.BeanParam QueryOptions options, + @jakarta.ws.rs.QueryParam("altSortOrder") @Parameter(description = "Alternate Sort direction", schema = @Schema(implementation = String.class, enumeration = { + "asc", "desc" }, defaultValue = "desc")) SortOrder sortOrder, + @jakarta.ws.rs.QueryParam("filter1") @Parameter(description = "Filter 1", schema = @Schema(description = "Standard filter schema")) FilterOptionA filter1, + @jakarta.ws.rs.QueryParam("filter2") @Parameter(description = "Filter 2", schema = @Schema(ref = "http://example.com/schemas/filter-schema-b.json", description = "Another filter")) FilterOptionA filter2, + @jakarta.ws.rs.QueryParam("filter3") @Parameter(description = "Filter 3 with multiple variants", content = { + @Content(mediaType = "text/plain", schema = @Schema(description = "Third filter in plain text format")), + @Content(mediaType = "application/json", schema = @Schema(description = "Third filter in JSON format")), + }) FilterOptionA multivariantFilter3, + @jakarta.ws.rs.QueryParam("request-id") @Parameter(description = "Identifier for the request", schema = @Schema(format = "custom-id")) Integer requestId, + @jakarta.ws.rs.QueryParam("secret-id") @Parameter(description = "Secret identifier for the request", schema = @Schema(format = "secret-id", description = "The schema for this parameter is not disclosed")) SecretIdentifier secretId, + @jakarta.ws.rs.QueryParam("variable-id") @Parameter(description = "Identifier with multiple variants", content = { + @Content(mediaType = "text/x-custom-type1", schema = @Schema(minimum = "1", maximum = "50")), + @Content(mediaType = "text/x-custom-type2", schema = @Schema(minimum = "51", maximum = "99")), + }) Long multivariantIdentifier) { return "ok"; } } - test("params.beanparam-enum-default-value.json", SortOrder.class, QueryOptions.class, ListResource.class); + test("params.beanparam-enum-default-value.json", SortOrder.class, FilterOptionA.class, FilterOptionB.class, + SecretIdentifier.class, + QueryOptions.class, ListResource.class); } enum SortOrder { diff --git a/extension-jaxrs/src/test/resources/io/smallrye/openapi/runtime/scanner/params.beanparam-enum-default-value.json b/extension-jaxrs/src/test/resources/io/smallrye/openapi/runtime/scanner/params.beanparam-enum-default-value.json index 1d24e2612..972429039 100644 --- a/extension-jaxrs/src/test/resources/io/smallrye/openapi/runtime/scanner/params.beanparam-enum-default-value.json +++ b/extension-jaxrs/src/test/resources/io/smallrye/openapi/runtime/scanner/params.beanparam-enum-default-value.json @@ -1,26 +1,116 @@ { - "openapi": "3.1.0", - "paths": { - "/beanparam-enum-default": { - "get": { - "parameters": [ - { - "name": "sortOrder", - "in": "query", - "description": "Sort direction", - "schema": { - "default": "ASC", - "$ref": "#/components/schemas/SortOrder" + "openapi" : "3.1.0", + "components" : { + "schemas" : { + "SortOrder" : { + "type" : "string", + "enum" : [ "ASC", "DESC" ] + }, + "filter-option-a" : { + "$ref" : "http://example.com/schemas/filter-schema-a.json" + } + } + }, + "paths" : { + "/beanparam-enum-default" : { + "get" : { + "parameters" : [ { + "description" : "Alternate Sort direction", + "schema" : { + "type" : "string", + "default" : "desc", + "enum" : [ "asc", "desc" ] + }, + "name" : "altSortOrder", + "in" : "query" + }, { + "description" : "Filter 1", + "schema" : { + "description" : "Standard filter schema", + "$ref" : "#/components/schemas/filter-option-a" + }, + "name" : "filter1", + "in" : "query" + }, { + "description" : "Filter 2", + "schema" : { + "$ref" : "http://example.com/schemas/filter-schema-b.json", + "description" : "Another filter" + }, + "name" : "filter2", + "in" : "query" + }, { + "content" : { + "text/plain" : { + "schema" : { + "description" : "Third filter in plain text format", + "$ref" : "#/components/schemas/filter-option-a" + } + }, + "application/json" : { + "schema" : { + "description" : "Third filter in JSON format", + "$ref" : "#/components/schemas/filter-option-a" + } } - } - ], - "responses": { - "200": { - "description": "OK", - "content": { - "application/json": { - "schema": { - "type": "string" + }, + "description" : "Filter 3 with multiple variants", + "name" : "filter3", + "in" : "query" + }, { + "description" : "Identifier for the request", + "schema" : { + "type" : "integer", + "format" : "custom-id" + }, + "name" : "request-id", + "in" : "query" + }, { + "description" : "Secret identifier for the request", + "schema" : { + "description" : "The schema for this parameter is not disclosed", + "format" : "secret-id" + }, + "name" : "secret-id", + "in" : "query" + }, { + "description" : "Sort direction", + "schema" : { + "default" : "ASC", + "$ref" : "#/components/schemas/SortOrder" + }, + "name" : "sortOrder", + "in" : "query" + }, { + "content" : { + "text/x-custom-type1" : { + "schema" : { + "type" : "integer", + "format" : "int64", + "maximum" : 50, + "minimum" : 1 + } + }, + "text/x-custom-type2" : { + "schema" : { + "type" : "integer", + "format" : "int64", + "maximum" : 99, + "minimum" : 51 + } + } + }, + "description" : "Identifier with multiple variants", + "name" : "variable-id", + "in" : "query" + } ], + "responses" : { + "200" : { + "description" : "OK", + "content" : { + "application/json" : { + "schema" : { + "type" : "string" } } } @@ -28,16 +118,5 @@ } } } - }, - "components": { - "schemas": { - "SortOrder": { - "enum": [ - "ASC", - "DESC" - ], - "type": "string" - } - } } }