From f3a9fe853771e4e6c29a4d02ed3d20329f026a55 Mon Sep 17 00:00:00 2001 From: Ricardo Zanini Date: Fri, 13 Jun 2025 18:57:12 -0400 Subject: [PATCH 1/3] Fix #1185 - Add additionalProperties when type object has default value and no props Signed-off-by: Ricardo Zanini --- .../codegen/OpenApiGeneratorCodeGenBase.java | 13 +-- .../codegen/OpenApiGeneratorOutputPaths.java | 3 +- .../wrapper/QuarkusJavaClientCodegen.java | 14 +++ .../OpenApiClientGeneratorWrapperTest.java | 24 +++++ .../test/resources/openapi/issue-1185.json | 89 +++++++++++++++++++ ...arkus-openapi-generator-moqu-wiremock.adoc | 2 +- ...qu-wiremock_quarkus.openapi-generator.adoc | 2 +- 7 files changed, 134 insertions(+), 13 deletions(-) create mode 100644 client/deployment/src/test/resources/openapi/issue-1185.json diff --git a/client/deployment/src/main/java/io/quarkiverse/openapi/generator/deployment/codegen/OpenApiGeneratorCodeGenBase.java b/client/deployment/src/main/java/io/quarkiverse/openapi/generator/deployment/codegen/OpenApiGeneratorCodeGenBase.java index 34ad096ec..809e0101e 100644 --- a/client/deployment/src/main/java/io/quarkiverse/openapi/generator/deployment/codegen/OpenApiGeneratorCodeGenBase.java +++ b/client/deployment/src/main/java/io/quarkiverse/openapi/generator/deployment/codegen/OpenApiGeneratorCodeGenBase.java @@ -473,12 +473,10 @@ private Optional getConfigKeyValues(final Config config, final Path openA Class propertyType) { Optional possibleConfigKey = getConfigKeyValue(config, openApiFilePath); - if (possibleConfigKey.isPresent()) { - return getValuesByConfigKey(config, CodegenConfig.getSpecConfigNameByConfigKey(possibleConfigKey.get(), configName), - propertyType, configName); - } + return possibleConfigKey + .flatMap(s -> getValuesByConfigKey(config, CodegenConfig.getSpecConfigNameByConfigKey(s, configName), + propertyType, configName)); - return Optional.empty(); } private Optional> getConfigKeyValues(final SmallRyeConfig config, final Path openApiFilePath, @@ -486,10 +484,7 @@ private Optional> getConfigKeyValues(final SmallRyeConfig confi Class kClass, Class vClass) { Optional possibleConfigKey = getConfigKeyValue(config, openApiFilePath); - if (possibleConfigKey.isPresent()) { - return getValuesByConfigKey(config, configName, kClass, vClass, possibleConfigKey.get()); - } + return possibleConfigKey.flatMap(s -> getValuesByConfigKey(config, configName, kClass, vClass, s)); - return Optional.empty(); } } diff --git a/client/deployment/src/main/java/io/quarkiverse/openapi/generator/deployment/codegen/OpenApiGeneratorOutputPaths.java b/client/deployment/src/main/java/io/quarkiverse/openapi/generator/deployment/codegen/OpenApiGeneratorOutputPaths.java index 2bfc71f6a..1575a5e6f 100644 --- a/client/deployment/src/main/java/io/quarkiverse/openapi/generator/deployment/codegen/OpenApiGeneratorOutputPaths.java +++ b/client/deployment/src/main/java/io/quarkiverse/openapi/generator/deployment/codegen/OpenApiGeneratorOutputPaths.java @@ -2,7 +2,6 @@ import java.nio.file.Path; import java.util.ArrayList; -import java.util.Arrays; import java.util.Collection; import java.util.Iterator; import java.util.List; @@ -12,7 +11,7 @@ public class OpenApiGeneratorOutputPaths { public static final String OPENAPI_PATH = "open-api"; public static final String STREAM_PATH = "open-api-stream"; - private static final Collection rootPaths = Arrays.asList(STREAM_PATH); + private static final Collection rootPaths = List.of(STREAM_PATH); public static Path getRelativePath(Path path) { List paths = new ArrayList<>(); diff --git a/client/deployment/src/main/java/io/quarkiverse/openapi/generator/deployment/wrapper/QuarkusJavaClientCodegen.java b/client/deployment/src/main/java/io/quarkiverse/openapi/generator/deployment/wrapper/QuarkusJavaClientCodegen.java index cfe51cde9..ec4feaf17 100644 --- a/client/deployment/src/main/java/io/quarkiverse/openapi/generator/deployment/wrapper/QuarkusJavaClientCodegen.java +++ b/client/deployment/src/main/java/io/quarkiverse/openapi/generator/deployment/wrapper/QuarkusJavaClientCodegen.java @@ -148,6 +148,20 @@ public CodegenModel fromModel(String name, Schema model) { return codegenModel; } + @Override + public CodegenProperty fromProperty(String name, Schema p, boolean required, boolean schemaIsFromAdditionalProperties) { + if (p != null && p.getType() != null) { + // Property is a `type: object` without `additionalProperties: true`, without `properties`, but has `default` values set! + // In this peculiar situation, the template will try to initialize a Java Object with such values, and it will fail to compile. + // See https://github.com/quarkiverse/quarkus-openapi-generator/issues/1185 for more context. + if (p.getType().equals("object") && p.getDefault() != null && p.getAdditionalProperties() == null + && p.getItems() == null) { + p.setAdditionalProperties(true); + } + } + return super.fromProperty(name, p, required, schemaIsFromAdditionalProperties); + } + private void warnIfDuplicated(CodegenModel m) { Set propertyNames = new TreeSet<>(); for (CodegenProperty element : m.allVars) { diff --git a/client/deployment/src/test/java/io/quarkiverse/openapi/generator/deployment/wrapper/OpenApiClientGeneratorWrapperTest.java b/client/deployment/src/test/java/io/quarkiverse/openapi/generator/deployment/wrapper/OpenApiClientGeneratorWrapperTest.java index d4309c5c2..62dda7757 100644 --- a/client/deployment/src/test/java/io/quarkiverse/openapi/generator/deployment/wrapper/OpenApiClientGeneratorWrapperTest.java +++ b/client/deployment/src/test/java/io/quarkiverse/openapi/generator/deployment/wrapper/OpenApiClientGeneratorWrapperTest.java @@ -718,6 +718,30 @@ void verifyDynamicUrlAnnotation() throws Exception { } } + @Test + void verifyAllowAdditionalPropertiesIfNotPresent() throws Exception { + List generatedFiles = createGeneratorWrapperReactive("issue-1185.json") + .generate("org.issue1185") + .stream() + .filter(file -> file.getPath().endsWith("PictureDescriptionLocal.java")).toList(); + + assertThat(generatedFiles).isNotEmpty(); + // generationConfig + for (File file : generatedFiles) { + try { + Optional var = StaticJavaParser.parse(file) + .findAll(FieldDeclaration.class).stream() + .filter(c -> c.getVariable(0) + .getNameAsString().equals("generationConfig")) + .findFirst(); + assertThat(var).isPresent(); + assertThat(var.get().getElementType().asString()).contains("Map"); + } catch (FileNotFoundException e) { + throw new RuntimeException(e.getMessage()); + } + } + } + private List generateRestClientFiles() throws URISyntaxException { OpenApiClientGeneratorWrapper generatorWrapper = createGeneratorWrapper("simple-openapi.json").withCircuitBreakerConfig( Map.of("org.openapitools.client.api.DefaultApi", List.of("opThatDoesNotExist", "byeMethodGet"))); diff --git a/client/deployment/src/test/resources/openapi/issue-1185.json b/client/deployment/src/test/resources/openapi/issue-1185.json new file mode 100644 index 000000000..2e2c5ee47 --- /dev/null +++ b/client/deployment/src/test/resources/openapi/issue-1185.json @@ -0,0 +1,89 @@ +{ + "openapi": "3.1.0", + "info": { + "title": "Docling Serve – PictureDescriptionLocal", + "version": "0.13.0" + }, + "paths": { + "/v1alpha/convert/file": { + "post": { + "summary": "Process File with Local Picture Description", + "operationId": "processFileWithPictureDescriptionLocal", + "tags": ["Docling"], + "requestBody": { + "required": true, + "content": { + "multipart/form-data": { + "schema": { + "type": "object", + "properties": { + "files": { + "type": "array", + "items": { + "type": "string", + "format": "binary" + } + }, + "picture_description_local": { + "$ref": "#/components/schemas/PictureDescriptionLocal" + } + }, + "required": ["files", "picture_description_local"] + } + } + } + }, + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "type": "object" + } + } + } + } + } + } + } + }, + "components": { + "schemas": { + "PictureDescriptionLocal": { + "type": "object", + "title": "PictureDescriptionLocal", + "description": "Options for running a local vision-language model in the picture description. Parameters refer to a model hosted on Hugging Face.", + "properties": { + "repo_id": { + "type": "string", + "title": "Repo Id", + "description": "Repository ID on the Hugging Face Hub." + }, + "prompt": { + "type": "string", + "title": "Prompt", + "description": "Prompt used when calling the vision-language model.", + "default": "Describe this image in a few sentences." + }, + "generation_config": { + "type": "object", + "title": "Generation Config", + "description": "Config from Transformers’ GenerationConfig (e.g. max_new_tokens, do_sample).", + "default": { + "max_new_tokens": 200, + "do_sample": false + }, + "examples": [ + { + "do_sample": false, + "max_new_tokens": 200 + } + ] + } + }, + "required": ["repo_id"] + } + } + } +} diff --git a/docs/modules/ROOT/pages/includes/quarkus-openapi-generator-moqu-wiremock.adoc b/docs/modules/ROOT/pages/includes/quarkus-openapi-generator-moqu-wiremock.adoc index fe7a5ce9e..951657c31 100644 --- a/docs/modules/ROOT/pages/includes/quarkus-openapi-generator-moqu-wiremock.adoc +++ b/docs/modules/ROOT/pages/includes/quarkus-openapi-generator-moqu-wiremock.adoc @@ -15,7 +15,7 @@ endif::add-copy-button-to-config-props[] [.description] -- -Path to the Moqu (relative to the project). +Path to the Moqu OpenAPI files, relative to the `src/main/resources` directory. ifdef::add-copy-button-to-env-var[] diff --git a/docs/modules/ROOT/pages/includes/quarkus-openapi-generator-moqu-wiremock_quarkus.openapi-generator.adoc b/docs/modules/ROOT/pages/includes/quarkus-openapi-generator-moqu-wiremock_quarkus.openapi-generator.adoc index fe7a5ce9e..951657c31 100644 --- a/docs/modules/ROOT/pages/includes/quarkus-openapi-generator-moqu-wiremock_quarkus.openapi-generator.adoc +++ b/docs/modules/ROOT/pages/includes/quarkus-openapi-generator-moqu-wiremock_quarkus.openapi-generator.adoc @@ -15,7 +15,7 @@ endif::add-copy-button-to-config-props[] [.description] -- -Path to the Moqu (relative to the project). +Path to the Moqu OpenAPI files, relative to the `src/main/resources` directory. ifdef::add-copy-button-to-env-var[] From 824ad70dc8e7e5612941d4d937a9f4395e7c4e08 Mon Sep 17 00:00:00 2001 From: Ricardo Zanini Date: Fri, 13 Jun 2025 19:12:20 -0400 Subject: [PATCH 2/3] Fix Windows path Signed-off-by: Ricardo Zanini --- .../OpenApiClientGeneratorWrapperTest.java | 28 +++++++++++++++++-- 1 file changed, 25 insertions(+), 3 deletions(-) diff --git a/client/deployment/src/test/java/io/quarkiverse/openapi/generator/deployment/wrapper/OpenApiClientGeneratorWrapperTest.java b/client/deployment/src/test/java/io/quarkiverse/openapi/generator/deployment/wrapper/OpenApiClientGeneratorWrapperTest.java index 62dda7757..83f697594 100644 --- a/client/deployment/src/test/java/io/quarkiverse/openapi/generator/deployment/wrapper/OpenApiClientGeneratorWrapperTest.java +++ b/client/deployment/src/test/java/io/quarkiverse/openapi/generator/deployment/wrapper/OpenApiClientGeneratorWrapperTest.java @@ -9,11 +9,14 @@ import java.io.File; import java.io.FileNotFoundException; +import java.net.URI; import java.net.URISyntaxException; +import java.net.URL; import java.nio.file.Path; import java.nio.file.Paths; import java.util.List; import java.util.Map; +import java.util.Objects; import java.util.Optional; import java.util.stream.Collectors; @@ -760,15 +763,34 @@ private OpenApiClientGeneratorWrapper createGeneratorWrapper(String specFileName } private Path getOpenApiSpecPath(String specFileName) throws URISyntaxException { - return Path.of(requireNonNull(this.getClass().getResource(String.format("/openapi/%s", specFileName))).toURI()); + URL url = this.getClass().getResource("/openapi/" + specFileName); + Objects.requireNonNull(url, "Could not find /openapi/" + specFileName); + + URI uri; + try { + uri = url.toURI(); + } catch (URISyntaxException e) { + // this should never happen for a well-formed file URL + throw new RuntimeException("Invalid URI for " + url, e); + } + + return Paths.get(uri); } private Path getOpenApiTargetPath(Path openApiSpec) throws URISyntaxException { return Paths.get(getTargetDir(), "openapi-gen"); } - private String getTargetDir() throws URISyntaxException { - return Paths.get(requireNonNull(getClass().getResource("/")).toURI()).getParent().toString(); + private String getTargetDir() { + URL url = requireNonNull(getClass().getResource("/"), + "Could not locate classpath root"); + try { + return Paths.get(url.toURI()) + .getParent() + .toString(); + } catch (URISyntaxException e) { + throw new RuntimeException("Invalid URI for " + url, e); + } } private Optional findVariableByName(List fields, String name) { From b710270eb7ae19c50a41812543709a5af10bbc8b Mon Sep 17 00:00:00 2001 From: Ricardo Zanini <1538000+ricardozanini@users.noreply.github.com> Date: Mon, 16 Jun 2025 13:08:49 -0400 Subject: [PATCH 3/3] Adding Gonzalo's comment MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Gonzalo Muñoz --- .../generator/deployment/wrapper/QuarkusJavaClientCodegen.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/deployment/src/main/java/io/quarkiverse/openapi/generator/deployment/wrapper/QuarkusJavaClientCodegen.java b/client/deployment/src/main/java/io/quarkiverse/openapi/generator/deployment/wrapper/QuarkusJavaClientCodegen.java index ec4feaf17..b7daaa74a 100644 --- a/client/deployment/src/main/java/io/quarkiverse/openapi/generator/deployment/wrapper/QuarkusJavaClientCodegen.java +++ b/client/deployment/src/main/java/io/quarkiverse/openapi/generator/deployment/wrapper/QuarkusJavaClientCodegen.java @@ -154,7 +154,7 @@ public CodegenProperty fromProperty(String name, Schema p, boolean required, boo // Property is a `type: object` without `additionalProperties: true`, without `properties`, but has `default` values set! // In this peculiar situation, the template will try to initialize a Java Object with such values, and it will fail to compile. // See https://github.com/quarkiverse/quarkus-openapi-generator/issues/1185 for more context. - if (p.getType().equals("object") && p.getDefault() != null && p.getAdditionalProperties() == null + if ("object".equals(p.getType()) && p.getDefault() != null && p.getAdditionalProperties() == null && p.getItems() == null) { p.setAdditionalProperties(true); }