From c5b39260e84dedb54d2626f15ff85176f267be99 Mon Sep 17 00:00:00 2001 From: Michael Edgar Date: Mon, 8 Dec 2025 14:20:46 -0500 Subject: [PATCH] Skip defaulting schema type to `object` when `implementation` is void Signed-off-by: Michael Edgar --- .../runtime/io/schema/SchemaFactory.java | 2 +- .../scanner/OpenApiDataObjectScanner.java | 28 ++++++++++++- .../openapi/runtime/util/TypeUtil.java | 1 + .../scanner/StandaloneSchemaScanTest.java | 27 ++++++++++++ ...s.schemas.void-implementation-no-type.json | 42 +++++++++++++++++++ .../refsEnabled.kitchenSink.expected.json | 1 - 6 files changed, 98 insertions(+), 3 deletions(-) create mode 100644 core/src/test/resources/io/smallrye/openapi/runtime/scanner/components.schemas.void-implementation-no-type.json diff --git a/core/src/main/java/io/smallrye/openapi/runtime/io/schema/SchemaFactory.java b/core/src/main/java/io/smallrye/openapi/runtime/io/schema/SchemaFactory.java index 61e4bf668..961e5a6f5 100644 --- a/core/src/main/java/io/smallrye/openapi/runtime/io/schema/SchemaFactory.java +++ b/core/src/main/java/io/smallrye/openapi/runtime/io/schema/SchemaFactory.java @@ -521,7 +521,7 @@ static List wrapInList(T value) { * @param schemaReferenceSupported */ static Schema readClassSchema(final AnnotationScannerContext context, Type type, boolean schemaReferenceSupported) { - if (type == null) { + if (type == null || TypeUtil.isVoid(type)) { return null; } Schema schema; diff --git a/core/src/main/java/io/smallrye/openapi/runtime/scanner/OpenApiDataObjectScanner.java b/core/src/main/java/io/smallrye/openapi/runtime/scanner/OpenApiDataObjectScanner.java index 7ba8587be..9cdcd8156 100644 --- a/core/src/main/java/io/smallrye/openapi/runtime/scanner/OpenApiDataObjectScanner.java +++ b/core/src/main/java/io/smallrye/openapi/runtime/scanner/OpenApiDataObjectScanner.java @@ -261,7 +261,8 @@ private void depthFirstGraphSearch() { continue; } - if (!hasNonNullType(currentSchema)) { + if (!hasNonNullType(currentSchema) + && !isImplementationVoid(currentTarget, currentClass)) { // If not schema has yet been set, consider this an "object" SchemaSupport.setType(currentSchema, Schema.SchemaType.OBJECT); } else if (allowRegistration) { @@ -288,6 +289,31 @@ private void depthFirstGraphSearch() { } } + /** + * Determine if the {@code implementation} for the schema has been voided via an annotation, + * either on the target being scanned or the class itself + * + * @param target annotated target, possible null + * @param clazz target class that may be hosting a {@literal @}Schema annotation + */ + private boolean isImplementationVoid(AnnotationTarget target, ClassInfo clazz) { + for (AnnotationTarget t : Arrays.asList(target, clazz)) { + Type type = context.annotations() + .getAnnotationValue(t, SchemaConstant.DOTNAME_SCHEMA, + SchemaConstant.PROP_IMPLEMENTATION); + + if (TypeUtil.isVoid(type)) { + return true; + } + + if (type != null) { + return false; + } + } + + return false; + } + private void maybeRegisterSchema(Type currentType, Schema currentSchema, Schema entrySchema) { Schema ref = SchemaFactory.schemaRegistration(context, currentType, currentSchema); Schema.SchemaType type = SchemaSupport.getNonNullType(currentSchema); diff --git a/core/src/main/java/io/smallrye/openapi/runtime/util/TypeUtil.java b/core/src/main/java/io/smallrye/openapi/runtime/util/TypeUtil.java index c219bc00b..330e97956 100644 --- a/core/src/main/java/io/smallrye/openapi/runtime/util/TypeUtil.java +++ b/core/src/main/java/io/smallrye/openapi/runtime/util/TypeUtil.java @@ -113,6 +113,7 @@ public class TypeUtil { // https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.1.0.md#dataTypeFormat static { TYPE_MAP.put(DOTNAME_OBJECT, ANY); + TYPE_MAP.put(DOTNAME_VOID, ANY); // String TYPE_MAP.put(DotName.createSimple(String.class.getName()), STRING_FORMAT); diff --git a/core/src/test/java/io/smallrye/openapi/runtime/scanner/StandaloneSchemaScanTest.java b/core/src/test/java/io/smallrye/openapi/runtime/scanner/StandaloneSchemaScanTest.java index c0a43f9fd..7e6c840f2 100644 --- a/core/src/test/java/io/smallrye/openapi/runtime/scanner/StandaloneSchemaScanTest.java +++ b/core/src/test/java/io/smallrye/openapi/runtime/scanner/StandaloneSchemaScanTest.java @@ -1084,4 +1084,31 @@ class DataList extends GenericListRoot { assertJsonEquals("components.schemas.resolved-mapped-generic-ptype.json", Bean.class, GenericRoot.class, GenericListRoot.class, Data.class, DataList.class); } + + static final class TestVoidImplementationNoType { + @Schema(oneOf = { Implementation.class, String.class }, implementation = Void.class) + interface IFace1 { + } + + interface IFace2 { + } + + @Schema(additionalProperties = Schema.False.class) + static class Implementation implements IFace1 { + @Schema(required = true, description = "Identifier") + String id; + @Schema(defaultValue = "1", minimum = "1", description = "Sequence of this implemenatation instance") + long sequence; + @Schema(oneOf = { Implementation.class, String.class }, implementation = Void.class) + IFace2 oneOfIface2; + @Schema(implementation = String.class) + IFace2 stringIface2; + } + } + + @Test + void testVoidImplementationNoType() throws IOException, JSONException { + assertJsonEquals("components.schemas.void-implementation-no-type.json", + TestVoidImplementationNoType.IFace1.class, TestVoidImplementationNoType.Implementation.class); + } } diff --git a/core/src/test/resources/io/smallrye/openapi/runtime/scanner/components.schemas.void-implementation-no-type.json b/core/src/test/resources/io/smallrye/openapi/runtime/scanner/components.schemas.void-implementation-no-type.json new file mode 100644 index 000000000..59ca4d193 --- /dev/null +++ b/core/src/test/resources/io/smallrye/openapi/runtime/scanner/components.schemas.void-implementation-no-type.json @@ -0,0 +1,42 @@ +{ + "openapi" : "3.1.0", + "components" : { + "schemas" : { + "IFace1" : { + "oneOf" : [ { + "$ref" : "#/components/schemas/Implementation" + }, { + "type" : "string" + } ] + }, + "Implementation" : { + "additionalProperties" : false, + "type" : "object", + "required" : [ "id" ], + "properties" : { + "id" : { + "type" : "string", + "description" : "Identifier" + }, + "sequence" : { + "type" : "integer", + "format" : "int64", + "minimum" : 1, + "description" : "Sequence of this implemenatation instance", + "default" : 1 + }, + "oneOfIface2" : { + "oneOf" : [ { + "$ref" : "#/components/schemas/Implementation" + }, { + "type" : "string" + } ] + }, + "stringIface2" : { + "type" : "string" + } + } + } + } + } +} diff --git a/extension-jaxrs/src/test/resources/io/smallrye/openapi/runtime/scanner/refsEnabled.kitchenSink.expected.json b/extension-jaxrs/src/test/resources/io/smallrye/openapi/runtime/scanner/refsEnabled.kitchenSink.expected.json index d5f55cd1a..4e7c6767e 100644 --- a/extension-jaxrs/src/test/resources/io/smallrye/openapi/runtime/scanner/refsEnabled.kitchenSink.expected.json +++ b/extension-jaxrs/src/test/resources/io/smallrye/openapi/runtime/scanner/refsEnabled.kitchenSink.expected.json @@ -502,7 +502,6 @@ } }, "voidField": { - "type": "object" }, "writeOnlyInteger": { "format": "int32",