diff --git a/bin/configs/kotlin-spring-boot-delegate.yaml b/bin/configs/kotlin-spring-boot-delegate.yaml index c09fcf345fdb..b887a2eb1721 100644 --- a/bin/configs/kotlin-spring-boot-delegate.yaml +++ b/bin/configs/kotlin-spring-boot-delegate.yaml @@ -1,8 +1,13 @@ generatorName: kotlin-spring outputDir: samples/server/petstore/kotlin-springboot-delegate library: spring-boot -inputSpec: modules/openapi-generator/src/test/resources/3_0/petstore.yaml +inputSpec: modules/openapi-generator/src/test/resources/3_0/kotlin/petstore-with-extra-annotation.yaml templateDir: modules/openapi-generator/src/main/resources/kotlin-spring +injectOperationVendorExtensions: + placeOrder.x-request-body-extra-annotation: "@NonNull" + placeOrder.x-extra-imports: org.springframework.lang.NonNull + getOrderById.orderId.x-field-extra-annotation: "@NonNull" + getOrderById.orderId.x-extra-imports: org.springframework.lang.NonNull additionalProperties: generateJsonIncludeAnnotations: "true" generateJsonSetterNullsAnnotations: "true" diff --git a/bin/configs/kotlin-spring-boot-reactive.yaml b/bin/configs/kotlin-spring-boot-reactive.yaml index a8272859ce07..dd7c9d6ccc30 100644 --- a/bin/configs/kotlin-spring-boot-reactive.yaml +++ b/bin/configs/kotlin-spring-boot-reactive.yaml @@ -1,8 +1,13 @@ generatorName: kotlin-spring outputDir: samples/server/petstore/kotlin-springboot-reactive library: spring-boot -inputSpec: modules/openapi-generator/src/test/resources/3_0/petstore.yaml +inputSpec: modules/openapi-generator/src/test/resources/3_0/kotlin/petstore-with-extra-annotation.yaml templateDir: modules/openapi-generator/src/main/resources/kotlin-spring +injectOperationVendorExtensions: + placeOrder.x-request-body-extra-annotation: "@NonNull" + placeOrder.x-extra-imports: org.springframework.lang.NonNull + getOrderById.orderId.x-field-extra-annotation: "@NonNull" + getOrderById.orderId.x-extra-imports: org.springframework.lang.NonNull additionalProperties: generateJsonIncludeAnnotations: "true" generateJsonSetterNullsAnnotations: "true" diff --git a/bin/configs/spring-boot-reactive.yaml b/bin/configs/spring-boot-reactive.yaml index 338a4ebaeedf..164719b6d418 100644 --- a/bin/configs/spring-boot-reactive.yaml +++ b/bin/configs/spring-boot-reactive.yaml @@ -1,7 +1,10 @@ generatorName: spring outputDir: samples/server/petstore/springboot-reactive -inputSpec: modules/openapi-generator/src/test/resources/3_0/spring/petstore-with-fake-endpoints-models-for-testing.yaml +inputSpec: modules/openapi-generator/src/test/resources/3_0/spring/petstore-with-fake-endpoints-models-for-testing-extra-annotation.yaml templateDir: modules/openapi-generator/src/main/resources/JavaSpring +injectOperationVendorExtensions: + placeOrder.x-request-body-extra-annotation: "@org.springframework.lang.NonNull" + getOrderById.order_id.x-field-extra-annotation: "@org.springframework.lang.NonNull" additionalProperties: generateJsonIncludeAnnotations: "true" generateJsonSetterNullsAnnotations: "true" diff --git a/bin/configs/spring-boot-useoptional.yaml b/bin/configs/spring-boot-useoptional.yaml index 428935c9bdd7..dba0e460064e 100644 --- a/bin/configs/spring-boot-useoptional.yaml +++ b/bin/configs/spring-boot-useoptional.yaml @@ -1,7 +1,10 @@ generatorName: spring outputDir: samples/server/petstore/springboot-useoptional -inputSpec: modules/openapi-generator/src/test/resources/3_0/spring/petstore-with-fake-endpoints-models-for-testing.yaml +inputSpec: modules/openapi-generator/src/test/resources/3_0/spring/petstore-with-fake-endpoints-models-for-testing-extra-annotation.yaml templateDir: modules/openapi-generator/src/main/resources/JavaSpring +injectOperationVendorExtensions: + placeOrder.x-request-body-extra-annotation: "@org.springframework.lang.NonNull" + getOrderById.order_id.x-field-extra-annotation: "@org.springframework.lang.NonNull" additionalProperties: generateJsonIncludeAnnotations: "true" generateJsonSetterNullsAnnotations: "true" diff --git a/docs/generators/kotlin-spring.md b/docs/generators/kotlin-spring.md index a89526feb3b3..4e0904853952 100644 --- a/docs/generators/kotlin-spring.md +++ b/docs/generators/kotlin-spring.md @@ -93,6 +93,7 @@ These options may be applied as additional-properties (cli) or configOptions (pl |x-field-extra-annotation|Custom annotation(s) to be added to property; accepts a string or list of strings|FIELD, OPERATION_PARAMETER|null |x-jackson-default-impl|Specifies the default implementation class name for Jackson @JsonTypeInfo(defaultImpl=...) on a oneOf interface schema (deduction-based or discriminator-based). Can be overridden per-schema by the `typeInfoDefaultImpls` generator option.|MODEL|null |x-operation-extra-annotation|Custom annotation(s) to be added to operation; accepts a string or list of strings|OPERATION|null +|x-request-body-extra-annotation|Custom annotation(s) to be added to the request body parameter; accepts a string or list of strings. Declared on the operation because the request body typically `$ref`s a shared model (so the annotation cannot be placed next to the `$ref`); the value is rendered by being merged into the body parameter's `x-field-extra-annotation`|OPERATION|null |x-extra-imports|Custom import(s) to add to the generated file that declares the annotated model, property, operation, or parameter (e.g. so custom annotations can be referenced by their short name); accepts a string or list of strings. Values are emitted verbatim (Kotlin alias imports supported) and only exact duplicates are removed|MODEL, FIELD, OPERATION, OPERATION_PARAMETER|null |x-pattern-message|Add this property whenever you need to customize the invalidation error message for the regex pattern of a variable|FIELD, OPERATION_PARAMETER|null |x-size-message|Add this property whenever you need to customize the invalidation error message for the size or length of a variable|FIELD, OPERATION_PARAMETER|null diff --git a/docs/generators/spring.md b/docs/generators/spring.md index 1bfe7eb2916d..7c431c1ecded 100644 --- a/docs/generators/spring.md +++ b/docs/generators/spring.md @@ -146,6 +146,7 @@ These options may be applied as additional-properties (cli) or configOptions (pl |x-class-extra-annotation|Custom annotation(s) to be added to model; accepts a string or list of strings|MODEL|null |x-field-extra-annotation|Custom annotation(s) to be added to property; accepts a string or list of strings|FIELD, OPERATION_PARAMETER|null |x-operation-extra-annotation|Custom annotation(s) to be added to operation; accepts a string or list of strings|OPERATION|null +|x-request-body-extra-annotation|Custom annotation(s) to be added to the request body parameter; accepts a string or list of strings. Declared on the operation because the request body typically `$ref`s a shared model (so the annotation cannot be placed next to the `$ref`); the value is rendered by being merged into the body parameter's `x-field-extra-annotation`|OPERATION|null |x-spring-paginated|Add `org.springframework.data.domain.Pageable` to controller method. Can be used to handle `page`, `size` and `sort` query parameters. If these query parameters are also specified in the operation spec, they will be removed from the controller method as their values can be obtained from the `Pageable` object. Applies when `library=spring-boot` or `library=spring-cloud`; ignored for other (client) libraries.|OPERATION|false |x-version-param|Marker property that tells that this parameter would be used for endpoint versioning. Applicable for headers & query params. true/false|OPERATION_PARAMETER|null |x-pattern-message|Add this property whenever you need to customize the invalidation error message for the regex pattern of a variable|FIELD, OPERATION_PARAMETER|null diff --git a/modules/openapi-generator-cli/src/main/java/org/openapitools/codegen/cmd/Generate.java b/modules/openapi-generator-cli/src/main/java/org/openapitools/codegen/cmd/Generate.java index 9c5a15758d27..c9c70e964775 100644 --- a/modules/openapi-generator-cli/src/main/java/org/openapitools/codegen/cmd/Generate.java +++ b/modules/openapi-generator-cli/src/main/java/org/openapitools/codegen/cmd/Generate.java @@ -256,9 +256,50 @@ public class Generate extends OpenApiGeneratorCommand { description = "injects vendor extensions into model classes or their properties." + " Class-level format: ModelName.x-extension-name=value." + " Property-level format: ModelName.propertyBaseName.x-extension-name=value." - + " You can also have multiple occurrences of this option.") + + " Values are strings, applied at render time, and overwrite existing values." + + " Each occurrence of this option is exactly one key=value pair; everything after" + + " the first '=' (including any commas, e.g. for @Size(min = 1, max = 100)) is" + + " taken literally as the value, so annotation arguments do not need escaping." + + " For the extra-annotation extensions, separate multiple annotations rendered on" + + " the same line within a single value with spaces (e.g." + + " 'ModelName.x-class-extra-annotation=@JsonProperty(value = \"example\") @Deprecated')," + + " or repeat this option with the same key to add another list entry (e.g. run it once" + + " with 'ModelName.x-class-extra-annotation=@Foo' and again with" + + " 'ModelName.x-class-extra-annotation=@Bar'); templates that loop over the extension" + + " (such as class- and property-level extra-annotations) render each repeated" + + " occurrence on its own line, in the order given." + + " Quote the whole key=value pair when the value contains spaces (e.g." + + " \"ModelName.x-class-extra-annotation=@Foo(\\\"some string with spaces\\\")\")," + + " and if your shell requires double-quoting the whole argument instead, escape" + + " embedded double quotes (e.g. \\\")." + + " You can have multiple occurrences of this option, one per injection target.") private List injectModelVendorExtensions = new ArrayList<>(); + @Option( + name = {"--inject-operation-vendor-extensions"}, + title = "inject operation vendor extensions", + description = "injects vendor extensions into operations or their parameters." + + " Operation-level format: operationId.x-extension-name=value." + + " Parameter-level format: operationId.paramBaseName.x-extension-name=value." + + " Values are strings, applied at render time, and overwrite existing values." + + " Each occurrence of this option is exactly one key=value pair; everything after" + + " the first '=' (including any commas, e.g. for @Size(min = 1, max = 100)) is" + + " taken literally as the value, so annotation arguments do not need escaping." + + " For the extra-annotation extensions, separate multiple annotations rendered on" + + " the same line within a single value with spaces (e.g." + + " 'operationId.x-operation-extra-annotation=@JsonProperty(value = \"example\") @Deprecated')," + + " or repeat this option with the same key to add another list entry (e.g. run it once" + + " with 'operationId.x-operation-extra-annotation=@Foo' and again with" + + " 'operationId.x-operation-extra-annotation=@Bar'); templates that loop over the" + + " extension (such as operation-level extra-annotations) render each repeated" + + " occurrence on its own line, in the order given." + + " Quote the whole key=value pair when the value contains spaces (e.g." + + " \"operationId.x-operation-extra-annotation=@Foo(\\\"some string with spaces\\\")\")," + + " and if your shell requires double-quoting the whole argument instead, escape" + + " embedded double quotes (e.g. \\\")." + + " You can have multiple occurrences of this option, one per injection target.") + private List injectOperationVendorExtensions = new ArrayList<>(); + @Option( name = {"--openapi-normalizer"}, title = "OpenAPI normalizer rules", @@ -606,6 +647,7 @@ public void execute() { applyEnumNameMappingsKvpList(enumNameMappings, configurator); applyOperationIdNameMappingsKvpList(operationIdNameMappings, configurator); applyInjectModelVendorExtensionsKvpList(injectModelVendorExtensions, configurator); + applyInjectOperationVendorExtensionsKvpList(injectOperationVendorExtensions, configurator); applyOpenapiNormalizerKvpList(openapiNormalizer, configurator); applyTypeMappingsKvpList(typeMappings, configurator); applyAdditionalPropertiesKvpList(additionalProperties, configurator); diff --git a/modules/openapi-generator-core/src/main/java/org/openapitools/codegen/config/GeneratorSettings.java b/modules/openapi-generator-core/src/main/java/org/openapitools/codegen/config/GeneratorSettings.java index 2bec5d708d78..5fb5572217e5 100644 --- a/modules/openapi-generator-core/src/main/java/org/openapitools/codegen/config/GeneratorSettings.java +++ b/modules/openapi-generator-core/src/main/java/org/openapitools/codegen/config/GeneratorSettings.java @@ -58,7 +58,8 @@ public final class GeneratorSettings implements Serializable { private final Map modelNameMappings; private final Map enumNameMappings; private final Map operationIdNameMappings; - private final Map injectModelVendorExtensions; + private final Map> injectModelVendorExtensions; + private final Map> injectOperationVendorExtensions; private final Map openapiNormalizer; private final Set languageSpecificPrimitives; private final Set openapiGeneratorIgnoreList; @@ -71,6 +72,45 @@ public final class GeneratorSettings implements Serializable { private String releaseNote; private String httpUserAgent; + /** + * Returns an unmodifiable deep copy of a {@code Map>}: a new outer map, + * with each value replaced by an unmodifiable copy of its list. Used for + * {@code injectModelVendorExtensions}/{@code injectOperationVendorExtensions} so that mutating + * a list returned by {@link #getInjectModelVendorExtensions()} (or reusing it as the starting + * point for another {@link Builder}, e.g. via {@link #newBuilder(GeneratorSettings)}) can never + * affect this (or any other) already-built {@code GeneratorSettings} instance. + * + * @param source the map to copy; may be {@code null} + * @return an unmodifiable deep copy, or an empty unmodifiable map if {@code source} is {@code null} + */ + private static Map> unmodifiableDeepCopy(Map> source) { + if (source == null) { + return Collections.unmodifiableMap(new HashMap<>(0)); + } + Map> copy = new HashMap<>(); + source.forEach((key, value) -> + copy.put(key, Collections.unmodifiableList(new ArrayList<>(value)))); + return Collections.unmodifiableMap(copy); + } + + /** + * Returns a mutable deep copy of a {@code Map>}: a new outer + * {@code HashMap} with each value replaced by a new mutable {@code ArrayList} copy of its list. + * Used when a {@link Builder} bulk setter (e.g. {@link Builder#withInjectModelVendorExtensions}) + * stores a caller-supplied map, so later mutation via {@link Builder#withInjectModelVendorExtension} + * cannot affect the caller's map/lists, and vice versa. + * + * @param source the map to copy; may be {@code null} + * @return a mutable deep copy, or a new empty mutable map if {@code source} is {@code null} + */ + private static Map> deepCopyForBuilder(Map> source) { + Map> copy = new HashMap<>(); + if (source != null) { + source.forEach((key, value) -> copy.put(key, new ArrayList<>(value))); + } + return copy; + } + /** * Gets the name of the generator to use. * @@ -320,12 +360,21 @@ public Map getOperationIdNameMappings() { /** * Gets inject model vendor extensions. * - * @return a map of ModelName.x-extension-name or ModelName.propertyBaseName.x-extension-name to extension value + * @return a map of ModelName.x-extension-name or ModelName.propertyBaseName.x-extension-name to a list of extension values (one per injected occurrence) */ - public Map getInjectModelVendorExtensions() { + public Map> getInjectModelVendorExtensions() { return injectModelVendorExtensions; } + /** + * Gets inject operation vendor extensions. + * + * @return a map of operationId.x-extension-name or operationId.paramBaseName.x-extension-name to a list of extension values (one per injected occurrence) + */ + public Map> getInjectOperationVendorExtensions() { + return injectOperationVendorExtensions; + } + /** * Gets OpenAPI normalizer rules * @@ -467,7 +516,8 @@ private GeneratorSettings(Builder builder) { modelNameMappings = Collections.unmodifiableMap(builder.modelNameMappings); enumNameMappings = Collections.unmodifiableMap(builder.enumNameMappings); operationIdNameMappings = Collections.unmodifiableMap(builder.operationIdNameMappings); - injectModelVendorExtensions = Collections.unmodifiableMap(builder.injectModelVendorExtensions); + injectModelVendorExtensions = unmodifiableDeepCopy(builder.injectModelVendorExtensions); + injectOperationVendorExtensions = unmodifiableDeepCopy(builder.injectOperationVendorExtensions); openapiNormalizer = Collections.unmodifiableMap(builder.openapiNormalizer); languageSpecificPrimitives = Collections.unmodifiableSet(builder.languageSpecificPrimitives); openapiGeneratorIgnoreList = Collections.unmodifiableSet(builder.openapiGeneratorIgnoreList); @@ -549,6 +599,7 @@ public GeneratorSettings() { enumNameMappings = Collections.unmodifiableMap(new HashMap<>(0)); operationIdNameMappings = Collections.unmodifiableMap(new HashMap<>(0)); injectModelVendorExtensions = Collections.unmodifiableMap(new HashMap<>(0)); + injectOperationVendorExtensions = Collections.unmodifiableMap(new HashMap<>(0)); openapiNormalizer = Collections.unmodifiableMap(new HashMap<>(0)); languageSpecificPrimitives = Collections.unmodifiableSet(new HashSet<>(0)); openapiGeneratorIgnoreList = Collections.unmodifiableSet(new HashSet<>(0)); @@ -627,7 +678,12 @@ public static Builder newBuilder(GeneratorSettings copy) { builder.operationIdNameMappings.putAll(copy.getOperationIdNameMappings()); } if (copy.getInjectModelVendorExtensions() != null) { - builder.injectModelVendorExtensions.putAll(copy.getInjectModelVendorExtensions()); + copy.getInjectModelVendorExtensions().forEach((key, value) -> + builder.injectModelVendorExtensions.put(key, new ArrayList<>(value))); + } + if (copy.getInjectOperationVendorExtensions() != null) { + copy.getInjectOperationVendorExtensions().forEach((key, value) -> + builder.injectOperationVendorExtensions.put(key, new ArrayList<>(value))); } if (copy.getOpenapiNormalizer() != null) { builder.openapiNormalizer.putAll(copy.getOpenapiNormalizer()); @@ -682,7 +738,8 @@ public static final class Builder { private Map modelNameMappings; private Map enumNameMappings; private Map operationIdNameMappings; - private Map injectModelVendorExtensions; + private Map> injectModelVendorExtensions; + private Map> injectOperationVendorExtensions; private Map openapiNormalizer; private Set languageSpecificPrimitives; private Set openapiGeneratorIgnoreList; @@ -711,6 +768,7 @@ public Builder() { enumNameMappings = new HashMap<>(); operationIdNameMappings = new HashMap<>(); injectModelVendorExtensions = new HashMap<>(); + injectOperationVendorExtensions = new HashMap<>(); openapiNormalizer = new HashMap<>(); languageSpecificPrimitives = new HashSet<>(); openapiGeneratorIgnoreList = new HashSet<>(); @@ -1169,13 +1227,19 @@ public Builder withOperationIdNameMapping(String key, String value) { * @param injectModelExtensions the {@code injectModelExtensions} to set * @return a reference to this Builder */ - public Builder withInjectModelVendorExtensions(Map injectModelVendorExtensions) { - this.injectModelVendorExtensions = injectModelVendorExtensions; + public Builder withInjectModelVendorExtensions(Map> injectModelVendorExtensions) { + this.injectModelVendorExtensions = deepCopyForBuilder(injectModelVendorExtensions); return this; } /** * Sets a single {@code injectModelVendorExtension} and returns a reference to this Builder so that the methods can be chained together. + * Calling this repeatedly for the same key (e.g. once per {@code --inject-model-vendor-extensions} + * occurrence targeting the same key) appends {@code value} as a new element to the list stored + * under {@code key}, in call order, rather than the later call silently overwriting the earlier + * one. This lets multiple annotations be added to the same extra-annotation target across + * separate option occurrences, each rendered as a distinct list entry by templates that loop over + * the resulting {@code List}. * * @param key A key in the format ModelName.x-extension-name or ModelName.propertyBaseName.x-extension-name * @param value The extension value @@ -1185,7 +1249,39 @@ public Builder withInjectModelVendorExtension(String key, String value) { if (this.injectModelVendorExtensions == null) { this.injectModelVendorExtensions = new HashMap<>(); } - this.injectModelVendorExtensions.put(key, value); + this.injectModelVendorExtensions.computeIfAbsent(key, k -> new ArrayList<>()).add(value); + return this; + } + + /** + * Sets the {@code injectOperationVendorExtensions} and returns a reference to this Builder so that the methods can be chained together. + * + * @param injectOperationVendorExtensions the {@code injectOperationVendorExtensions} to set + * @return a reference to this Builder + */ + public Builder withInjectOperationVendorExtensions(Map> injectOperationVendorExtensions) { + this.injectOperationVendorExtensions = deepCopyForBuilder(injectOperationVendorExtensions); + return this; + } + + /** + * Sets a single {@code injectOperationVendorExtension} and returns a reference to this Builder so that the methods can be chained together. + * Calling this repeatedly for the same key (e.g. once per {@code --inject-operation-vendor-extensions} + * occurrence targeting the same key) appends {@code value} as a new element to the list stored + * under {@code key}, in call order, rather than the later call silently overwriting the earlier + * one. This lets multiple annotations be added to the same extra-annotation target across + * separate option occurrences, each rendered as a distinct list entry by templates that loop over + * the resulting {@code List}. + * + * @param key A key in the format operationId.x-extension-name or operationId.paramBaseName.x-extension-name + * @param value The extension value + * @return a reference to this Builder + */ + public Builder withInjectOperationVendorExtension(String key, String value) { + if (this.injectOperationVendorExtensions == null) { + this.injectOperationVendorExtensions = new HashMap<>(); + } + this.injectOperationVendorExtensions.computeIfAbsent(key, k -> new ArrayList<>()).add(value); return this; } @@ -1433,6 +1529,8 @@ public boolean equals(Object o) { Objects.equals(getModelNameMappings(), that.getModelNameMappings()) && Objects.equals(getEnumNameMappings(), that.getEnumNameMappings()) && Objects.equals(getOperationIdNameMappings(), that.getOperationIdNameMappings()) && + Objects.equals(getInjectModelVendorExtensions(), that.getInjectModelVendorExtensions()) && + Objects.equals(getInjectOperationVendorExtensions(), that.getInjectOperationVendorExtensions()) && Objects.equals(getOpenapiNormalizer(), that.getOpenapiNormalizer()) && Objects.equals(getLanguageSpecificPrimitives(), that.getLanguageSpecificPrimitives()) && Objects.equals(getOpenapiGeneratorIgnoreList(), that.getOpenapiGeneratorIgnoreList()) && @@ -1471,6 +1569,8 @@ public int hashCode() { getModelNameMappings(), getEnumNameMappings(), getOperationIdNameMappings(), + getInjectModelVendorExtensions(), + getInjectOperationVendorExtensions(), getOpenapiNormalizer(), getLanguageSpecificPrimitives(), getOpenapiGeneratorIgnoreList(), diff --git a/modules/openapi-generator-core/src/test/java/org/openapitools/codegen/config/GeneratorSettingsTest.java b/modules/openapi-generator-core/src/test/java/org/openapitools/codegen/config/GeneratorSettingsTest.java new file mode 100644 index 000000000000..1fa2448f475e --- /dev/null +++ b/modules/openapi-generator-core/src/test/java/org/openapitools/codegen/config/GeneratorSettingsTest.java @@ -0,0 +1,110 @@ +/* + * Copyright 2026 OpenAPI-Generator Contributors (https://openapi-generator.tech) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.openapitools.codegen.config; + +import org.testng.annotations.Test; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import static org.testng.Assert.assertEquals; +import static org.testng.Assert.assertThrows; + +public class GeneratorSettingsTest { + + @Test + public void withInjectModelVendorExtensionAppendsRepeatedKeyValuesInOrder() { + GeneratorSettings settings = GeneratorSettings.newBuilder() + .withGeneratorName("spring") + .withInjectModelVendorExtension("Pet.x-class-extra-annotation", "@Foo") + .withInjectModelVendorExtension("Pet.x-class-extra-annotation", "@Bar") + .build(); + + assertEquals(settings.getInjectModelVendorExtensions().get("Pet.x-class-extra-annotation"), List.of("@Foo", "@Bar")); + } + + @Test + public void withInjectOperationVendorExtensionAppendsRepeatedKeyValuesInOrder() { + GeneratorSettings settings = GeneratorSettings.newBuilder() + .withGeneratorName("spring") + .withInjectOperationVendorExtension("addPet.x-operation-extra-annotation", "@Foo") + .withInjectOperationVendorExtension("addPet.x-operation-extra-annotation", "@Bar") + .withInjectOperationVendorExtension("addPet.x-operation-extra-annotation", "@Baz") + .build(); + + assertEquals(settings.getInjectOperationVendorExtensions().get("addPet.x-operation-extra-annotation"), + List.of("@Foo", "@Bar", "@Baz")); + } + + @Test + public void withInjectOperationVendorExtensionKeepsDistinctKeysIndependent() { + GeneratorSettings settings = GeneratorSettings.newBuilder() + .withGeneratorName("spring") + .withInjectOperationVendorExtension("addPet.x-operation-extra-annotation", "@Foo") + .withInjectOperationVendorExtension("addPet.orgId.x-field-extra-annotation", "@Bar") + .build(); + + assertEquals(settings.getInjectOperationVendorExtensions().get("addPet.x-operation-extra-annotation"), List.of("@Foo")); + assertEquals(settings.getInjectOperationVendorExtensions().get("addPet.orgId.x-field-extra-annotation"), List.of("@Bar")); + } + + @Test + public void getInjectModelVendorExtensionsListIsUnmodifiable() { + GeneratorSettings settings = GeneratorSettings.newBuilder() + .withGeneratorName("spring") + .withInjectModelVendorExtension("Pet.x-class-extra-annotation", "@Foo") + .build(); + + List values = settings.getInjectModelVendorExtensions().get("Pet.x-class-extra-annotation"); + assertThrows(UnsupportedOperationException.class, () -> values.add("@Bar")); + } + + @Test + public void copyingBuilderDoesNotLeakMutationsBetweenSettingsInstances() { + GeneratorSettings original = GeneratorSettings.newBuilder() + .withGeneratorName("spring") + .withInjectModelVendorExtension("Pet.x-class-extra-annotation", "@Foo") + .build(); + + // Derive a second settings instance from the first (as GenerateBatch does when several + // configurators share a base config) and append another value to the same key. + GeneratorSettings.newBuilder(original) + .withInjectModelVendorExtension("Pet.x-class-extra-annotation", "@Bar") + .build(); + + // The original, already-built settings must be unaffected by the copy's mutation. + assertEquals(original.getInjectModelVendorExtensions().get("Pet.x-class-extra-annotation"), List.of("@Foo")); + } + + @Test + public void withInjectModelVendorExtensionsBulkSetterDoesNotShareListsWithCaller() { + Map> extensions = new HashMap<>(); + extensions.put("Pet.x-class-extra-annotation", new ArrayList<>(List.of("@Foo"))); + + GeneratorSettings settings = GeneratorSettings.newBuilder() + .withGeneratorName("spring") + .withInjectModelVendorExtensions(extensions) + .build(); + + // Mutating the caller's map/list after building must not affect the built settings. + extensions.get("Pet.x-class-extra-annotation").add("@Bar"); + + assertEquals(settings.getInjectModelVendorExtensions().get("Pet.x-class-extra-annotation"), List.of("@Foo")); + } +} diff --git a/modules/openapi-generator-gradle-plugin/README.adoc b/modules/openapi-generator-gradle-plugin/README.adoc index eeb6e4705d25..5bc61155708c 100644 --- a/modules/openapi-generator-gradle-plugin/README.adoc +++ b/modules/openapi-generator-gradle-plugin/README.adoc @@ -200,6 +200,16 @@ apply plugin: 'org.openapi.generator' |None |Sets specified global properties. +|injectModelVendorExtensions +|Map> / Provider +|None +|Injects vendor extensions into models or their properties without editing the input spec. Keys use the form `modelName.x-extension-name` (model) or `modelName.propertyBaseName.x-extension-name` (model property). Each key's value is a `List`: templates that loop over the extension (such as the extra-annotation family) render each list entry as a separate item (e.g. one annotation per line), in list order. See the note below. + +|injectOperationVendorExtensions +|Map> / Provider +|None +|Injects vendor extensions into operations or their parameters without editing the input spec. Keys use the form `operationId.x-extension-name` (operation) or `operationId.paramBaseName.x-extension-name` (parameter, matched by its raw spec name). Each key's value is a `List`: templates that loop over the extension (such as the extra-annotation family) render each list entry as a separate item (e.g. one annotation per line), in list order. See the note below. + |configFile |String / Provider |None @@ -559,6 +569,30 @@ models: "User:Pet" ---- ==== +[NOTE] +==== +`injectModelVendorExtensions` and `injectOperationVendorExtensions` are a generic mechanism for +setting *any* vendor extension on a model, property, operation, or parameter without editing the spec. +Keep in mind: + +* Each key's value is a `List`, applied *late* (at codegen/render time) and **overwrite** any + existing value; missing targets are a silent no-op. It is best suited to extensions consumed by + templates, such as the Spring extra-annotation extensions. +* In Gradle, supply the list directly for a key, e.g. + `["addPet.x-request-body-extra-annotation": ["@com.example.MyValidation", "@Valid"]]` — each + list entry renders as a separate item (e.g. one annotation per line) for templates that loop + over the extension. +* On the CLI/Maven side, each occurrence of `--inject-model-vendor-extensions`/ + `--inject-operation-vendor-extensions` is exactly one key=value pair (everything after the first + `=` is taken literally, so commas inside a value, e.g. `@Size(min = 0, max = 10)`, need no + escaping). Repeating the *same* key across occurrences appends `value` as a new list entry + (e.g. `@Foo` then `@Bar` becomes the list `["@Foo", "@Bar"]`, rendered on separate lines by + templates that loop) instead of the later one overwriting the earlier one. To put multiple + annotations on the *same* rendered line/list entry, put them in one occurrence's value + separated by *spaces* (as in source), e.g. + `"addPet.x-request-body-extra-annotation=@com.example.MyValidation @Valid"`. +==== + === openApiValidate .Options diff --git a/modules/openapi-generator-gradle-plugin/src/main/kotlin/org/openapitools/generator/gradle/plugin/OpenApiGeneratorPlugin.kt b/modules/openapi-generator-gradle-plugin/src/main/kotlin/org/openapitools/generator/gradle/plugin/OpenApiGeneratorPlugin.kt index 5a17bf6b2b7c..64eb77e51e66 100644 --- a/modules/openapi-generator-gradle-plugin/src/main/kotlin/org/openapitools/generator/gradle/plugin/OpenApiGeneratorPlugin.kt +++ b/modules/openapi-generator-gradle-plugin/src/main/kotlin/org/openapitools/generator/gradle/plugin/OpenApiGeneratorPlugin.kt @@ -127,6 +127,8 @@ class OpenApiGeneratorPlugin : Plugin { templateResourcePath.set(generate.templateResourcePath) auth.set(generate.auth) globalProperties.set(generate.globalProperties) + injectModelVendorExtensions.set(generate.injectModelVendorExtensions) + injectOperationVendorExtensions.set(generate.injectOperationVendorExtensions) configFile.set(generate.configFile) skipOverwrite.set(generate.skipOverwrite) packageName.set(generate.packageName) diff --git a/modules/openapi-generator-gradle-plugin/src/main/kotlin/org/openapitools/generator/gradle/plugin/extensions/OpenApiGeneratorGenerateExtension.kt b/modules/openapi-generator-gradle-plugin/src/main/kotlin/org/openapitools/generator/gradle/plugin/extensions/OpenApiGeneratorGenerateExtension.kt index 8936c22099c6..edcc65bfb9a7 100644 --- a/modules/openapi-generator-gradle-plugin/src/main/kotlin/org/openapitools/generator/gradle/plugin/extensions/OpenApiGeneratorGenerateExtension.kt +++ b/modules/openapi-generator-gradle-plugin/src/main/kotlin/org/openapitools/generator/gradle/plugin/extensions/OpenApiGeneratorGenerateExtension.kt @@ -163,6 +163,26 @@ open class OpenApiGeneratorGenerateExtension(private val project: Project) { */ val globalProperties = project.objects.mapProperty() + /** + * Injects vendor extensions into models or their properties without editing the input spec. + * + * Keys use the form {@code modelName.x-extension-name} for a model or + * {@code modelName.propertyBaseName.x-extension-name} for a model property; the value is a + * list of extension values, each rendered as a separate entry (e.g. one annotation per line, + * in list order, for templates that loop over the extension). + */ + val injectModelVendorExtensions = project.objects.mapProperty>() + + /** + * Injects vendor extensions into operations or their parameters without editing the input spec. + * + * Keys use the form {@code operationId.x-extension-name} for an operation or + * {@code operationId.paramBaseName.x-extension-name} for a parameter (matched by its raw spec + * name); the value is a list of extension values, each rendered as a separate entry (e.g. one + * annotation per line, in list order, for templates that loop over the extension). + */ + val injectOperationVendorExtensions = project.objects.mapProperty>() + /** * Path to json configuration file. * File content should be in a json format { "optionKey":"optionValue", "optionKey1":"optionValue1"...} diff --git a/modules/openapi-generator-gradle-plugin/src/main/kotlin/org/openapitools/generator/gradle/plugin/tasks/GenerateTask.kt b/modules/openapi-generator-gradle-plugin/src/main/kotlin/org/openapitools/generator/gradle/plugin/tasks/GenerateTask.kt index 44c6b7f31661..0d7308fe6f23 100644 --- a/modules/openapi-generator-gradle-plugin/src/main/kotlin/org/openapitools/generator/gradle/plugin/tasks/GenerateTask.kt +++ b/modules/openapi-generator-gradle-plugin/src/main/kotlin/org/openapitools/generator/gradle/plugin/tasks/GenerateTask.kt @@ -89,6 +89,8 @@ interface OpenApiWorkParameters : WorkParameters { val dryRun: Property val globalProperties: MapProperty + val injectModelVendorExtensions: MapProperty> + val injectOperationVendorExtensions: MapProperty> val instantiationTypes: MapProperty val importMappings: MapProperty val schemaMappings: MapProperty @@ -211,6 +213,8 @@ abstract class OpenApiWorkAction : WorkAction { // Maps and Lists params.globalProperties.orNull?.forEach { (k, v) -> configurator.addGlobalProperty(k, v) } + params.injectModelVendorExtensions.orNull?.forEach { (k, values) -> values.forEach { v -> configurator.addInjectModelVendorExtension(k, v) } } + params.injectOperationVendorExtensions.orNull?.forEach { (k, values) -> values.forEach { v -> configurator.addInjectOperationVendorExtension(k, v) } } params.instantiationTypes.orNull?.forEach { (k, v) -> configurator.addInstantiationType(k, v) } params.importMappings.orNull?.forEach { (k, v) -> configurator.addImportMapping(k, v) } params.schemaMappings.orNull?.forEach { (k, v) -> configurator.addSchemaMapping(k, v) } @@ -521,6 +525,30 @@ abstract class GenerateTask : DefaultTask() { @get:Input abstract val globalProperties: MapProperty + /** + * Injects vendor extensions into models or their properties without editing the input spec. + * + * Keys use the form {@code modelName.x-extension-name} for a model or + * {@code modelName.propertyBaseName.x-extension-name} for a model property; the value is a + * list of extension values, each rendered as a separate entry (e.g. one annotation per line, + * in list order, for templates that loop over the extension). + */ + @get:Optional + @get:Input + abstract val injectModelVendorExtensions: MapProperty> + + /** + * Injects vendor extensions into operations or their parameters without editing the input spec. + * + * Keys use the form {@code operationId.x-extension-name} for an operation or + * {@code operationId.paramBaseName.x-extension-name} for a parameter (matched by its raw spec + * name); the value is a list of extension values, each rendered as a separate entry (e.g. one + * annotation per line, in list order, for templates that loop over the extension). + */ + @get:Optional + @get:Input + abstract val injectOperationVendorExtensions: MapProperty> + /** * Path to json configuration file. * File content should be in a json format { "optionKey":"optionValue", "optionKey1":"optionValue1"...} @@ -1225,6 +1253,8 @@ abstract class GenerateTask : DefaultTask() { parameters.dryRun.set(dryRun) parameters.globalProperties.set(globalProperties) + parameters.injectModelVendorExtensions.set(injectModelVendorExtensions) + parameters.injectOperationVendorExtensions.set(injectOperationVendorExtensions) parameters.instantiationTypes.set(instantiationTypes) parameters.importMappings.set(importMappings) parameters.schemaMappings.set(schemaMappings) diff --git a/modules/openapi-generator-gradle-plugin/src/test/kotlin/ParameterWiringRegressionTest.kt b/modules/openapi-generator-gradle-plugin/src/test/kotlin/ParameterWiringRegressionTest.kt index d24892023f78..75415a750d27 100644 --- a/modules/openapi-generator-gradle-plugin/src/test/kotlin/ParameterWiringRegressionTest.kt +++ b/modules/openapi-generator-gradle-plugin/src/test/kotlin/ParameterWiringRegressionTest.kt @@ -226,4 +226,44 @@ class ParameterWiringRegressionTest : TestBase() { assertEquals(TaskOutcome.SUCCESS, result.task(":openApiGenerate")?.outcome) } + + // ------------------------------------------------------------------------- + // injectOperationVendorExtensions (side-loading, was not exposed on the plugin) + // ------------------------------------------------------------------------- + + @Test + fun `injectOperationVendorExtensions is wired from extension to task`() { + // Before the fix injectOperationVendorExtensions was not exposed on the Gradle plugin, + // so vendor extensions could only be side-loaded via a configFile. We inject + // x-operation-extra-annotation onto the listPets operation and verify the Spring + // generator renders the injected annotation into the generated API interface. + val result = runOpenApiGenerate(""" + plugins { id 'org.openapi.generator' } + openApiGenerate { + generatorName = "spring" + inputSpec = file("spec.yaml").absolutePath + outputDir = file("build/spring").absolutePath + configOptions = ["interfaceOnly": "true"] + injectOperationVendorExtensions = ["listPets.x-operation-extra-annotation": ["@Deprecated"]] + } + """.trimIndent(), "spec.yaml" to "specs/petstore-v3.0.yaml") + + assertEquals( + TaskOutcome.SUCCESS, result.task(":openApiGenerate")?.outcome, + "Generation failed after wiring injectOperationVendorExtensions — check plugin wiring" + ) + + val generatedSrcRoot = File(temp, "build/spring/src/main/java") + val allJavaSources = generatedSrcRoot.walkTopDown().filter { it.extension == "java" }.toList() + assertTrue(allJavaSources.isNotEmpty(), "No Java source files were generated") + + val allSourceText = allJavaSources.joinToString("\n") { it.readText() } + val occurrences = Regex("@Deprecated").findAll(allSourceText).count() + assertEquals( + 1, occurrences, + "Expected injected operation annotation '@Deprecated' to appear exactly once (only on the " + + "listPets operation) — found $occurrences occurrences. injectOperationVendorExtensions " + + "may not be selective to the targeted operation." + ) + } } diff --git a/modules/openapi-generator-maven-plugin/README.md b/modules/openapi-generator-maven-plugin/README.md index 4c5637e16a74..d866a24fc206 100644 --- a/modules/openapi-generator-maven-plugin/README.md +++ b/modules/openapi-generator-maven-plugin/README.md @@ -94,6 +94,8 @@ mvn clean compile | `parameterNameMappings` | `openapi.generator.maven.plugin.parameterNameMappings` | specifies mappings between the parameter name and the new name in the format of param_a=first_parameter,param_b=second_parameter. https://openapi-generator.tech/docs/customization/#name-mapping | | `inlineSchemaNameMappings` | `openapi.generator.maven.plugin.inlineSchemaNameMappings` | specifies mappings between the inline schema name and the new name in the format of inline_object_2=Cat,inline_object_5=Bird. | | `inlineSchemaOptions` | `openapi.generator.maven.plugin.inlineSchemaOptions` | specifies the options used when naming inline schema in inline model resolver | +| `injectModelVendorExtensions` | `openapi.generator.maven.plugin.injectModelVendorExtensions` | sets vendor extensions on a model or its properties without editing the spec, as `modelName.x-extension-name=value` (model) or `modelName.propertyBaseName.x-extension-name=value` (property). Values are strings, applied at render time, and overwrite existing values. Each occurrence is exactly one key=value pair, so commas in the value (e.g. `@Size(min = 1, max = 100)`) are taken literally and need no escaping. For the extra-annotation extensions, separate multiple annotations rendered on the same line within one value with spaces (`@Foo @Bar`), or repeat this option with the same key to add another list entry; templates that loop over the extension (such as class- and property-level extra-annotations) render each repeated occurrence on its own line, in the order given. You can have multiple occurrences of this option | +| `injectOperationVendorExtensions` | `openapi.generator.maven.plugin.injectOperationVendorExtensions` | sets vendor extensions on an operation or its parameters without editing the spec, as `operationId.x-extension-name=value` (operation) or `operationId.paramBaseName.x-extension-name=value` (parameter, matched by its raw spec name). Values are strings, applied at render time, and overwrite existing values. Each occurrence is exactly one key=value pair, so commas in the value (e.g. `@Size(min = 1, max = 100)`) are taken literally and need no escaping. For the extra-annotation extensions, separate multiple annotations rendered on the same line within one value with spaces (`@Foo @Bar`), or repeat this option with the same key to add another list entry; templates that loop over the extension (such as operation-level extra-annotations) render each repeated occurrence on its own line, in the order given. You can have multiple occurrences of this option | | `languageSpecificPrimitives` | `openapi.generator.maven.plugin.languageSpecificPrimitives` | specifies additional language specific primitive types in the format of type1,type2,type3,type3. For example: `String,boolean,Boolean,Double`. You can also have multiple occurrences of this option | | `additionalProperties` | `openapi.generator.maven.plugin.additionalProperties` | sets additional properties that can be referenced by the mustache templates in the format of name=value,name=value. You can also have multiple occurrences of this option | | `serverVariableOverrides` | `openapi.generator.maven.plugin.serverVariableOverrides` | A map of server variable overrides for specs that support server URL templating | diff --git a/modules/openapi-generator-maven-plugin/src/main/java/org/openapitools/codegen/plugin/CodeGenMojo.java b/modules/openapi-generator-maven-plugin/src/main/java/org/openapitools/codegen/plugin/CodeGenMojo.java index 7cb3c174f28b..880aff18276d 100644 --- a/modules/openapi-generator-maven-plugin/src/main/java/org/openapitools/codegen/plugin/CodeGenMojo.java +++ b/modules/openapi-generator-maven-plugin/src/main/java/org/openapitools/codegen/plugin/CodeGenMojo.java @@ -439,6 +439,23 @@ public class CodeGenMojo extends AbstractMojo { @Parameter(name = "operationIdNameMappings", property = "openapi.generator.maven.plugin.operationIdNameMappings") private List operationIdNameMappings; + /** + * A map of vendor extensions to inject into models or their properties, without editing the input + * spec. Each entry is of the form {@code modelName.x-extension-name=value} for a model or + * {@code modelName.propertyBaseName.x-extension-name=value} for a model property. + */ + @Parameter(name = "injectModelVendorExtensions", property = "openapi.generator.maven.plugin.injectModelVendorExtensions") + private List injectModelVendorExtensions; + + /** + * A map of vendor extensions to inject into operations or their parameters, without editing the + * input spec. Each entry is of the form {@code operationId.x-extension-name=value} for an operation + * or {@code operationId.paramBaseName.x-extension-name=value} for a parameter (matched by its raw + * spec name). + */ + @Parameter(name = "injectOperationVendorExtensions", property = "openapi.generator.maven.plugin.injectOperationVendorExtensions") + private List injectOperationVendorExtensions; + /** * A set of rules for OpenAPI normalizer */ @@ -996,31 +1013,46 @@ public void execute() throws MojoExecutionException { applyInlineSchemaOptionsKvpList(inlineSchemaOptions, configurator); } - // Apply Name Mappings - if (nameMappings != null && (configOptions == null || !configOptions.containsKey("name-mappings"))) { + // Apply Name Mappings. + // These *-name-mappings options are not generator CliOptions and have no configOptions + // compatibility reader above, so a configOptions guard would protect nothing. + if (nameMappings != null) { applyNameMappingsKvpList(nameMappings, configurator); } // Apply Parameter Name Mappings - if (parameterNameMappings != null && (configOptions == null || !configOptions.containsKey("parameter-name-mappings"))) { + if (parameterNameMappings != null) { applyParameterNameMappingsKvpList(parameterNameMappings, configurator); } // Apply Model Name Mappings - if (modelNameMappings != null && (configOptions == null || !configOptions.containsKey("model-name-mappings"))) { + if (modelNameMappings != null) { applyModelNameMappingsKvpList(modelNameMappings, configurator); } // Apply Enum Name Mappings - if (enumNameMappings != null && (configOptions == null || !configOptions.containsKey("enum-name-mappings"))) { + if (enumNameMappings != null) { applyEnumNameMappingsKvpList(enumNameMappings, configurator); } // Apply Operation ID Name Mappings - if (operationIdNameMappings != null && (configOptions == null || !configOptions.containsKey("operation-id-name-mappings"))) { + if (operationIdNameMappings != null) { applyOperationIdNameMappingsKvpList(operationIdNameMappings, configurator); } + // Apply Inject Model Vendor Extensions. + // Unlike the legacy mapping options above, there is no configOptions compatibility path + // for this setting (it is not a generator CliOption), so no configOptions guard is needed. + if (injectModelVendorExtensions != null) { + applyInjectModelVendorExtensionsKvpList(injectModelVendorExtensions, configurator); + } + + // Apply Inject Operation Vendor Extensions. + // No configOptions compatibility path exists for this setting either, so no guard is needed. + if (injectOperationVendorExtensions != null) { + applyInjectOperationVendorExtensionsKvpList(injectOperationVendorExtensions, configurator); + } + // Apply OpenAPI normalizer rules if (openapiNormalizer != null && (configOptions == null || !configOptions.containsKey("openapi-normalizer"))) { applyOpenapiNormalizerKvpList(openapiNormalizer, configurator); diff --git a/modules/openapi-generator-maven-plugin/src/test/java/org/openapitools/codegen/plugin/CodeGenMojoTest.java b/modules/openapi-generator-maven-plugin/src/test/java/org/openapitools/codegen/plugin/CodeGenMojoTest.java index 18b6df86f1e8..cbf84888c2ae 100644 --- a/modules/openapi-generator-maven-plugin/src/test/java/org/openapitools/codegen/plugin/CodeGenMojoTest.java +++ b/modules/openapi-generator-maven-plugin/src/test/java/org/openapitools/codegen/plugin/CodeGenMojoTest.java @@ -101,6 +101,85 @@ public void testMinimalUpdateConfiguration() throws Exception { assertEquals(Boolean.TRUE, getVariableValueFromObject(mojo, "minimalUpdate")); } + @SuppressWarnings("unchecked") + public void testInjectOperationVendorExtensions() throws Exception { + // GIVEN + final Path tempDir = newTempFolder(); + CodeGenMojo mojo = loadMojo(tempDir, "src/test/resources/inject-vendor-extensions", null, "executionId"); + + // WHEN + mojo.execute(); + + // THEN + // The configured parameter is bound onto the mojo. + List injected = (List) getVariableValueFromObject(mojo, "injectOperationVendorExtensions"); + assertNotNull(injected); + assertEquals(1, injected.size()); + assertEquals("addPet.x-request-body-extra-annotation=@com.example.MyValidation", injected.get(0)); + + // The injected request-body annotation is merged into the body parameter and rendered on the + // generated Spring API interface. Assert it lands specifically on addPet's body parameter and + // is NOT applied to a control operation (updatePet) that also has a request body but no + // injection, so the test fails if the merge became non-selective or targeted the wrong method. + final Path generatedDir = tempDir.resolve("target/generated-sources/inject-vendor-extensions"); + final Path petApi; + try (Stream files = Files.walk(generatedDir)) { + petApi = files + .filter(Files::isRegularFile) + .filter(path -> path.getFileName().toString().equals("PetApi.java")) + .findFirst() + .orElseThrow(() -> new AssertionError("PetApi.java was not generated under " + generatedDir)); + } + final String petApiSource = Files.readString(petApi); + final String addPetSignature = extractMethodSignature(petApiSource, "addPet"); + assertAnnotationPrecedesRequestBody( + addPetSignature, + "Injected annotation should be rendered on addPet's request body parameter before @RequestBody" + ); + + final String updatePetSignature = extractMethodSignature(petApiSource, "updatePet"); + assertFalse( + "Injected annotation must not leak onto the control operation updatePet", + updatePetSignature.contains("@com.example.MyValidation") + ); + + // Belt and suspenders: the injected annotation must appear exactly once in the whole + // interface, proving it was not applied to every operation. + int occurrences = petApiSource.split("@com.example.MyValidation", -1).length - 1; + assertEquals("Injected annotation should appear exactly once (only on addPet)", 1, occurrences); + } + + /** + * Extracts a method's declaration up to and including its balanced parameter-list parentheses, + * so parameter annotations can be asserted per-method. Assumes {@code methodName(} appears only + * at the method declaration (true for the generated Spring interface). + */ + private static String extractMethodSignature(String source, String methodName) { + int start = source.indexOf(methodName + "("); + assertTrue("Method '" + methodName + "' not found in generated source", start >= 0); + int depth = 0; + int i = start + methodName.length(); + for (; i < source.length(); i++) { + char c = source.charAt(i); + if (c == '(') { + depth++; + } else if (c == ')') { + depth--; + if (depth == 0) { + i++; + break; + } + } + } + return source.substring(start, i); + } + + private static void assertAnnotationPrecedesRequestBody(String methodSignature, String message) { + int injectedAnnotationIndex = methodSignature.indexOf("@com.example.MyValidation"); + int requestBodyIndex = methodSignature.indexOf("@RequestBody"); + assertTrue(message, injectedAnnotationIndex >= 0 && requestBodyIndex >= 0 && injectedAnnotationIndex < requestBodyIndex); + } + public void testHashGenerationFileContainsExecutionId() throws Exception { // GIVEN final Path tempDir = newTempFolder(); diff --git a/modules/openapi-generator-maven-plugin/src/test/resources/inject-vendor-extensions/pom.xml b/modules/openapi-generator-maven-plugin/src/test/resources/inject-vendor-extensions/pom.xml new file mode 100644 index 000000000000..cda479b71b6a --- /dev/null +++ b/modules/openapi-generator-maven-plugin/src/test/resources/inject-vendor-extensions/pom.xml @@ -0,0 +1,55 @@ + + + + 4.0.0 + inject.vendor.extensions.test + inject-vendor-extensions-test + jar + 1.0.0-SNAPSHOT + OpenAPI Generator Inject Vendor Extensions Test + https://openapi-generator.tech/ + + inject-vendor-extensions-test + + + org.openapitools + openapi-generator-maven-plugin + + petstore-on-classpath.yaml + spring + ${basedir}/target/generated-sources/inject-vendor-extensions + + true + true + + + addPet.x-request-body-extra-annotation=@com.example.MyValidation + + + + + executionId + generate-sources + + generate + + + + + + + diff --git a/modules/openapi-generator/src/main/java/org/openapitools/codegen/CodegenConfig.java b/modules/openapi-generator/src/main/java/org/openapitools/codegen/CodegenConfig.java index 99cb42eb3987..265194ba6510 100644 --- a/modules/openapi-generator/src/main/java/org/openapitools/codegen/CodegenConfig.java +++ b/modules/openapi-generator/src/main/java/org/openapitools/codegen/CodegenConfig.java @@ -179,7 +179,9 @@ default List divideOperationsByContentType(OpenAPI openAPI, String pa Map operationIdNameMapping(); - Map injectModelVendorExtensions(); + Map> injectModelVendorExtensions(); + + Map> injectOperationVendorExtensions(); Map openapiNormalizer(); diff --git a/modules/openapi-generator/src/main/java/org/openapitools/codegen/DefaultCodegen.java b/modules/openapi-generator/src/main/java/org/openapitools/codegen/DefaultCodegen.java index dcdad60d1816..1215da4d41cc 100644 --- a/modules/openapi-generator/src/main/java/org/openapitools/codegen/DefaultCodegen.java +++ b/modules/openapi-generator/src/main/java/org/openapitools/codegen/DefaultCodegen.java @@ -199,8 +199,10 @@ public class DefaultCodegen implements CodegenConfig { protected Map enumNameMapping = new HashMap<>(); // a map to store the mapping between operation id name and the name provided by the user protected Map operationIdNameMapping = new HashMap<>(); - // a map to inject vendor extensions into model classes or their properties: key=ModelName.x-extension-name or ModelName.propertyBaseName.x-extension-name, value=extensionValue - protected Map injectModelVendorExtensions = new HashMap<>(); + // a map to inject vendor extensions into model classes or their properties: key=ModelName.x-extension-name or ModelName.propertyBaseName.x-extension-name, value=list of extension values (one per injected occurrence) + protected Map> injectModelVendorExtensions = new HashMap<>(); + // a map to inject vendor extensions into operations or their parameters: key=operationId.x-extension-name or operationId.paramName.x-extension-name, value=list of extension values (one per injected occurrence) + protected Map> injectOperationVendorExtensions = new HashMap<>(); // a map to store the rules in OpenAPI Normalizer protected Map openapiNormalizer = new HashMap<>(); @Setter @@ -254,6 +256,16 @@ apiTemplateFiles are for API outputs only (controllers/handlers). // sort operations by default protected boolean skipSortingOperations = false; + // Carries the operationId used to match injected parameter-level vendor extensions + // (--inject-operation-vendor-extensions) across the fromOperation -> fromParameter(Parameter, Set) + // virtual dispatch boundary, so overriding generators (e.g. Dart, TypeScript Fetch) still run + // their own fromParameter logic while parameter-level injection still applies before + // postProcessParameter. Set/restored around each call in fromOperation's parameter loop; null + // for any other caller of the public fromParameter(Parameter, Set) method. A ThreadLocal + // (rather than a plain instance field) so this stays correct even if a single codegen instance + // is ever used to process operations from multiple threads concurrently. + private final ThreadLocal currentOperationVendorExtensionMatchOperationId = new ThreadLocal<>(); + protected final static Pattern XML_MIME_PATTERN = Pattern.compile("(?i)application/(.*)[+]?xml(;.*)?"); protected final static Pattern JSON_MIME_PATTERN = Pattern.compile("(?i)application/json(;.*)?"); protected final static Pattern JSON_VENDOR_MIME_PATTERN = Pattern.compile("(?i)application/vnd.(.*)+json(;.*)?"); @@ -554,34 +566,31 @@ public Map postProcessAllModels(Map objs) } } - // Inject vendor extensions from --inject-property-extensions into matching schema properties + // Inject vendor extensions from --inject-model-vendor-extensions into matching schema properties if (!injectModelVendorExtensions.isEmpty()) { for (Map.Entry entry : objs.entrySet()) { CodegenModel model = ModelUtils.getModelByName(entry.getKey(), objs); if (model == null) continue; - for (Map.Entry extEntry : injectModelVendorExtensions.entrySet()) { - String[] parts = extEntry.getKey().split("\\.", 3); - if (parts.length < 2) continue; - String modelName = parts[0]; - String extensionValue = extEntry.getValue(); - - if (!modelName.equals(entry.getKey())) continue; + for (Map.Entry> extEntry : injectModelVendorExtensions.entrySet()) { + String[] parts = getInjectedVendorExtensionParts(entry.getKey(), extEntry.getKey()); + if (parts == null) continue; + List extensionValues = extEntry.getValue(); - if (parts.length == 2) { + if (parts.length == 1) { // class-level extension: ModelName.x-extension-name - model.vendorExtensions.put(parts[1], extensionValue); + model.vendorExtensions.put(parts[0], extensionValues); } else { // property-level extension: ModelName.propertyBaseName.x-extension-name - String propertyBaseName = parts[1]; - String extensionName = parts[2]; + String propertyBaseName = parts[0]; + String extensionName = parts[1]; List> allPropertyLists = Arrays.asList( model.vars, model.allVars, model.readWriteVars, model.requiredVars, model.optionalVars, model.parentVars, model.readOnlyVars, model.nonNullableVars); for (List properties : allPropertyLists) { for (CodegenProperty property : properties) { if (propertyBaseName.equals(property.baseName)) { - property.vendorExtensions.put(extensionName, extensionValue); + property.vendorExtensions.put(extensionName, extensionValues); } } } @@ -1663,10 +1672,15 @@ public Map operationIdNameMapping() { } @Override - public Map injectModelVendorExtensions() { + public Map> injectModelVendorExtensions() { return injectModelVendorExtensions; } + @Override + public Map> injectOperationVendorExtensions() { + return injectOperationVendorExtensions; + } + @Override public Map openapiNormalizer() { return openapiNormalizer; @@ -4957,6 +4971,8 @@ public CodegenOperation fromOperation(String path, // store the original operationId for plug-in op.operationIdOriginal = operation.getOperationId(); op.operationId = getOrGenerateOperationId(operation, path, httpMethod); + String operationVendorExtensionMatchOperationId = getInjectedVendorExtensionsOperationId(op); + injectOperationLevelVendorExtensions(op, operationVendorExtensionMatchOperationId); if (isStrictSpecBehavior() && !path.startsWith("/")) { // modifies an operation.path to strictly conform to OpenAPI Spec @@ -5113,6 +5129,7 @@ public CodegenOperation fromOperation(String path, op.isMultipart = contentType.startsWith("multipart"); for (CodegenParameter cp : formParams) { setParameterEncodingValues(cp, requestBody.getContent().get(contentType)); + injectParameterLevelVendorExtensions(operationVendorExtensionMatchOperationId, cp); postProcessParameter(cp); } // add form parameters to the beginning of all parameter list @@ -5125,7 +5142,13 @@ public CodegenOperation fromOperation(String path, // process body parameter String bodyParameterName = ""; if (op.vendorExtensions != null && op.vendorExtensions.containsKey("x-codegen-request-body-name")) { - bodyParameterName = (String) op.vendorExtensions.get("x-codegen-request-body-name"); + // May be a plain String (spec-authored) or a List (injected via + // --inject-operation-vendor-extensions); this extension is scalar-by-nature so + // only the first entry is honored when injected as a list. + List injectedNames = getObjectAsStringList(op.vendorExtensions.get("x-codegen-request-body-name")); + if (!injectedNames.isEmpty()) { + bodyParameterName = injectedNames.get(0); + } } if (requestBody.getExtensions() != null && requestBody.getExtensions().containsKey("x-codegen-request-body-name")) { bodyParameterName = (String) requestBody.getExtensions().get("x-codegen-request-body-name"); @@ -5135,6 +5158,7 @@ public CodegenOperation fromOperation(String path, if (bodyParam != null) { bodyParam.description = escapeText(requestBody.getDescription()); + injectParameterLevelVendorExtensions(operationVendorExtensionMatchOperationId, bodyParam); postProcessParameter(bodyParam); bodyParams.add(bodyParam); if (prependFormOrBodyParameters) { @@ -5153,7 +5177,18 @@ public CodegenOperation fromOperation(String path, for (Parameter param : parameters) { param = ModelUtils.getReferencedParameter(this.openAPI, param); - CodegenParameter p = fromParameter(param, imports); + // Call the public, overridable fromParameter(Parameter, Set) (not the + // private 3-arg overload directly) so subclass overrides (e.g. Dart, TypeScript + // Fetch) still run; the match id is threaded through via a ThreadLocal so the + // base implementation can still apply parameter-level injection at the right point. + String previousOperationVendorExtensionMatchOperationId = currentOperationVendorExtensionMatchOperationId.get(); + currentOperationVendorExtensionMatchOperationId.set(operationVendorExtensionMatchOperationId); + CodegenParameter p; + try { + p = fromParameter(param, imports); + } finally { + currentOperationVendorExtensionMatchOperationId.set(previousOperationVendorExtensionMatchOperationId); + } p.setContent(getContent(param.getContent(), imports, "RequestParameter" + toModelName(param.getName()))); // ensure unique params @@ -5252,6 +5287,98 @@ public CodegenOperation fromOperation(String path, return op; } + /** + * Resolves the operationId segment used to match entries in {@code injectOperationVendorExtensions}. + * The spec-authored operationId is preferred when present; otherwise the generated operationId is + * used. + * + * @param op the operation being built + * @return the operationId segment to match, or {@code null} when injection is disabled + */ + private String getInjectedVendorExtensionsOperationId(CodegenOperation op) { + if (injectOperationVendorExtensions.isEmpty()) { + return null; + } + // The operation always has a non-blank operationId at this point: getOrGenerateOperationId + // synthesizes one from the path and HTTP method when the spec omits or blanks it. + Objects.requireNonNull(op.operationId, "operationId must be set before injecting operation vendor extensions"); + return StringUtils.isNotBlank(op.operationIdOriginal) ? op.operationIdOriginal : op.operationId; + } + + /** + * Injects operation-level vendor extensions supplied via {@code injectOperationVendorExtensions} + * onto the given operation. Keys are dotted as {@code operationId.x-extension-name}. + * + * @param op the operation to update + * @param matchOperationId the operationId segment to match + */ + private void injectOperationLevelVendorExtensions(CodegenOperation op, String matchOperationId) { + if (matchOperationId == null) { + return; + } + for (Map.Entry> extEntry : injectOperationVendorExtensions.entrySet()) { + String[] extensionParts = getInjectedVendorExtensionParts(matchOperationId, extEntry.getKey()); + if (extensionParts == null || extensionParts.length != 1) { + continue; + } + op.vendorExtensions.put(extensionParts[0], extEntry.getValue()); + } + } + + /** + * Injects parameter-level vendor extensions supplied via {@code injectOperationVendorExtensions} + * onto a parameter before {@code postProcessParameter} runs. Keys are dotted as + * {@code operationId.paramBaseName.x-extension-name}, where {@code paramBaseName} is matched + * against the parameter's spec-authored base name. + * + * @param matchOperationId the operationId segment to match + * @param parameter the parameter to update + */ + private void injectParameterLevelVendorExtensions(String matchOperationId, CodegenParameter parameter) { + if (matchOperationId == null || parameter == null) { + return; + } + for (Map.Entry> extEntry : injectOperationVendorExtensions.entrySet()) { + String[] extensionParts = getInjectedVendorExtensionParts(matchOperationId, extEntry.getKey()); + if (extensionParts == null || extensionParts.length != 2) { + continue; + } + if (extensionParts[0].equals(parameter.baseName)) { + parameter.vendorExtensions.put(extensionParts[1], extEntry.getValue()); + } + } + } + + /** + * Parses an injection key after confirming it targets the matched prefix (an operationId or a + * model name). The known prefix is removed first so literal dots inside it do not break + * matching. The remainder is then split at the last {@code .x-} boundary — since vendor + * extension names always start with {@code x-}, this keeps a dotted parameter/property base + * name intact even when it contains dots itself. Returns either a single-element array + * {@code [x-extension-name]} for prefix-level injection (operation- or model-level), or a + * two-element array {@code [baseName, x-extension-name]} for parameter- or property-level + * injection. + * + * @param matchPrefix the operationId or model name segment to match + * @param injectionKey the configured injection key + * @return parsed parts after the prefix, or {@code null} when the key does not match + */ + private String[] getInjectedVendorExtensionParts(String matchPrefix, String injectionKey) { + String prefix = matchPrefix + "."; + if (!injectionKey.startsWith(prefix)) { + return null; + } + String remainder = injectionKey.substring(prefix.length()); + if (remainder.isEmpty()) { + return null; + } + int splitIndex = remainder.lastIndexOf(".x-"); + if (splitIndex < 0) { + return new String[]{remainder}; + } + return new String[]{remainder.substring(0, splitIndex), remainder.substring(splitIndex + 1)}; + } + /** * Helper method to add an import for a data type if it exists in the importMapping. * @@ -5547,7 +5674,7 @@ public CodegenCallback fromCallback(String name, Callback callback, List return c; } - private void finishUpdatingParameter(CodegenParameter codegenParameter, Parameter parameter) { + private void finishUpdatingParameter(CodegenParameter codegenParameter, Parameter parameter, String operationVendorExtensionMatchOperationId) { // default to UNKNOWN_PARAMETER_NAME if paramName is null if (codegenParameter.paramName == null) { LOGGER.warn("Parameter name not defined properly. Default to UNKNOWN_PARAMETER_NAME"); @@ -5560,6 +5687,7 @@ private void finishUpdatingParameter(CodegenParameter codegenParameter, Paramete // set the parameter examples (if available) setParameterExamples(codegenParameter, parameter); + injectParameterLevelVendorExtensions(operationVendorExtensionMatchOperationId, codegenParameter); postProcessParameter(codegenParameter); LOGGER.debug("debugging codegenParameter return: {}", codegenParameter); } @@ -5618,6 +5746,10 @@ protected void updateParameterForString(CodegenParameter codegenParameter, Schem * @return Codegen Parameter object */ public CodegenParameter fromParameter(Parameter parameter, Set imports) { + return fromParameter(parameter, imports, currentOperationVendorExtensionMatchOperationId.get()); + } + + private CodegenParameter fromParameter(Parameter parameter, Set imports, String operationVendorExtensionMatchOperationId) { CodegenParameter codegenParameter = CodegenModelFactory.newInstance(CodegenModelType.PARAMETER); codegenParameter.baseName = parameter.getName(); @@ -5694,7 +5826,7 @@ public CodegenParameter fromParameter(Parameter parameter, Set imports) if (parameterSchema == null) { LOGGER.error("Not handling {} as Body Parameter at the moment", parameter); - finishUpdatingParameter(codegenParameter, parameter); + finishUpdatingParameter(codegenParameter, parameter, operationVendorExtensionMatchOperationId); return codegenParameter; } @@ -5702,7 +5834,7 @@ public CodegenParameter fromParameter(Parameter parameter, Set imports) parameterSchema = unaliasSchema(parameterSchema); if (parameterSchema == null) { LOGGER.warn("warning! Schema not found for parameter \" {} \"", parameter.getName()); - finishUpdatingParameter(codegenParameter, parameter); + finishUpdatingParameter(codegenParameter, parameter, operationVendorExtensionMatchOperationId); return codegenParameter; } @@ -5898,7 +6030,7 @@ public CodegenParameter fromParameter(Parameter parameter, Set imports) // set default value codegenParameter.defaultValue = toDefaultParameterValue(codegenProperty, parameterSchema); - finishUpdatingParameter(codegenParameter, parameter); + finishUpdatingParameter(codegenParameter, parameter, operationVendorExtensionMatchOperationId); return codegenParameter; } @@ -6088,7 +6220,7 @@ protected boolean isReservedWord(String word) { * @param operation the operation object * @param path the path of the operation * @param httpMethod the HTTP method of the operation - * @return the (generated) operationId + * @return the (generated) operationId; never null or blank */ protected String getOrGenerateOperationId(Operation operation, String path, String httpMethod) { String operationId = operation.getOperationId(); @@ -7540,6 +7672,68 @@ public static void normalizeVendorExtensionWithStringList(Map ve vendorExtensions.put(name, new ArrayList<>(getObjectAsStringList(vendorExtensions.get(name)))); } + /** + * Normalizes a vendor extension across all of an operation's parameter collections into a mutable + * {@code List}, so that a value authored as either a single string or a list is handled + * uniformly and further values can be appended. The same parameter can appear in several of the + * operation's parameter collections, so they are de-duplicated by identity before being updated. + * + * @param operation operation whose parameters should be updated + * @param name vendor extension name + */ + protected void normalizeOperationParameterVendorExtensions(CodegenOperation operation, String name) { + Set parameters = Collections.newSetFromMap(new IdentityHashMap<>()); + parameters.addAll(operation.allParams); + parameters.addAll(operation.bodyParams); + parameters.addAll(operation.pathParams); + parameters.addAll(operation.queryParams); + parameters.addAll(operation.headerParams); + parameters.addAll(operation.implicitHeadersParams); + parameters.addAll(operation.constantParams); + parameters.addAll(operation.formParams); + parameters.addAll(operation.cookieParams); + parameters.addAll(operation.requiredParams); + parameters.addAll(operation.optionalParams); + parameters.addAll(operation.requiredAndNotNullableParams); + parameters.addAll(operation.notNullableParams); + for (CodegenParameter parameter : parameters) { + normalizeVendorExtensionWithStringList(parameter.vendorExtensions, name); + } + } + + /** + * Merges the values of an operation-level vendor extension into a list-valued vendor extension on + * each request body parameter. This lets an annotation authored on the operation be applied to the + * request body parameter, which is needed because a request body typically {@code $ref}s a shared + * model and so cannot carry the annotation itself. Any values already present on the body parameter + * are preserved (they appear first), then the operation-level values are appended. This is a no-op + * when the source extension is absent or empty, leaving existing body-parameter extensions untouched. + * + * @param operation operation whose {@code bodyParams} should receive the merged values + * @param sourceName operation-level vendor extension name to read the values from + * @param targetName body-parameter vendor extension name to merge the values into + */ + public static void mergeOperationVendorExtensionIntoBodyParams(CodegenOperation operation, String sourceName, String targetName) { + List sourceValues = getObjectAsStringList(operation.vendorExtensions.get(sourceName)); + if (sourceValues.isEmpty()) { + return; + } + // A request body parameter may be represented by more than one object; update every + // instance so the merged values are applied consistently. + Set bodyParameters = Collections.newSetFromMap(new IdentityHashMap<>()); + bodyParameters.addAll(operation.bodyParams); + for (CodegenParameter param : operation.allParams) { + if (param.isBodyParam) { + bodyParameters.add(param); + } + } + for (CodegenParameter bodyParam : bodyParameters) { + List merged = new ArrayList<>(getObjectAsStringList(bodyParam.vendorExtensions.get(targetName))); + merged.addAll(sourceValues); + bodyParam.vendorExtensions.put(targetName, merged); + } + } + public Map getPropertyAsStringMap(String propertyKey) { final Object value = additionalProperties.get(propertyKey); return getObjectAsStringMap(value); diff --git a/modules/openapi-generator/src/main/java/org/openapitools/codegen/VendorExtension.java b/modules/openapi-generator/src/main/java/org/openapitools/codegen/VendorExtension.java index 088cb57a765a..1fec1fd33c28 100644 --- a/modules/openapi-generator/src/main/java/org/openapitools/codegen/VendorExtension.java +++ b/modules/openapi-generator/src/main/java/org/openapitools/codegen/VendorExtension.java @@ -26,6 +26,7 @@ public enum VendorExtension { X_CLASS_EXTRA_ANNOTATION("x-class-extra-annotation", ExtensionLevel.MODEL, "Custom annotation(s) to be added to model; accepts a string or list of strings", null), X_FIELD_EXTRA_ANNOTATION("x-field-extra-annotation", Arrays.asList(ExtensionLevel.FIELD, ExtensionLevel.OPERATION_PARAMETER), "Custom annotation(s) to be added to property; accepts a string or list of strings", null), X_OPERATION_EXTRA_ANNOTATION("x-operation-extra-annotation", ExtensionLevel.OPERATION, "Custom annotation(s) to be added to operation; accepts a string or list of strings", null), + X_REQUEST_BODY_EXTRA_ANNOTATION("x-request-body-extra-annotation", ExtensionLevel.OPERATION, "Custom annotation(s) to be added to the request body parameter; accepts a string or list of strings. Declared on the operation because the request body typically `$ref`s a shared model (so the annotation cannot be placed next to the `$ref`); the value is rendered by being merged into the body parameter's `x-field-extra-annotation`", null), X_EXTRA_IMPORTS("x-extra-imports", Arrays.asList(ExtensionLevel.MODEL, ExtensionLevel.FIELD, ExtensionLevel.OPERATION, ExtensionLevel.OPERATION_PARAMETER), "Custom import(s) to add to the generated file that declares the annotated model, property, operation, or parameter (e.g. so custom annotations can be referenced by their short name); accepts a string or list of strings. Values are emitted verbatim (Kotlin alias imports supported) and only exact duplicates are removed", null), X_VERSION_PARAM("x-version-param", ExtensionLevel.OPERATION_PARAMETER, "Marker property that tells that this parameter would be used for endpoint versioning. Applicable for headers & query params. true/false", null), X_PATTERN_MESSAGE("x-pattern-message", Arrays.asList(ExtensionLevel.FIELD, ExtensionLevel.OPERATION_PARAMETER), "Add this property whenever you need to customize the invalidation error message for the regex pattern of a variable", null), diff --git a/modules/openapi-generator/src/main/java/org/openapitools/codegen/config/CodegenConfigurator.java b/modules/openapi-generator/src/main/java/org/openapitools/codegen/config/CodegenConfigurator.java index 6a53f919fe97..d9d87b33fd41 100644 --- a/modules/openapi-generator/src/main/java/org/openapitools/codegen/config/CodegenConfigurator.java +++ b/modules/openapi-generator/src/main/java/org/openapitools/codegen/config/CodegenConfigurator.java @@ -17,6 +17,7 @@ package org.openapitools.codegen.config; +import com.fasterxml.jackson.databind.DeserializationFeature; import com.fasterxml.jackson.databind.Module; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.datatype.guava.GuavaModule; @@ -74,9 +75,10 @@ public class CodegenConfigurator { private Map nameMappings = new HashMap<>(); private Map parameterNameMappings = new HashMap<>(); private Map modelNameMappings = new HashMap<>(); + private Map> injectOperationVendorExtensions = new HashMap<>(); private Map enumNameMappings = new HashMap<>(); private Map operationIdNameMappings = new HashMap<>(); - private Map injectModelVendorExtensions = new HashMap<>(); + private Map> injectModelVendorExtensions = new HashMap<>(); private Map openapiNormalizer = new HashMap<>(); private Set languageSpecificPrimitives = new HashSet<>(); private Set openapiGeneratorIgnoreList = new HashSet<>(); @@ -147,7 +149,16 @@ public static CodegenConfigurator fromFile(String configFile, Module... modules) configurator.operationIdNameMappings.putAll(generatorSettings.getOperationIdNameMappings()); } if (generatorSettings.getInjectModelVendorExtensions() != null) { - configurator.injectModelVendorExtensions.putAll(generatorSettings.getInjectModelVendorExtensions()); + // Deep-copy: getInjectModelVendorExtensions() returns unmodifiable lists, and this + // configurator's own injectModelVendorExtensions field must stay independently + // mutable so a later addInjectModelVendorExtension() call (e.g. a CLI flag adding + // to a key already present in the config file) can still append to it. + generatorSettings.getInjectModelVendorExtensions().forEach((key, value) -> + configurator.injectModelVendorExtensions.put(key, new ArrayList<>(value))); + } + if (generatorSettings.getInjectOperationVendorExtensions() != null) { + generatorSettings.getInjectOperationVendorExtensions().forEach((key, value) -> + configurator.injectOperationVendorExtensions.put(key, new ArrayList<>(value))); } if (generatorSettings.getOpenapiNormalizer() != null) { configurator.openapiNormalizer.putAll(generatorSettings.getOpenapiNormalizer()); @@ -192,6 +203,14 @@ private static DynamicSettings readDynamicSettings(String configFile, Module... mapper.registerModule(new GuavaModule()); + // Allows injectModelVendorExtensions/injectOperationVendorExtensions values (and any other + // List-valued config property) to be authored as either a plain scalar or a list, so config + // files written before injected values became lists (e.g. `x-setter-visibility: private`) + // keep working. This is a standard Jackson feature scoped to List/Collection content + // deserialization, so Map key/value type validation and top-level shape validation + // elsewhere are unaffected. + mapper.enable(DeserializationFeature.ACCEPT_SINGLE_VALUE_AS_ARRAY); + try { return mapper.readValue(new File(configFile), DynamicSettings.class); } catch (IOException ex) { @@ -272,18 +291,44 @@ public CodegenConfigurator addOperationIdNameMapping(String key, String value) { return this; } + /** + * Adds a single {@code injectModelVendorExtension}. Calling this repeatedly for the same key appends + * {@code value} as a new element to the list stored under {@code key}, in call order, rather than + * the later call overwriting the earlier one — so multiple {@code --inject-model-vendor-extensions} + * occurrences targeting the same key accumulate as distinct list entries instead of the last one + * silently winning. + */ public CodegenConfigurator addInjectModelVendorExtension(String key, String value) { - this.injectModelVendorExtensions.put(key, value); + this.injectModelVendorExtensions.computeIfAbsent(key, k -> new ArrayList<>()).add(value); generatorSettingsBuilder.withInjectModelVendorExtension(key, value); return this; } - public CodegenConfigurator setInjectModelVendorExtensions(Map extensions) { + public CodegenConfigurator setInjectModelVendorExtensions(Map> extensions) { this.injectModelVendorExtensions = extensions; generatorSettingsBuilder.withInjectModelVendorExtensions(extensions); return this; } + /** + * Adds a single {@code injectOperationVendorExtension}. Calling this repeatedly for the same key appends + * {@code value} as a new element to the list stored under {@code key}, in call order, rather than + * the later call overwriting the earlier one — so multiple {@code --inject-operation-vendor-extensions} + * occurrences targeting the same key accumulate as distinct list entries instead of the last one + * silently winning. + */ + public CodegenConfigurator addInjectOperationVendorExtension(String key, String value) { + this.injectOperationVendorExtensions.computeIfAbsent(key, k -> new ArrayList<>()).add(value); + generatorSettingsBuilder.withInjectOperationVendorExtension(key, value); + return this; + } + + public CodegenConfigurator setInjectOperationVendorExtensions(Map> extensions) { + this.injectOperationVendorExtensions = extensions; + generatorSettingsBuilder.withInjectOperationVendorExtensions(extensions); + return this; + } + public CodegenConfigurator addOpenapiNormalizer(String key, String value) { this.openapiNormalizer.put(key, value); generatorSettingsBuilder.withOpenapiNormalizer(key, value); @@ -804,6 +849,7 @@ public ClientOptInput toClientOptInput() { config.enumNameMapping().putAll(generatorSettings.getEnumNameMappings()); config.operationIdNameMapping().putAll(generatorSettings.getOperationIdNameMappings()); config.injectModelVendorExtensions().putAll(generatorSettings.getInjectModelVendorExtensions()); + config.injectOperationVendorExtensions().putAll(generatorSettings.getInjectOperationVendorExtensions()); config.openapiNormalizer().putAll(generatorSettings.getOpenapiNormalizer()); config.languageSpecificPrimitives().addAll(generatorSettings.getLanguageSpecificPrimitives()); config.openapiGeneratorIgnoreList().addAll(generatorSettings.getOpenapiGeneratorIgnoreList()); diff --git a/modules/openapi-generator/src/main/java/org/openapitools/codegen/config/CodegenConfiguratorUtils.java b/modules/openapi-generator/src/main/java/org/openapitools/codegen/config/CodegenConfiguratorUtils.java index 8b48e7fcf4e4..bca74be8a502 100644 --- a/modules/openapi-generator/src/main/java/org/openapitools/codegen/config/CodegenConfiguratorUtils.java +++ b/modules/openapi-generator/src/main/java/org/openapitools/codegen/config/CodegenConfiguratorUtils.java @@ -205,10 +205,48 @@ public static void applyInjectModelVendorExtensionsKvpList(List injectMo } public static void applyInjectModelVendorExtensionsKvp(String injectModelVendorExtensions, CodegenConfigurator configurator) { - final Map map = createMapFromKeyValuePairs(injectModelVendorExtensions); - for (Map.Entry entry : map.entrySet()) { - configurator.addInjectModelVendorExtension(entry.getKey().trim(), entry.getValue().trim()); + final Pair pair = parseSingleInjectVendorExtensionKvp(injectModelVendorExtensions); + if (pair != null) { + configurator.addInjectModelVendorExtension(pair.getLeft().trim(), pair.getRight().trim()); + } + } + + public static void applyInjectOperationVendorExtensionsKvpList(List injectOperationVendorExtensions, CodegenConfigurator configurator) { + for (String propString : injectOperationVendorExtensions) { + applyInjectOperationVendorExtensionsKvp(propString, configurator); + } + } + + public static void applyInjectOperationVendorExtensionsKvp(String injectOperationVendorExtensions, CodegenConfigurator configurator) { + final Pair pair = parseSingleInjectVendorExtensionKvp(injectOperationVendorExtensions); + if (pair != null) { + configurator.addInjectOperationVendorExtension(pair.getLeft().trim(), pair.getRight().trim()); + } + } + + /** + * Parses a single {@code key=value} pair for {@code --inject-model-vendor-extensions} / + * {@code --inject-operation-vendor-extensions} by splitting only on the first {@code =}. + *

+ * Unlike {@link #createMapFromKeyValuePairs}, this deliberately does NOT treat an unquoted comma + * as a separator between multiple injection targets: injected values are frequently annotation + * literals containing their own commas (e.g. {@code @Size(min = 1, max = 100)}), and comma-splitting + * would either mis-parse such a value into bogus targets, or require quoting the value in a way + * that leaves stray quote characters embedded in the rendered annotation. Each occurrence of the + * option is therefore exactly one injection target; use multiple occurrences for multiple targets. + * + * @param kvp the raw {@code key=value} string for one option occurrence + * @return the parsed key/value pair, or {@code null} if {@code kvp} has no {@code =} (or starts with one) + */ + private static Pair parseSingleInjectVendorExtensionKvp(String kvp) { + if (kvp == null) { + return null; + } + int ix = kvp.indexOf('='); + if (ix <= 0) { + return null; } + return Pair.of(kvp.substring(0, ix), kvp.substring(ix + 1)); } public static void applyTypeMappingsKvpList(List typeMappings, CodegenConfigurator configurator) { diff --git a/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/AbstractCSharpCodegen.java b/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/AbstractCSharpCodegen.java index ab46ba2bb4b2..7f693953585f 100644 --- a/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/AbstractCSharpCodegen.java +++ b/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/AbstractCSharpCodegen.java @@ -968,9 +968,12 @@ protected void patchProperty(Map enumRefs, CodegenModel mo // Normalize x-setter-visibility: // "public" -> remove extension, set isReadOnly=false (public setter = default, no modifier needed) // any other value -> set isReadOnly=true (template emits "{{.}} set;" using the extension value) - Object setterVisibilityObj = property.vendorExtensions.get("x-setter-visibility"); - if (setterVisibilityObj instanceof String) { - if ("public".equals(setterVisibilityObj)) { + // The value may be injected via --inject-model-vendor-extensions (a List, scalar-by-nature + // so only the first entry is honored) or authored directly in the spec as a plain string. + List setterVisibility = DefaultCodegen.getObjectAsStringList(property.vendorExtensions.get("x-setter-visibility")); + if (!setterVisibility.isEmpty()) { + String setterVisibilityValue = setterVisibility.get(0); + if ("public".equals(setterVisibilityValue)) { property.vendorExtensions.remove("x-setter-visibility"); property.isReadOnly = false; } else { diff --git a/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/AbstractJavaCodegen.java b/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/AbstractJavaCodegen.java index ceee9c985df0..fcf70b391642 100644 --- a/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/AbstractJavaCodegen.java +++ b/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/AbstractJavaCodegen.java @@ -2362,36 +2362,6 @@ private void normalizeModelPropertyVendorExtensions(CodegenModel model, String n } } - /** - * Normalizes an operation parameter vendor extension across all parameter collections. - * In this context, normalization means converting a missing value, a single string, or a list value - * into a predictable mutable {@code List} on each parameter. The same parameter can appear in - * several operation collections, so the collections are de-duplicated before updating the extension map. - * - * @param operation operation whose parameters should be updated - * @param name vendor extension name - */ - protected void normalizeOperationParameterVendorExtensions(CodegenOperation operation, String name) { - Set parameters = Collections.newSetFromMap(new IdentityHashMap<>()); - parameters.addAll(operation.allParams); - parameters.addAll(operation.bodyParams); - parameters.addAll(operation.pathParams); - parameters.addAll(operation.queryParams); - parameters.addAll(operation.headerParams); - parameters.addAll(operation.implicitHeadersParams); - parameters.addAll(operation.constantParams); - parameters.addAll(operation.formParams); - parameters.addAll(operation.cookieParams); - parameters.addAll(operation.requiredParams); - parameters.addAll(operation.optionalParams); - parameters.addAll(operation.requiredAndNotNullableParams); - parameters.addAll(operation.notNullableParams); - - for (CodegenParameter parameter : parameters) { - normalizeVendorExtensionWithStringList(parameter.vendorExtensions, name); - } - } - @Override public void preprocessOpenAPI(OpenAPI openAPI) { super.preprocessOpenAPI(openAPI); diff --git a/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/JavaCamelServerCodegen.java b/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/JavaCamelServerCodegen.java index 831c083aa0b1..eb8a34859149 100644 --- a/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/JavaCamelServerCodegen.java +++ b/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/JavaCamelServerCodegen.java @@ -20,6 +20,7 @@ import org.openapitools.codegen.CliOption; import org.openapitools.codegen.CodegenOperation; import org.openapitools.codegen.SupportingFile; +import org.openapitools.codegen.VendorExtension; import org.openapitools.codegen.languages.features.BeanValidationFeatures; import org.openapitools.codegen.languages.features.OptionalFeatures; import org.openapitools.codegen.languages.features.PerformBeanValidationFeatures; @@ -190,4 +191,13 @@ private T manageAdditionalProperty(String propertyName, T defaultValue) { private Boolean manageBooleanAdditionalProperty(String propertyValue) { return Boolean.parseBoolean(propertyValue); } + + @Override + public List getSupportedVendorExtensions() { + List extensions = super.getSupportedVendorExtensions(); + // The Camel REST DSL templates do not render a request-body parameter annotation, + // so this generator does not support the extension inherited from SpringCodegen. + extensions.remove(VendorExtension.X_REQUEST_BODY_EXTRA_ANNOTATION); + return extensions; + } } diff --git a/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/KotlinSpringServerCodegen.java b/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/KotlinSpringServerCodegen.java index 88fa847200aa..201238b01bbb 100644 --- a/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/KotlinSpringServerCodegen.java +++ b/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/KotlinSpringServerCodegen.java @@ -1758,6 +1758,14 @@ public OperationsMap postProcessOperationsWithModels(OperationsMap objs, List ops = operations.getOperation(); ops.forEach(operation -> { + // Normalize x-field-extra-annotation on each parameter (including the body) so a + // string or list value is handled uniformly and further values can be appended. + normalizeOperationParameterVendorExtensions(operation, VendorExtension.X_FIELD_EXTRA_ANNOTATION.getName()); + // x-request-body-extra-annotation is authored on the operation (the request body usually + // $refs a shared model, so it cannot be placed next to the $ref). Merge its values into the + // body parameter's x-field-extra-annotation so it renders through the same template path. + normalizeVendorExtensionWithStringList(operation.vendorExtensions, VendorExtension.X_REQUEST_BODY_EXTRA_ANNOTATION.getName()); + mergeOperationVendorExtensionIntoBodyParams(operation, VendorExtension.X_REQUEST_BODY_EXTRA_ANNOTATION.getName(), VendorExtension.X_FIELD_EXTRA_ANNOTATION.getName()); List responses = operation.responses; if (responses != null) { responses.forEach(resp -> { @@ -2001,6 +2009,7 @@ public List getSupportedVendorExtensions() { extensions.add(VendorExtension.X_FIELD_EXTRA_ANNOTATION); extensions.add(VendorExtension.X_JACKSON_DEFAULT_IMPL); extensions.add(VendorExtension.X_OPERATION_EXTRA_ANNOTATION); + extensions.add(VendorExtension.X_REQUEST_BODY_EXTRA_ANNOTATION); extensions.add(VendorExtension.X_EXTRA_IMPORTS); extensions.add(VendorExtension.X_PATTERN_MESSAGE); extensions.add(VendorExtension.X_SIZE_MESSAGE); diff --git a/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/SpringCodegen.java b/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/SpringCodegen.java index bf7e272e91a7..f59e1170d198 100644 --- a/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/SpringCodegen.java +++ b/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/SpringCodegen.java @@ -1144,6 +1144,11 @@ public void setIsVoid(boolean isVoid) { handleImplicitHeaders(operation); normalizeVendorExtensionWithStringList(operation.vendorExtensions, VendorExtension.X_OPERATION_EXTRA_ANNOTATION.getName()); normalizeOperationParameterVendorExtensions(operation, VendorExtension.X_FIELD_EXTRA_ANNOTATION.getName()); + // x-request-body-extra-annotation is authored on the operation (the request body usually + // $refs a shared model, so it cannot be placed next to the $ref). Merge its values into the + // body parameter's x-field-extra-annotation so it renders through the same template path. + normalizeVendorExtensionWithStringList(operation.vendorExtensions, VendorExtension.X_REQUEST_BODY_EXTRA_ANNOTATION.getName()); + mergeOperationVendorExtensionIntoBodyParams(operation, VendorExtension.X_REQUEST_BODY_EXTRA_ANNOTATION.getName(), VendorExtension.X_FIELD_EXTRA_ANNOTATION.getName()); if (useSpringSecurityPreAuthorize) { addSpringSecurityPreAuthorize(operation); @@ -1812,6 +1817,7 @@ public void setUseSwaggerUI(boolean useSwaggerUI) { public List getSupportedVendorExtensions() { List extensions = super.getSupportedVendorExtensions(); extensions.add(VendorExtension.X_OPERATION_EXTRA_ANNOTATION); + extensions.add(VendorExtension.X_REQUEST_BODY_EXTRA_ANNOTATION); extensions.add(VendorExtension.X_SPRING_PAGINATED); extensions.add(VendorExtension.X_VERSION_PARAM); extensions.add(VendorExtension.X_PATTERN_MESSAGE); diff --git a/modules/openapi-generator/src/main/resources/JavaSpring/bodyParams.mustache b/modules/openapi-generator/src/main/resources/JavaSpring/bodyParams.mustache index 411135a6f963..8e2a1c6c0e61 100644 --- a/modules/openapi-generator/src/main/resources/JavaSpring/bodyParams.mustache +++ b/modules/openapi-generator/src/main/resources/JavaSpring/bodyParams.mustache @@ -1 +1 @@ -{{#isBodyParam}}{{>paramDoc}}{{#useBeanValidation}} {{>beanValidationBodyParams}}{{#reactive}}@Valid {{/reactive}}{{^reactive}}{{^isContainer}}@Valid {{/isContainer}}{{/reactive}}{{/useBeanValidation}}{{^useBeanValidation}} {{/useBeanValidation}}@RequestBody{{^required}}(required = false){{/required}} {{^reactive}}{{>nullableAnnotation}}{{>optionalDataType}}{{/reactive}}{{#reactive}}{{^isArray}}Mono<{{{dataType}}}>{{/isArray}}{{#isArray}}Flux<{{{baseType}}}>{{/isArray}}{{/reactive}} {{paramName}}{{/isBodyParam}} \ No newline at end of file +{{#isBodyParam}}{{>paramDoc}}{{#vendorExtensions.x-field-extra-annotation}} {{{.}}}{{/vendorExtensions.x-field-extra-annotation}}{{#useBeanValidation}} {{>beanValidationBodyParams}}{{#reactive}}@Valid {{/reactive}}{{^reactive}}{{^isContainer}}@Valid {{/isContainer}}{{/reactive}}{{/useBeanValidation}}{{^useBeanValidation}} {{/useBeanValidation}}@RequestBody{{^required}}(required = false){{/required}} {{^reactive}}{{>nullableAnnotation}}{{>optionalDataType}}{{/reactive}}{{#reactive}}{{^isArray}}Mono<{{{dataType}}}>{{/isArray}}{{#isArray}}Flux<{{{baseType}}}>{{/isArray}}{{/reactive}} {{paramName}}{{/isBodyParam}} \ No newline at end of file diff --git a/modules/openapi-generator/src/main/resources/JavaSpring/cookieParams.mustache b/modules/openapi-generator/src/main/resources/JavaSpring/cookieParams.mustache index a255b5c7daf2..87865ce82f1b 100644 --- a/modules/openapi-generator/src/main/resources/JavaSpring/cookieParams.mustache +++ b/modules/openapi-generator/src/main/resources/JavaSpring/cookieParams.mustache @@ -1 +1 @@ -{{#isCookieParam}}{{#useBeanValidation}}{{>beanValidationQueryParams}}{{/useBeanValidation}}{{>paramDoc}} @CookieValue(name = "{{baseName}}"{{^required}}, required = false{{/required}}{{#defaultValue}}, defaultValue = "{{{.}}}"{{/defaultValue}}){{>dateTimeParam}} {{>nullableAnnotation}}{{>optionalDataType}} {{paramName}}{{/isCookieParam}} \ No newline at end of file +{{#isCookieParam}}{{#vendorExtensions.x-field-extra-annotation}}{{{.}}} {{/vendorExtensions.x-field-extra-annotation}}{{#useBeanValidation}}{{>beanValidationQueryParams}}{{/useBeanValidation}}{{>paramDoc}} @CookieValue(name = "{{baseName}}"{{^required}}, required = false{{/required}}{{#defaultValue}}, defaultValue = "{{{.}}}"{{/defaultValue}}){{>dateTimeParam}} {{>nullableAnnotation}}{{>optionalDataType}} {{paramName}}{{/isCookieParam}} \ No newline at end of file diff --git a/modules/openapi-generator/src/main/resources/kotlin-spring/bodyParams.mustache b/modules/openapi-generator/src/main/resources/kotlin-spring/bodyParams.mustache index b4f03475cd0b..c44f1a1740bc 100644 --- a/modules/openapi-generator/src/main/resources/kotlin-spring/bodyParams.mustache +++ b/modules/openapi-generator/src/main/resources/kotlin-spring/bodyParams.mustache @@ -1 +1 @@ -{{#isBodyParam}}{{#swagger2AnnotationLibrary}}@Parameter(description = "{{{description}}}"{{#required}}, required = true{{/required}}{{^isContainer}}{{#allowableValues}}{{#defaultValue}}, schema = Schema(allowableValues = ["{{{allowableValues}}}"], defaultValue = {{^isString}}"{{/isString}}{{#isString}}{{#isEnum}}"{{/isEnum}}{{/isString}}{{{defaultValue}}}{{^isString}}"{{/isString}}{{#isString}}{{#isEnum}}"{{/isEnum}}{{/isString}}){{/defaultValue}}{{/allowableValues}}{{^allowableValues}}{{#defaultValue}}, schema = Schema(defaultValue = {{^isString}}"{{/isString}}{{#isString}}{{#isEnum}}"{{/isEnum}}{{/isString}}{{{defaultValue}}}{{^isString}}"{{/isString}}{{#isString}}{{#isEnum}}"{{/isEnum}}{{/isString}}){{/defaultValue}}{{/allowableValues}}{{#allowableValues}}{{^defaultValue}}, schema = Schema(allowableValues = ["{{{allowableValues}}}"]){{/defaultValue}}{{/allowableValues}}{{/isContainer}}) {{/swagger2AnnotationLibrary}}{{#swagger1AnnotationLibrary}}@ApiParam(value = "{{{description}}}"{{#required}}, required = true{{/required}}{{^isContainer}}{{#allowableValues}}, allowableValues = "{{{.}}}"{{/allowableValues}}{{/isContainer}}{{#defaultValue}}, defaultValue = "{{{.}}}"{{/defaultValue}}) {{/swagger1AnnotationLibrary}}{{#useBeanValidation}}@Valid{{>beanValidationBodyParams}}{{/useBeanValidation}} @RequestBody{{^required}}(required = false){{/required}} {{{paramName}}}: {{^reactive}}{{>optionalDataType}}{{/reactive}}{{#reactive}}{{^isArray}}{{>optionalDataType}}{{/isArray}}{{#isArray}}Flow<{{{baseType}}}>{{/isArray}}{{/reactive}}{{/isBodyParam}} \ No newline at end of file +{{#isBodyParam}}{{#swagger2AnnotationLibrary}}@Parameter(description = "{{{description}}}"{{#required}}, required = true{{/required}}{{^isContainer}}{{#allowableValues}}{{#defaultValue}}, schema = Schema(allowableValues = ["{{{allowableValues}}}"], defaultValue = {{^isString}}"{{/isString}}{{#isString}}{{#isEnum}}"{{/isEnum}}{{/isString}}{{{defaultValue}}}{{^isString}}"{{/isString}}{{#isString}}{{#isEnum}}"{{/isEnum}}{{/isString}}){{/defaultValue}}{{/allowableValues}}{{^allowableValues}}{{#defaultValue}}, schema = Schema(defaultValue = {{^isString}}"{{/isString}}{{#isString}}{{#isEnum}}"{{/isEnum}}{{/isString}}{{{defaultValue}}}{{^isString}}"{{/isString}}{{#isString}}{{#isEnum}}"{{/isEnum}}{{/isString}}){{/defaultValue}}{{/allowableValues}}{{#allowableValues}}{{^defaultValue}}, schema = Schema(allowableValues = ["{{{allowableValues}}}"]){{/defaultValue}}{{/allowableValues}}{{/isContainer}}) {{/swagger2AnnotationLibrary}}{{#swagger1AnnotationLibrary}}@ApiParam(value = "{{{description}}}"{{#required}}, required = true{{/required}}{{^isContainer}}{{#allowableValues}}, allowableValues = "{{{.}}}"{{/allowableValues}}{{/isContainer}}{{#defaultValue}}, defaultValue = "{{{.}}}"{{/defaultValue}}) {{/swagger1AnnotationLibrary}}{{#vendorExtensions.x-field-extra-annotation}}{{{.}}} {{/vendorExtensions.x-field-extra-annotation}}{{#useBeanValidation}}@Valid{{>beanValidationBodyParams}}{{/useBeanValidation}} @RequestBody{{^required}}(required = false){{/required}} {{{paramName}}}: {{^reactive}}{{>optionalDataType}}{{/reactive}}{{#reactive}}{{^isArray}}{{>optionalDataType}}{{/isArray}}{{#isArray}}Flow<{{{baseType}}}>{{/isArray}}{{/reactive}}{{/isBodyParam}} \ No newline at end of file diff --git a/modules/openapi-generator/src/main/resources/kotlin-spring/cookieParams.mustache b/modules/openapi-generator/src/main/resources/kotlin-spring/cookieParams.mustache index 028264a18bcf..acce5461e358 100644 --- a/modules/openapi-generator/src/main/resources/kotlin-spring/cookieParams.mustache +++ b/modules/openapi-generator/src/main/resources/kotlin-spring/cookieParams.mustache @@ -1 +1 @@ -{{#isCookieParam}}{{#useBeanValidation}}{{>beanValidationQueryParams}}{{/useBeanValidation}}@CookieValue(name = "{{baseName}}"{{^required}}, required = false{{/required}}{{#defaultValue}}, defaultValue = "{{{.}}}"{{/defaultValue}}) {{{paramName}}}: {{>optionalDataType}}{{/isCookieParam}} \ No newline at end of file +{{#isCookieParam}}{{#vendorExtensions.x-field-extra-annotation}}{{{.}}} {{/vendorExtensions.x-field-extra-annotation}}{{#useBeanValidation}}{{>beanValidationQueryParams}}{{/useBeanValidation}}@CookieValue(name = "{{baseName}}"{{^required}}, required = false{{/required}}{{#defaultValue}}, defaultValue = "{{{.}}}"{{/defaultValue}}) {{{paramName}}}: {{>optionalDataType}}{{/isCookieParam}} \ No newline at end of file diff --git a/modules/openapi-generator/src/main/resources/kotlin-spring/formParams.mustache b/modules/openapi-generator/src/main/resources/kotlin-spring/formParams.mustache index 456af893718f..4cc39f195fb8 100644 --- a/modules/openapi-generator/src/main/resources/kotlin-spring/formParams.mustache +++ b/modules/openapi-generator/src/main/resources/kotlin-spring/formParams.mustache @@ -1 +1 @@ -{{#isFormParam}}{{^isFile}}{{#swagger2AnnotationLibrary}}@Parameter(description = "{{{description}}}"{{#required}}, required = true{{/required}}{{#allowableValues}}{{#defaultValue}}, schema = Schema(allowableValues = [{{#values}}"{{{.}}}"{{^-last}}, {{/-last}}{{/values}}]{{^isContainer}}, defaultValue = {{^isString}}"{{/isString}}{{#isString}}{{#isEnum}}"{{/isEnum}}{{/isString}}{{{defaultValue}}}{{^isString}}"{{/isString}}{{#isString}}{{#isEnum}}"{{/isEnum}}{{/isString}}{{/isContainer}}){{/defaultValue}}{{/allowableValues}}{{#allowableValues}}{{^defaultValue}}, schema = Schema(allowableValues = [{{#values}}"{{{.}}}"{{^-last}}, {{/-last}}{{/values}}]){{/defaultValue}}{{/allowableValues}}{{^allowableValues}}{{#defaultValue}}{{^isContainer}}, schema = Schema(defaultValue = {{^isString}}"{{/isString}}{{#isString}}{{#isEnum}}"{{/isEnum}}{{/isString}}{{{defaultValue}}}{{^isString}}"{{/isString}}{{#isString}}{{#isEnum}}"{{/isEnum}}{{/isString}}){{/isContainer}}{{/defaultValue}}{{/allowableValues}}) {{/swagger2AnnotationLibrary}}{{#swagger1AnnotationLibrary}}@ApiParam(value = "{{{description}}}"{{#required}}, required = true{{/required}}{{#allowableValues}}, allowableValues = "{{#values}}{{{.}}}{{^-last}}, {{/-last}}{{/values}}"{{/allowableValues}}{{#defaultValue}}, defaultValue = "{{{.}}}"{{/defaultValue}}) {{/swagger1AnnotationLibrary}}{{#useBeanValidation}}@Valid {{/useBeanValidation}}{{#isModel}}@RequestPart{{/isModel}}{{^isModel}}@RequestParam{{/isModel}}(value = "{{baseName}}"{{#required}}, required = true{{/required}}{{^required}}, required = false{{/required}}) {{{paramName}}}: {{>optionalDataType}}{{/isFile}}{{#isFile}}{{#swagger2AnnotationLibrary}}@Parameter(description = "{{{description}}}") {{/swagger2AnnotationLibrary}}{{#swagger1AnnotationLibrary}}@ApiParam(value = "file detail") {{/swagger1AnnotationLibrary}}{{#useBeanValidation}}@Valid{{/useBeanValidation}} @RequestPart("{{baseName}}"{{#required}}, required = true{{/required}}{{^required}}, required = false{{/required}}) {{{paramName}}}: {{>optionalDataType}}{{/isFile}}{{/isFormParam}} \ No newline at end of file +{{#isFormParam}}{{^isFile}}{{#vendorExtensions.x-field-extra-annotation}}{{{.}}} {{/vendorExtensions.x-field-extra-annotation}}{{#swagger2AnnotationLibrary}}@Parameter(description = "{{{description}}}"{{#required}}, required = true{{/required}}{{#allowableValues}}{{#defaultValue}}, schema = Schema(allowableValues = [{{#values}}"{{{.}}}"{{^-last}}, {{/-last}}{{/values}}]{{^isContainer}}, defaultValue = {{^isString}}"{{/isString}}{{#isString}}{{#isEnum}}"{{/isEnum}}{{/isString}}{{{defaultValue}}}{{^isString}}"{{/isString}}{{#isString}}{{#isEnum}}"{{/isEnum}}{{/isString}}{{/isContainer}}){{/defaultValue}}{{/allowableValues}}{{#allowableValues}}{{^defaultValue}}, schema = Schema(allowableValues = [{{#values}}"{{{.}}}"{{^-last}}, {{/-last}}{{/values}}]){{/defaultValue}}{{/allowableValues}}{{^allowableValues}}{{#defaultValue}}{{^isContainer}}, schema = Schema(defaultValue = {{^isString}}"{{/isString}}{{#isString}}{{#isEnum}}"{{/isEnum}}{{/isString}}{{{defaultValue}}}{{^isString}}"{{/isString}}{{#isString}}{{#isEnum}}"{{/isEnum}}{{/isString}}){{/isContainer}}{{/defaultValue}}{{/allowableValues}}) {{/swagger2AnnotationLibrary}}{{#swagger1AnnotationLibrary}}@ApiParam(value = "{{{description}}}"{{#required}}, required = true{{/required}}{{#allowableValues}}, allowableValues = "{{#values}}{{{.}}}{{^-last}}, {{/-last}}{{/values}}"{{/allowableValues}}{{#defaultValue}}, defaultValue = "{{{.}}}"{{/defaultValue}}) {{/swagger1AnnotationLibrary}}{{#useBeanValidation}}@Valid {{/useBeanValidation}}{{#isModel}}@RequestPart{{/isModel}}{{^isModel}}@RequestParam{{/isModel}}(value = "{{baseName}}"{{#required}}, required = true{{/required}}{{^required}}, required = false{{/required}}) {{{paramName}}}: {{>optionalDataType}}{{/isFile}}{{#isFile}}{{#vendorExtensions.x-field-extra-annotation}}{{{.}}} {{/vendorExtensions.x-field-extra-annotation}}{{#swagger2AnnotationLibrary}}@Parameter(description = "{{{description}}}") {{/swagger2AnnotationLibrary}}{{#swagger1AnnotationLibrary}}@ApiParam(value = "file detail") {{/swagger1AnnotationLibrary}}{{#useBeanValidation}}@Valid{{/useBeanValidation}} @RequestPart("{{baseName}}"{{#required}}, required = true{{/required}}{{^required}}, required = false{{/required}}) {{{paramName}}}: {{>optionalDataType}}{{/isFile}}{{/isFormParam}} \ No newline at end of file diff --git a/modules/openapi-generator/src/main/resources/kotlin-spring/headerParams.mustache b/modules/openapi-generator/src/main/resources/kotlin-spring/headerParams.mustache index 0c2678f1bf67..a29b151e1c64 100644 --- a/modules/openapi-generator/src/main/resources/kotlin-spring/headerParams.mustache +++ b/modules/openapi-generator/src/main/resources/kotlin-spring/headerParams.mustache @@ -1 +1 @@ -{{#isHeaderParam}}{{#useBeanValidation}}{{>beanValidationCore}}{{/useBeanValidation}}{{#swagger2AnnotationLibrary}}@Parameter(description = "{{{description}}}", `in` = ParameterIn.HEADER{{#required}}, required = true{{/required}}{{#allowableValues}}{{#defaultValue}}, schema = Schema(allowableValues = [{{#values}}"{{{.}}}"{{^-last}}, {{/-last}}{{/values}}]{{^isContainer}}, defaultValue = {{^isString}}"{{/isString}}{{#isString}}{{#isEnum}}"{{/isEnum}}{{/isString}}{{{defaultValue}}}{{^isString}}"{{/isString}}{{#isString}}{{#isEnum}}"{{/isEnum}}{{/isString}}{{/isContainer}}){{/defaultValue}}{{/allowableValues}}{{#allowableValues}}{{^defaultValue}}, schema = Schema(allowableValues = [{{#values}}"{{{.}}}"{{^-last}}, {{/-last}}{{/values}}]){{/defaultValue}}{{/allowableValues}}{{^allowableValues}}{{#defaultValue}}{{^isContainer}}, schema = Schema(defaultValue = {{^isString}}"{{/isString}}{{#isString}}{{#isEnum}}"{{/isEnum}}{{/isString}}{{{defaultValue}}}{{^isString}}"{{/isString}}{{#isString}}{{#isEnum}}"{{/isEnum}}{{/isString}}){{/isContainer}}{{/defaultValue}}{{/allowableValues}}) {{/swagger2AnnotationLibrary}}{{#swagger1AnnotationLibrary}}@ApiParam(value = "{{{description}}}"{{#required}}, required = true{{/required}}{{#allowableValues}}, allowableValues = "{{#values}}{{{.}}}{{^-last}}, {{/-last}}{{/values}}"{{/allowableValues}}{{#defaultValue}}, defaultValue = "{{{.}}}"{{/defaultValue}}) {{/swagger1AnnotationLibrary}}@RequestHeader(value = "{{baseName}}", required = {{#required}}true{{/required}}{{^required}}false{{/required}}{{#defaultValue}}, defaultValue = {{^isString}}"{{{.}}}"{{/isString}}{{#isString}}{{#isEnum}}"{{{.}}}"{{/isEnum}}{{^isEnum}}{{{.}}}{{/isEnum}}{{/isString}}{{/defaultValue}}) {{{paramName}}}: {{>optionalDataType}}{{/isHeaderParam}} \ No newline at end of file +{{#isHeaderParam}}{{#vendorExtensions.x-field-extra-annotation}}{{{.}}} {{/vendorExtensions.x-field-extra-annotation}}{{#useBeanValidation}}{{>beanValidationCore}}{{/useBeanValidation}}{{#swagger2AnnotationLibrary}}@Parameter(description = "{{{description}}}", `in` = ParameterIn.HEADER{{#required}}, required = true{{/required}}{{#allowableValues}}{{#defaultValue}}, schema = Schema(allowableValues = [{{#values}}"{{{.}}}"{{^-last}}, {{/-last}}{{/values}}]{{^isContainer}}, defaultValue = {{^isString}}"{{/isString}}{{#isString}}{{#isEnum}}"{{/isEnum}}{{/isString}}{{{defaultValue}}}{{^isString}}"{{/isString}}{{#isString}}{{#isEnum}}"{{/isEnum}}{{/isString}}{{/isContainer}}){{/defaultValue}}{{/allowableValues}}{{#allowableValues}}{{^defaultValue}}, schema = Schema(allowableValues = [{{#values}}"{{{.}}}"{{^-last}}, {{/-last}}{{/values}}]){{/defaultValue}}{{/allowableValues}}{{^allowableValues}}{{#defaultValue}}{{^isContainer}}, schema = Schema(defaultValue = {{^isString}}"{{/isString}}{{#isString}}{{#isEnum}}"{{/isEnum}}{{/isString}}{{{defaultValue}}}{{^isString}}"{{/isString}}{{#isString}}{{#isEnum}}"{{/isEnum}}{{/isString}}){{/isContainer}}{{/defaultValue}}{{/allowableValues}}) {{/swagger2AnnotationLibrary}}{{#swagger1AnnotationLibrary}}@ApiParam(value = "{{{description}}}"{{#required}}, required = true{{/required}}{{#allowableValues}}, allowableValues = "{{#values}}{{{.}}}{{^-last}}, {{/-last}}{{/values}}"{{/allowableValues}}{{#defaultValue}}, defaultValue = "{{{.}}}"{{/defaultValue}}) {{/swagger1AnnotationLibrary}}@RequestHeader(value = "{{baseName}}", required = {{#required}}true{{/required}}{{^required}}false{{/required}}{{#defaultValue}}, defaultValue = {{^isString}}"{{{.}}}"{{/isString}}{{#isString}}{{#isEnum}}"{{{.}}}"{{/isEnum}}{{^isEnum}}{{{.}}}{{/isEnum}}{{/isString}}{{/defaultValue}}) {{{paramName}}}: {{>optionalDataType}}{{/isHeaderParam}} \ No newline at end of file diff --git a/modules/openapi-generator/src/main/resources/kotlin-spring/pathParams.mustache b/modules/openapi-generator/src/main/resources/kotlin-spring/pathParams.mustache index 2e28d18c78fa..3f22fecbbd9b 100644 --- a/modules/openapi-generator/src/main/resources/kotlin-spring/pathParams.mustache +++ b/modules/openapi-generator/src/main/resources/kotlin-spring/pathParams.mustache @@ -1 +1 @@ -{{#isPathParam}}{{#useBeanValidation}}{{>beanValidationPathParams}}{{/useBeanValidation}}{{#swagger2AnnotationLibrary}}@Parameter(description = "{{{description}}}"{{#required}}, required = true{{/required}}{{#allowableValues}}{{#defaultValue}}, schema = Schema(allowableValues = [{{#values}}"{{{.}}}"{{^-last}}, {{/-last}}{{/values}}]{{^isContainer}}, defaultValue = {{^isString}}"{{/isString}}{{#isString}}{{#isEnum}}"{{/isEnum}}{{/isString}}{{{defaultValue}}}{{^isString}}"{{/isString}}{{#isString}}{{#isEnum}}"{{/isEnum}}{{/isString}}{{/isContainer}}){{/defaultValue}}{{/allowableValues}}{{#allowableValues}}{{^defaultValue}}, schema = Schema(allowableValues = [{{#values}}"{{{.}}}"{{^-last}}, {{/-last}}{{/values}}]){{/defaultValue}}{{/allowableValues}}{{^allowableValues}}{{#defaultValue}}{{^isContainer}}, schema = Schema(defaultValue = {{^isString}}"{{/isString}}{{#isString}}{{#isEnum}}"{{/isEnum}}{{/isString}}{{{defaultValue}}}{{^isString}}"{{/isString}}{{#isString}}{{#isEnum}}"{{/isEnum}}{{/isString}}){{/isContainer}}{{/defaultValue}}{{/allowableValues}}) {{/swagger2AnnotationLibrary}}{{#swagger1AnnotationLibrary}}@ApiParam(value = "{{{description}}}"{{#required}}, required = true{{/required}}{{#allowableValues}}, allowableValues = "{{#values}}{{{.}}}{{^-last}}, {{/-last}}{{/values}}"{{/allowableValues}}{{#defaultValue}}, defaultValue = "{{{.}}}"{{/defaultValue}}) {{/swagger1AnnotationLibrary}}@PathVariable("{{baseName}}") {{{paramName}}}: {{>optionalDataType}}{{/isPathParam}} \ No newline at end of file +{{#isPathParam}}{{#vendorExtensions.x-field-extra-annotation}}{{{.}}} {{/vendorExtensions.x-field-extra-annotation}}{{#useBeanValidation}}{{>beanValidationPathParams}}{{/useBeanValidation}}{{#swagger2AnnotationLibrary}}@Parameter(description = "{{{description}}}"{{#required}}, required = true{{/required}}{{#allowableValues}}{{#defaultValue}}, schema = Schema(allowableValues = [{{#values}}"{{{.}}}"{{^-last}}, {{/-last}}{{/values}}]{{^isContainer}}, defaultValue = {{^isString}}"{{/isString}}{{#isString}}{{#isEnum}}"{{/isEnum}}{{/isString}}{{{defaultValue}}}{{^isString}}"{{/isString}}{{#isString}}{{#isEnum}}"{{/isEnum}}{{/isString}}{{/isContainer}}){{/defaultValue}}{{/allowableValues}}{{#allowableValues}}{{^defaultValue}}, schema = Schema(allowableValues = [{{#values}}"{{{.}}}"{{^-last}}, {{/-last}}{{/values}}]){{/defaultValue}}{{/allowableValues}}{{^allowableValues}}{{#defaultValue}}{{^isContainer}}, schema = Schema(defaultValue = {{^isString}}"{{/isString}}{{#isString}}{{#isEnum}}"{{/isEnum}}{{/isString}}{{{defaultValue}}}{{^isString}}"{{/isString}}{{#isString}}{{#isEnum}}"{{/isEnum}}{{/isString}}){{/isContainer}}{{/defaultValue}}{{/allowableValues}}) {{/swagger2AnnotationLibrary}}{{#swagger1AnnotationLibrary}}@ApiParam(value = "{{{description}}}"{{#required}}, required = true{{/required}}{{#allowableValues}}, allowableValues = "{{#values}}{{{.}}}{{^-last}}, {{/-last}}{{/values}}"{{/allowableValues}}{{#defaultValue}}, defaultValue = "{{{.}}}"{{/defaultValue}}) {{/swagger1AnnotationLibrary}}@PathVariable("{{baseName}}") {{{paramName}}}: {{>optionalDataType}}{{/isPathParam}} \ No newline at end of file diff --git a/modules/openapi-generator/src/main/resources/kotlin-spring/queryParams.mustache b/modules/openapi-generator/src/main/resources/kotlin-spring/queryParams.mustache index 27d7e286bb33..551811ddbe23 100644 --- a/modules/openapi-generator/src/main/resources/kotlin-spring/queryParams.mustache +++ b/modules/openapi-generator/src/main/resources/kotlin-spring/queryParams.mustache @@ -1 +1 @@ -{{#isQueryParam}}{{#useBeanValidation}}{{>beanValidationQueryParams}}{{/useBeanValidation}}{{#swagger2AnnotationLibrary}}@Parameter(description = "{{{description}}}"{{#required}}, required = true{{/required}}{{#allowableValues}}{{#defaultValue}}, schema = Schema(allowableValues = [{{#values}}"{{{.}}}"{{^-last}}, {{/-last}}{{/values}}]{{^isContainer}}, defaultValue = {{^isString}}"{{/isString}}{{#isString}}{{#isEnum}}"{{/isEnum}}{{/isString}}{{{defaultValue}}}{{^isString}}"{{/isString}}{{#isString}}{{#isEnum}}"{{/isEnum}}{{/isString}}{{/isContainer}}){{/defaultValue}}{{/allowableValues}}{{#allowableValues}}{{^defaultValue}}, schema = Schema(allowableValues = [{{#values}}"{{{.}}}"{{^-last}}, {{/-last}}{{/values}}]){{/defaultValue}}{{/allowableValues}}{{^allowableValues}}{{#defaultValue}}{{^isContainer}}, schema = Schema(defaultValue = {{^isString}}"{{/isString}}{{#isString}}{{#isEnum}}"{{/isEnum}}{{/isString}}{{{defaultValue}}}{{^isString}}"{{/isString}}{{#isString}}{{#isEnum}}"{{/isEnum}}{{/isString}}){{/isContainer}}{{/defaultValue}}{{/allowableValues}}) {{/swagger2AnnotationLibrary}}{{#swagger1AnnotationLibrary}}@ApiParam(value = "{{{description}}}"{{#required}}, required = true{{/required}}{{#allowableValues}}, allowableValues = "{{#values}}{{{.}}}{{^-last}}, {{/-last}}{{/values}}"{{/allowableValues}}{{^isContainer}}{{#defaultValue}}, defaultValue = {{^isString}}"{{/isString}}{{#isString}}{{#isEnum}}"{{/isEnum}}{{/isString}}{{{defaultValue}}}{{^isString}}"{{/isString}}{{#isString}}{{#isEnum}}"{{/isEnum}}{{/isString}}{{/defaultValue}}{{/isContainer}}) {{/swagger1AnnotationLibrary}}{{#useBeanValidation}}@Valid{{/useBeanValidation}}{{^isModel}} @RequestParam(value = "{{baseName}}"{{#required}}, required = true{{/required}}{{^required}}, required = false{{/required}}{{^isContainer}}{{#defaultValue}}, defaultValue = {{^isString}}"{{/isString}}{{#isString}}{{#isEnum}}"{{/isEnum}}{{/isString}}{{{defaultValue}}}{{^isString}}"{{/isString}}{{#isString}}{{#isEnum}}"{{/isEnum}}{{/isString}}{{/defaultValue}}{{/isContainer}}){{/isModel}}{{#isDate}} @org.springframework.format.annotation.DateTimeFormat(iso = org.springframework.format.annotation.DateTimeFormat.ISO.DATE){{/isDate}}{{#isDateTime}} @org.springframework.format.annotation.DateTimeFormat(iso = org.springframework.format.annotation.DateTimeFormat.ISO.DATE_TIME){{/isDateTime}} {{{paramName}}}: {{>optionalDataType}}{{/isQueryParam}} \ No newline at end of file +{{#isQueryParam}}{{#vendorExtensions.x-field-extra-annotation}}{{{.}}} {{/vendorExtensions.x-field-extra-annotation}}{{#useBeanValidation}}{{>beanValidationQueryParams}}{{/useBeanValidation}}{{#swagger2AnnotationLibrary}}@Parameter(description = "{{{description}}}"{{#required}}, required = true{{/required}}{{#allowableValues}}{{#defaultValue}}, schema = Schema(allowableValues = [{{#values}}"{{{.}}}"{{^-last}}, {{/-last}}{{/values}}]{{^isContainer}}, defaultValue = {{^isString}}"{{/isString}}{{#isString}}{{#isEnum}}"{{/isEnum}}{{/isString}}{{{defaultValue}}}{{^isString}}"{{/isString}}{{#isString}}{{#isEnum}}"{{/isEnum}}{{/isString}}{{/isContainer}}){{/defaultValue}}{{/allowableValues}}{{#allowableValues}}{{^defaultValue}}, schema = Schema(allowableValues = [{{#values}}"{{{.}}}"{{^-last}}, {{/-last}}{{/values}}]){{/defaultValue}}{{/allowableValues}}{{^allowableValues}}{{#defaultValue}}{{^isContainer}}, schema = Schema(defaultValue = {{^isString}}"{{/isString}}{{#isString}}{{#isEnum}}"{{/isEnum}}{{/isString}}{{{defaultValue}}}{{^isString}}"{{/isString}}{{#isString}}{{#isEnum}}"{{/isEnum}}{{/isString}}){{/isContainer}}{{/defaultValue}}{{/allowableValues}}) {{/swagger2AnnotationLibrary}}{{#swagger1AnnotationLibrary}}@ApiParam(value = "{{{description}}}"{{#required}}, required = true{{/required}}{{#allowableValues}}, allowableValues = "{{#values}}{{{.}}}{{^-last}}, {{/-last}}{{/values}}"{{/allowableValues}}{{^isContainer}}{{#defaultValue}}, defaultValue = {{^isString}}"{{/isString}}{{#isString}}{{#isEnum}}"{{/isEnum}}{{/isString}}{{{defaultValue}}}{{^isString}}"{{/isString}}{{#isString}}{{#isEnum}}"{{/isEnum}}{{/isString}}{{/defaultValue}}{{/isContainer}}) {{/swagger1AnnotationLibrary}}{{#useBeanValidation}}@Valid{{/useBeanValidation}}{{^isModel}} @RequestParam(value = "{{baseName}}"{{#required}}, required = true{{/required}}{{^required}}, required = false{{/required}}{{^isContainer}}{{#defaultValue}}, defaultValue = {{^isString}}"{{/isString}}{{#isString}}{{#isEnum}}"{{/isEnum}}{{/isString}}{{{defaultValue}}}{{^isString}}"{{/isString}}{{#isString}}{{#isEnum}}"{{/isEnum}}{{/isString}}{{/defaultValue}}{{/isContainer}}){{/isModel}}{{#isDate}} @org.springframework.format.annotation.DateTimeFormat(iso = org.springframework.format.annotation.DateTimeFormat.ISO.DATE){{/isDate}}{{#isDateTime}} @org.springframework.format.annotation.DateTimeFormat(iso = org.springframework.format.annotation.DateTimeFormat.ISO.DATE_TIME){{/isDateTime}} {{{paramName}}}: {{>optionalDataType}}{{/isQueryParam}} \ No newline at end of file diff --git a/modules/openapi-generator/src/test/java/org/openapitools/codegen/DefaultCodegenTest.java b/modules/openapi-generator/src/test/java/org/openapitools/codegen/DefaultCodegenTest.java index e94a6d5e5e72..cf059ed54e13 100644 --- a/modules/openapi-generator/src/test/java/org/openapitools/codegen/DefaultCodegenTest.java +++ b/modules/openapi-generator/src/test/java/org/openapitools/codegen/DefaultCodegenTest.java @@ -31,12 +31,14 @@ import io.swagger.v3.oas.models.PathItem; import io.swagger.v3.oas.models.headers.Header; import io.swagger.v3.oas.models.media.*; +import io.swagger.v3.oas.models.parameters.Parameter; import io.swagger.v3.oas.models.parameters.QueryParameter; import io.swagger.v3.oas.models.parameters.RequestBody; import io.swagger.v3.oas.models.responses.ApiResponse; import io.swagger.v3.oas.models.responses.ApiResponses; import io.swagger.v3.oas.models.security.SecurityScheme; import io.swagger.v3.parser.core.models.ParseOptions; +import org.apache.commons.lang3.StringUtils; import org.junit.jupiter.api.Assertions; import org.openapitools.codegen.config.CodegenConfigurator; import org.openapitools.codegen.config.GlobalSettings; @@ -161,6 +163,247 @@ public void testEnumImports() { assertEquals(1, Sets.intersection(operation.imports, Sets.newHashSet("PetByType")).size()); } + @Test + public void testInjectOperationVendorExtensions() { + final DefaultCodegen codegen = new DefaultCodegen(); + final OpenAPI openApi = TestUtils.parseFlattenSpec("src/test/resources/3_0/inject-operation-vendor-extensions.yaml"); + codegen.setOpenAPI(openApi); + codegen.injectOperationVendorExtensions().put("createEmployee.x-request-body-extra-annotation", List.of("@com.example.MyValidation")); + codegen.injectOperationVendorExtensions().put("createEmployee.orgId.x-field-extra-annotation", List.of("@com.example.ValidOrgId")); + // non-matching operationId is a no-op + codegen.injectOperationVendorExtensions().put("noSuchOperation.x-foo", List.of("bar")); + + PathItem path = openApi.getPaths().get("/orgs/{orgId}/employees"); + CodegenOperation operation = codegen.fromOperation("/orgs/{orgId}/employees", "post", path.getPost(), path.getServers()); + + // operation-level extension landed on the operation + assertEquals(List.of("@com.example.MyValidation"), operation.vendorExtensions.get("x-request-body-extra-annotation")); + assertNull(operation.vendorExtensions.get("x-foo")); + + // parameter-level extension landed on the matching parameter across collections + CodegenParameter orgId = operation.allParams.stream() + .filter(p -> "orgId".equals(p.baseName)).findFirst().orElseThrow(); + assertEquals(List.of("@com.example.ValidOrgId"), orgId.vendorExtensions.get("x-field-extra-annotation")); + CodegenParameter orgIdPath = operation.pathParams.stream() + .filter(p -> "orgId".equals(p.baseName)).findFirst().orElseThrow(); + assertEquals(List.of("@com.example.ValidOrgId"), orgIdPath.vendorExtensions.get("x-field-extra-annotation")); + } + + @Test + public void testInjectOperationVendorExtensionsAppendsAcrossRepeatedOccurrences() { + final DefaultCodegen codegen = new DefaultCodegen(); + final OpenAPI openApi = TestUtils.parseFlattenSpec("src/test/resources/3_0/inject-operation-vendor-extensions.yaml"); + codegen.setOpenAPI(openApi); + // Simulates two occurrences of --inject-operation-vendor-extensions targeting the same key: + // each occurrence's value becomes a distinct list entry, in call order. + codegen.injectOperationVendorExtensions().put("createEmployee.x-request-body-extra-annotation", + new ArrayList<>(List.of("@Foo"))); + codegen.injectOperationVendorExtensions().get("createEmployee.x-request-body-extra-annotation").add("@Bar"); + + PathItem path = openApi.getPaths().get("/orgs/{orgId}/employees"); + CodegenOperation operation = codegen.fromOperation("/orgs/{orgId}/employees", "post", path.getPost(), path.getServers()); + + assertEquals(List.of("@Foo", "@Bar"), operation.vendorExtensions.get("x-request-body-extra-annotation")); + } + + @Test + public void testInjectOperationVendorExtensionsMatchesSpecOperationId() { + final DefaultCodegen codegen = new DefaultCodegen(); + final OpenAPI openApi = TestUtils.parseFlattenSpec("src/test/resources/3_0/inject-operation-vendor-extensions.yaml"); + codegen.setOpenAPI(openApi); + // key uses the spec-authored (snake_case) operationId, not the generated/camelized one + codegen.injectOperationVendorExtensions().put("create_employee_snake.x-request-body-extra-annotation", List.of("@com.example.MyValidation")); + // the generated/camelized operationId must not match when the spec provides an operationId + codegen.injectOperationVendorExtensions().put("createEmployeeSnake.x-foo", List.of("bar")); + + PathItem path = openApi.getPaths().get("/orgs/{orgId}/employees/snake"); + CodegenOperation operation = codegen.fromOperation("/orgs/{orgId}/employees/snake", "post", path.getPost(), path.getServers()); + + assertEquals("create_employee_snake", operation.operationIdOriginal); + assertEquals(List.of("@com.example.MyValidation"), operation.vendorExtensions.get("x-request-body-extra-annotation")); + assertNull(operation.vendorExtensions.get("x-foo")); + } + + @Test + public void testInjectOperationVendorExtensionsFallsBackToGeneratedIdWhenBlank() { + final DefaultCodegen codegen = new DefaultCodegen(); + final OpenAPI openApi = TestUtils.parseFlattenSpec("src/test/resources/3_0/inject-operation-vendor-extensions.yaml"); + codegen.setOpenAPI(openApi); + + PathItem path = openApi.getPaths().get("/orgs/{orgId}/employees/blank"); + // first pass discovers the generated operationId (the spec leaves operationId blank) + CodegenOperation discovered = codegen.fromOperation("/orgs/{orgId}/employees/blank", "post", path.getPost(), path.getServers()); + assertTrue(discovered.operationIdOriginal == null || discovered.operationIdOriginal.isEmpty()); + assertTrue(StringUtils.isNotBlank(discovered.operationId)); + + // injecting via the generated operationId must apply, since the blank original cannot be matched + codegen.injectOperationVendorExtensions().put(discovered.operationId + ".x-request-body-extra-annotation", List.of("@com.example.MyValidation")); + CodegenOperation operation = codegen.fromOperation("/orgs/{orgId}/employees/blank", "post", path.getPost(), path.getServers()); + assertEquals(List.of("@com.example.MyValidation"), operation.vendorExtensions.get("x-request-body-extra-annotation")); + } + + @Test + public void testInjectOperationVendorExtensionsAvailableDuringFromOperation() { + final DefaultCodegen codegen = new DefaultCodegen(); + final OpenAPI openApi = TestUtils.parseFlattenSpec("src/test/resources/3_0/inject-operation-vendor-extensions.yaml"); + codegen.setOpenAPI(openApi); + codegen.injectOperationVendorExtensions().put("createEmployee.x-codegen-request-body-name", List.of("employeeRequest")); + + PathItem path = openApi.getPaths().get("/orgs/{orgId}/employees"); + CodegenOperation operation = codegen.fromOperation("/orgs/{orgId}/employees", "post", path.getPost(), path.getServers()); + + assertNotNull(operation.bodyParam); + assertEquals("employeeRequest", operation.bodyParam.baseName); + assertEquals("employeeRequest", operation.bodyParam.paramName); + } + + @Test + public void testInjectOperationVendorExtensionsVisibleToPostProcessParameter() { + final OpenAPI openApi = TestUtils.parseFlattenSpec("src/test/resources/3_0/inject-operation-vendor-extensions.yaml"); + final DefaultCodegen discoveryCodegen = new DefaultCodegen(); + discoveryCodegen.setOpenAPI(openApi); + + PathItem path = openApi.getPaths().get("/orgs/{orgId}/employees"); + CodegenOperation discovered = discoveryCodegen.fromOperation("/orgs/{orgId}/employees", "post", path.getPost(), path.getServers()); + assertNotNull(discovered.bodyParam); + + final class RecordingCodegen extends DefaultCodegen { + private final Map> observedAnnotations = new HashMap<>(); + + @Override + public void postProcessParameter(CodegenParameter parameter) { + Object annotation = parameter.vendorExtensions.get("x-field-extra-annotation"); + if (annotation instanceof List) { + @SuppressWarnings("unchecked") + List annotationList = (List) annotation; + observedAnnotations.put(parameter.baseName, annotationList); + } + } + + private Map> getObservedAnnotations() { + return observedAnnotations; + } + } + + final RecordingCodegen codegen = new RecordingCodegen(); + codegen.setOpenAPI(openApi); + codegen.injectOperationVendorExtensions().put("createEmployee.orgId.x-field-extra-annotation", List.of("@com.example.ValidOrgId")); + codegen.injectOperationVendorExtensions().put( + "createEmployee." + discovered.bodyParam.baseName + ".x-field-extra-annotation", + List.of("@com.example.ValidEmployee")); + + CodegenOperation operation = codegen.fromOperation("/orgs/{orgId}/employees", "post", path.getPost(), path.getServers()); + + assertEquals(List.of("@com.example.ValidOrgId"), codegen.getObservedAnnotations().get("orgId")); + assertEquals(List.of("@com.example.ValidEmployee"), codegen.getObservedAnnotations().get(discovered.bodyParam.baseName)); + assertEquals(List.of("@com.example.ValidEmployee"), operation.bodyParam.vendorExtensions.get("x-field-extra-annotation")); + } + + @Test + public void testInjectOperationVendorExtensionsSupportsDottedParameterBaseName() { + // A parameter whose spec-authored name contains a dot must still be matched correctly: + // the key/operationId boundary must be resolved by the last ".x-" occurrence, not the + // first dot after the operationId, otherwise a dotted baseName gets mis-split. + final DefaultCodegen codegen = new DefaultCodegen(); + codegen.setOpenAPI(new OpenAPI().components(new Components())); + codegen.injectOperationVendorExtensions().put("dottedParamOp.org.id.x-field-extra-annotation", List.of("@com.example.ValidOrgId")); + + Operation operation = new Operation() + .operationId("dottedParamOp") + .addParametersItem(new io.swagger.v3.oas.models.parameters.QueryParameter() + .name("org.id") + .required(true) + .schema(new StringSchema())) + .responses(new ApiResponses().addApiResponse("200", new ApiResponse().description("ok"))); + + CodegenOperation co = codegen.fromOperation("/dotted", "get", operation, null); + + assertEquals(1, co.allParams.size()); + assertEquals("org.id", co.allParams.get(0).baseName); + assertEquals(List.of("@com.example.ValidOrgId"), co.allParams.get(0).vendorExtensions.get("x-field-extra-annotation")); + } + + @Test + public void testInjectModelVendorExtensionsSupportsDottedPropertyBaseName() { + // A property whose spec-authored name contains a dot must still be matched correctly: + // the key/model-name boundary must be resolved by the last ".x-" occurrence, not the + // first dot after the model name, otherwise a dotted property baseName gets mis-split. + final DefaultCodegen codegen = new DefaultCodegen(); + codegen.injectModelVendorExtensions().put("Pet.org.id.x-field-extra-annotation", List.of("@com.example.ValidOrgId")); + + CodegenModel model = new CodegenModel(); + model.name = "Pet"; + CodegenProperty property = new CodegenProperty(); + property.baseName = "org.id"; + model.vars = new ArrayList<>(List.of(property)); + model.allVars = model.vars; + + Map objs = Map.of("Pet", TestUtils.createCodegenModelWrapper(model)); + codegen.postProcessAllModels(objs); + + assertEquals(List.of("@com.example.ValidOrgId"), property.vendorExtensions.get("x-field-extra-annotation")); + } + + @Test + public void testFromOperationPreservesFromParameterVirtualDispatch() { + // Simulates generators (e.g. Dart, TypeScript Fetch) that override the public + // fromParameter(Parameter, Set) and call super.fromParameter(...) internally. + // fromOperation's parameter loop must dispatch through this override, not bypass it via + // the private 3-arg overload used for parameter-level vendor extension injection. Also + // registers an injected parameter-level vendor extension for the same parameter, to verify + // both mechanisms (override dispatch and parameter-level injection) still work together + // when a subclass overrides fromParameter and delegates to super. + final class OverridingCodegen extends DefaultCodegen { + @Override + public CodegenParameter fromParameter(Parameter parameter, Set imports) { + CodegenParameter param = super.fromParameter(parameter, imports); + param.vendorExtensions.put("x-marked-by-override", Boolean.TRUE); + return param; + } + } + + final OverridingCodegen codegen = new OverridingCodegen(); + codegen.setOpenAPI(new OpenAPI().components(new Components())); + codegen.injectOperationVendorExtensions().put("markedOperation.orgId.x-foo", List.of("bar")); + + Operation operation = new Operation() + .operationId("markedOperation") + .addParametersItem(new io.swagger.v3.oas.models.parameters.PathParameter() + .name("orgId") + .required(true) + .schema(new StringSchema())) + .responses(new ApiResponses().addApiResponse("200", new ApiResponse().description("ok"))); + + CodegenOperation co = codegen.fromOperation("/orgs/{orgId}", "get", operation, null); + + assertEquals(1, co.allParams.size()); + assertEquals(Boolean.TRUE, co.allParams.get(0).vendorExtensions.get("x-marked-by-override")); + assertEquals(List.of("bar"), co.allParams.get(0).vendorExtensions.get("x-foo")); + } + + @Test + public void testInjectOperationVendorExtensionsMatchesOperationIdContainingDot() { + final DefaultCodegen codegen = new DefaultCodegen(); + codegen.setOpenAPI(new OpenAPI().components(new Components())); + codegen.injectOperationVendorExtensions().put("my.operation.id.x-foo", List.of("bar")); + codegen.injectOperationVendorExtensions().put("my.operation.id.orgId.x-field-extra-annotation", List.of("@com.example.ValidOrgId")); + + Operation operation = new Operation() + .operationId("my.operation.id") + .addParametersItem(new io.swagger.v3.oas.models.parameters.PathParameter() + .name("orgId") + .required(true) + .schema(new StringSchema())) + .responses(new ApiResponses().addApiResponse("200", new ApiResponse().description("ok"))); + + CodegenOperation codegenOperation = codegen.fromOperation("/orgs/{orgId}", "get", operation, null); + + assertEquals(List.of("bar"), codegenOperation.vendorExtensions.get("x-foo")); + CodegenParameter orgId = codegenOperation.pathParams.stream() + .filter(p -> "orgId".equals(p.baseName)).findFirst().orElseThrow(); + assertEquals(List.of("@com.example.ValidOrgId"), orgId.vendorExtensions.get("x-field-extra-annotation")); + } + @Test public void testHasBodyParameter() { final Schema refSchema = new Schema<>().$ref("#/components/schemas/Pet"); diff --git a/modules/openapi-generator/src/test/java/org/openapitools/codegen/config/CodegenConfiguratorTest.java b/modules/openapi-generator/src/test/java/org/openapitools/codegen/config/CodegenConfiguratorTest.java index 551c613b1ca0..83ea58243799 100644 --- a/modules/openapi-generator/src/test/java/org/openapitools/codegen/config/CodegenConfiguratorTest.java +++ b/modules/openapi-generator/src/test/java/org/openapitools/codegen/config/CodegenConfiguratorTest.java @@ -133,4 +133,44 @@ public void resolvesResponses() { Assertions.assertNotNull(context.getSpecDocument().getPaths().get("/hello").getGet().getResponses().get("200").getContent()); } + + @Test + public void setInjectModelVendorExtensionsFollowedByAddDoesNotDoubleAppend() { + Map> initial = new HashMap<>(); + initial.put("Pet.x-class-extra-annotation", new java.util.ArrayList<>(java.util.List.of("@Foo"))); + + CodegenConfigurator configurator = new CodegenConfigurator() + .setGeneratorName("java") + .setInputSpec("src/test/resources/3_0/ping.yaml") + .setInjectModelVendorExtensions(initial) + .addInjectModelVendorExtension("Pet.x-class-extra-annotation", "@Bar"); + + GeneratorSettings generatorSettings = configurator.toContext().getGeneratorSettings(); + Assertions.assertEquals(java.util.List.of("@Foo", "@Bar"), + generatorSettings.getInjectModelVendorExtensions().get("Pet.x-class-extra-annotation")); + } + + @Test + public void fromFileConfigWithScalarInjectedExtensionCanBeAppendedToByCli() throws IOException { + // A config file authored before injected values became lists (scalar form), combined with + // a CLI-style addInjectModelVendorExtension call targeting the same key, must not throw + // UnsupportedOperationException (the parsed GeneratorSettings exposes unmodifiable lists). + File configFile = Files.createTempFile("inject-vendor-extensions", ".yaml").toFile(); + configFile.deleteOnExit(); + Files.writeString(configFile.toPath(), String.join(System.lineSeparator(), + "generatorName: java", + "inputSpec: src/test/resources/3_0/ping.yaml", + "injectModelVendorExtensions:", + " Pet.x-class-extra-annotation: '@Foo'", + "")); + + CodegenConfigurator configurator = CodegenConfigurator.fromFile(configFile.getAbsolutePath()); + Assertions.assertNotNull(configurator); + + configurator.addInjectModelVendorExtension("Pet.x-class-extra-annotation", "@Bar"); + + GeneratorSettings generatorSettings = configurator.toContext().getGeneratorSettings(); + Assertions.assertEquals(java.util.List.of("@Foo", "@Bar"), + generatorSettings.getInjectModelVendorExtensions().get("Pet.x-class-extra-annotation")); + } } diff --git a/modules/openapi-generator/src/test/java/org/openapitools/codegen/config/CodegenConfiguratorUtilsTest.java b/modules/openapi-generator/src/test/java/org/openapitools/codegen/config/CodegenConfiguratorUtilsTest.java new file mode 100644 index 000000000000..06c8dfea81e6 --- /dev/null +++ b/modules/openapi-generator/src/test/java/org/openapitools/codegen/config/CodegenConfiguratorUtilsTest.java @@ -0,0 +1,79 @@ +/* + * Copyright 2026 OpenAPI-Generator Contributors (https://openapi-generator.tech) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.openapitools.codegen.config; + +import org.testng.annotations.Test; + +import java.util.Arrays; + +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoMoreInteractions; + +/** + * Unlike most CLI/Maven key-value-pair options, {@code --inject-model-vendor-extensions} and + * {@code --inject-operation-vendor-extensions} values are commonly annotation literals containing + * their own commas (e.g. {@code @Size(min = 1, max = 100)}). These tests confirm that each option + * occurrence is parsed as exactly one key=value pair (split only on the first '='), so such commas + * are kept literally instead of being mistaken for a separator between injection targets. + */ +public class CodegenConfiguratorUtilsTest { + + @Test + public void injectModelVendorExtensionsKvpKeepsCommasInValueLiteral() { + CodegenConfigurator configurator = mock(CodegenConfigurator.class); + CodegenConfiguratorUtils.applyInjectModelVendorExtensionsKvp( + "Pet.x-class-extra-annotation=@Size(min = 1, max = 100)", configurator); + + verify(configurator).addInjectModelVendorExtension("Pet.x-class-extra-annotation", "@Size(min = 1, max = 100)"); + verifyNoMoreInteractions(configurator); + } + + @Test + public void injectOperationVendorExtensionsKvpKeepsCommasInValueLiteral() { + CodegenConfigurator configurator = mock(CodegenConfigurator.class); + CodegenConfiguratorUtils.applyInjectOperationVendorExtensionsKvp( + "addPet.orgId.x-field-extra-annotation=@Size(min = 1, max = 100) @Deprecated", configurator); + + verify(configurator).addInjectOperationVendorExtension( + "addPet.orgId.x-field-extra-annotation", "@Size(min = 1, max = 100) @Deprecated"); + verifyNoMoreInteractions(configurator); + } + + @Test + public void injectOperationVendorExtensionsKvpListSupportsMultipleOccurrences() { + CodegenConfigurator configurator = mock(CodegenConfigurator.class); + CodegenConfiguratorUtils.applyInjectOperationVendorExtensionsKvpList( + Arrays.asList( + "addPet.x-operation-extra-annotation=@Deprecated", + "addPet.orgId.x-field-extra-annotation=@Size(min = 1, max = 100)"), + configurator); + + verify(configurator).addInjectOperationVendorExtension("addPet.x-operation-extra-annotation", "@Deprecated"); + verify(configurator).addInjectOperationVendorExtension( + "addPet.orgId.x-field-extra-annotation", "@Size(min = 1, max = 100)"); + verifyNoMoreInteractions(configurator); + } + + @Test + public void injectModelVendorExtensionsKvpIgnoresEntryWithoutEquals() { + CodegenConfigurator configurator = mock(CodegenConfigurator.class); + CodegenConfiguratorUtils.applyInjectModelVendorExtensionsKvp("not-a-kvp", configurator); + + verifyNoMoreInteractions(configurator); + } +} diff --git a/modules/openapi-generator/src/test/java/org/openapitools/codegen/java/spring/SpringCodegenTest.java b/modules/openapi-generator/src/test/java/org/openapitools/codegen/java/spring/SpringCodegenTest.java index 7b8b861dda4a..685929fdd4ad 100644 --- a/modules/openapi-generator/src/test/java/org/openapitools/codegen/java/spring/SpringCodegenTest.java +++ b/modules/openapi-generator/src/test/java/org/openapitools/codegen/java/spring/SpringCodegenTest.java @@ -4184,7 +4184,126 @@ public void testHasOperationParameterExtraAnnotation_issue18224() throws IOExcep .toMethod() .assertParameter("clientId") .assertParameterAnnotations() - .containsWithName("com.test.MyAnnotationInHeader"); + .containsWithName("com.test.MyAnnotationInHeader") + .toParameter() + .toMethod() + .assertParameter("sessionId") + .assertParameterAnnotations() + .containsWithName("com.test.MyAnnotationInCookie"); + } + + @Test + public void testRequestBodyExtraAnnotation() throws IOException { + File output = Files.createTempDirectory("test").toFile().getCanonicalFile(); + output.deleteOnExit(); + + final OpenAPI openAPI = TestUtils.parseFlattenSpec("src/test/resources/3_0/request-body-extra-annotation.yaml"); + final SpringCodegen codegen = new SpringCodegen(); + codegen.setOpenAPI(openAPI); + codegen.setOutputDir(output.getAbsolutePath()); + codegen.additionalProperties().put(INTERFACE_ONLY, "true"); + + ClientOptInput input = new ClientOptInput(); + input.openAPI(openAPI); + input.config(codegen); + + DefaultGenerator generator = new DefaultGenerator(); + generator.setGenerateMetadata(false); + generator.setGeneratorPropertyDefault(CodegenConstants.APIS, "true"); + + Map files = generator.opts(input).generate().stream() + .collect(Collectors.toMap(File::getName, Function.identity())); + + JavaFileAssert.assertThat(files.get("EmployeesApi.java")) + // single string value renders before the body param + .assertMethod("createEmployee") + .assertParameter("employee") + .assertParameterAnnotations() + .containsWithName("com.example.MyValidation") + .containsWithName("RequestBody") + .toParameter().toMethod().toFileAssert() + // list value renders all annotations before the body param + .assertMethod("createEmployeeBulk") + .assertParameter("employee") + .assertParameterAnnotations() + .containsWithName("com.example.MyValidation") + .containsWithName("com.example.AuditLogged") + .toParameter().toMethod().toFileAssert() + // selectivity: operation without the extension referencing the same model is unaffected + .assertMethod("createEmployeePlain") + .assertParameter("employee") + .assertParameterAnnotations() + .doesNotContainWithName("com.example.MyValidation") + .doesNotContainWithName("com.example.AuditLogged") + .toParameter().toMethod().toFileAssert() + // annotation placed directly on the inline requestBody object renders before the body param + .assertMethod("createEmployeeInlineAnnotation") + .assertParameter("employee") + .assertParameterAnnotations() + .containsWithName("com.example.InlineBodyValidation") + .containsWithName("RequestBody") + .toParameter().toMethod().toFileAssert() + // reusable components.requestBodies annotation applies to every operation that refs it + .assertMethod("createEmployeeReusableA") + .assertParameter("employee") + .assertParameterAnnotations() + .containsWithName("com.example.ReusableBodyValidation") + .containsWithName("com.example.ReusableAuditLogged") + .toParameter().toMethod().toFileAssert() + .assertMethod("createEmployeeReusableB") + .assertParameter("employee") + .assertParameterAnnotations() + .containsWithName("com.example.ReusableBodyValidation") + .containsWithName("com.example.ReusableAuditLogged"); + } + + @Test + public void testInjectOperationVendorExtensions() throws IOException { + File output = Files.createTempDirectory("test").toFile().getCanonicalFile(); + output.deleteOnExit(); + + final OpenAPI openAPI = TestUtils.parseFlattenSpec("src/test/resources/3_0/inject-operation-vendor-extensions.yaml"); + final SpringCodegen codegen = new SpringCodegen(); + codegen.setOpenAPI(openAPI); + codegen.setOutputDir(output.getAbsolutePath()); + codegen.additionalProperties().put(INTERFACE_ONLY, "true"); + + // operation-level injection drives the request-body annotation; parameter-level injection + // annotates the path param, both without editing the spec + codegen.injectOperationVendorExtensions().put("createEmployee.x-request-body-extra-annotation", List.of("@com.example.MyValidation")); + codegen.injectOperationVendorExtensions().put("createEmployee.orgId.x-field-extra-annotation", List.of("@com.example.ValidOrgId")); + + ClientOptInput input = new ClientOptInput(); + input.openAPI(openAPI); + input.config(codegen); + + DefaultGenerator generator = new DefaultGenerator(); + generator.setGenerateMetadata(false); + generator.setGeneratorPropertyDefault(CodegenConstants.APIS, "true"); + + Map files = generator.opts(input).generate().stream() + .collect(Collectors.toMap(File::getName, Function.identity())); + + JavaFileAssert.assertThat(files.get("OrgsApi.java")) + .assertMethod("createEmployee") + .assertParameter("employee") + .assertParameterAnnotations() + .containsWithName("com.example.MyValidation") + .containsWithName("RequestBody") + .toParameter().toMethod() + .assertParameter("orgId") + .assertParameterAnnotations() + .containsWithName("com.example.ValidOrgId") + .toParameter().toMethod().toFileAssert() + // selectivity: the second operation referencing the same model/param is unaffected + .assertMethod("createEmployeePlain") + .assertParameter("employee") + .assertParameterAnnotations() + .doesNotContainWithName("com.example.MyValidation") + .toParameter().toMethod() + .assertParameter("orgId") + .assertParameterAnnotations() + .doesNotContainWithName("com.example.ValidOrgId"); } @Test diff --git a/modules/openapi-generator/src/test/java/org/openapitools/codegen/kotlin/spring/KotlinSpringServerCodegenTest.java b/modules/openapi-generator/src/test/java/org/openapitools/codegen/kotlin/spring/KotlinSpringServerCodegenTest.java index bdd8a1008307..531e930283a2 100644 --- a/modules/openapi-generator/src/test/java/org/openapitools/codegen/kotlin/spring/KotlinSpringServerCodegenTest.java +++ b/modules/openapi-generator/src/test/java/org/openapitools/codegen/kotlin/spring/KotlinSpringServerCodegenTest.java @@ -4489,6 +4489,81 @@ public void declarativeReactorArrayOfStringReturnsMonoResponseEntity() throws Ex "kotlin.collections.Set<", "Mono files = generateFromContract( + "src/test/resources/3_0/kotlin/kotlin-spring-param-extra-annotation.yaml", + Map.of(INTERFACE_ONLY, true)); + + Path locationApi = files.get("OrgsApi.kt").toPath(); + // path param (single value), path param (list value -> both render), query param, + // header param (single value), cookie param (list value -> both render) + assertFileContains(locationApi, + "@com.example.ValidOrgId ", + "@com.example.ValidLocId @com.example.Trimmed ", + "@com.example.ValidFilter ", + "@com.example.ValidTrace ", + "@com.example.ValidSession @com.example.Trimmed "); + + // form param + assertFileContains(files.get("DevicesApi.kt").toPath(), "@com.example.ValidDeviceId "); + } + + @Test + public void testRequestBodyExtraAnnotation() throws IOException { + Map files = generateFromContract( + "src/test/resources/3_0/request-body-extra-annotation.yaml", + Map.of(INTERFACE_ONLY, true)); + + Path employeeApi = files.get("EmployeesApi.kt").toPath(); + // single string value and list value both render before the body binding + assertFileContains(employeeApi, + "@com.example.MyValidation ", + "@com.example.AuditLogged "); + + // selectivity: the annotation is applied per-operation only, even though all three + // operations reference the same shared Employee model. It must render exactly twice + // (createEmployee + createEmployeeBulk), never for createEmployeePlain. + String content = Files.readString(employeeApi); + int myValidationCount = content.split("@com.example.MyValidation", -1).length - 1; + assertThat(myValidationCount).isEqualTo(2); + + // annotation placed directly on the inline requestBody object renders before the body binding + assertFileContains(employeeApi, "@com.example.InlineBodyValidation "); + // reusable components.requestBodies annotation applies to every operation that refs it (2 ops) + assertFileContains(employeeApi, + "@com.example.ReusableBodyValidation ", + "@com.example.ReusableAuditLogged "); + assertThat(content.split("@com.example.ReusableBodyValidation", -1).length - 1).isEqualTo(2); + } + + @Test + public void testInjectOperationVendorExtensions() throws IOException { + Map files = generateFromContract( + "src/test/resources/3_0/inject-operation-vendor-extensions.yaml", + Map.of(INTERFACE_ONLY, true), + new HashMap<>(), + configurator -> { + // operation-level injection drives the request-body annotation; parameter-level + // injection annotates the path param, both without editing the spec + configurator.addInjectOperationVendorExtension( + "createEmployee.x-request-body-extra-annotation", "@com.example.MyValidation"); + configurator.addInjectOperationVendorExtension( + "createEmployee.orgId.x-field-extra-annotation", "@com.example.ValidOrgId"); + }); + + Path employeesApi = files.get("OrgsApi.kt").toPath(); + assertFileContains(employeesApi, + "@com.example.MyValidation ", + "@com.example.ValidOrgId "); + + // selectivity: only createEmployee is annotated, not createEmployeePlain, even though + // both reference the same shared Employee model and OrgId parameter schema + String content = Files.readString(employeesApi); + assertThat(content.split("@com.example.MyValidation", -1).length - 1).isEqualTo(1); + assertThat(content.split("@com.example.ValidOrgId", -1).length - 1).isEqualTo(1); + } + private Map generateFromContract(String url) throws IOException { return generateFromContract(url, new HashMap<>(), new HashMap<>()); } diff --git a/modules/openapi-generator/src/test/java/org/openapitools/codegen/languages/JavaCamelServerCodegenTest.java b/modules/openapi-generator/src/test/java/org/openapitools/codegen/languages/JavaCamelServerCodegenTest.java new file mode 100644 index 000000000000..6610e1863b7e --- /dev/null +++ b/modules/openapi-generator/src/test/java/org/openapitools/codegen/languages/JavaCamelServerCodegenTest.java @@ -0,0 +1,21 @@ +package org.openapitools.codegen.languages; + +import org.openapitools.codegen.VendorExtension; +import org.testng.annotations.Test; + +import static org.testng.Assert.assertFalse; +import static org.testng.Assert.assertTrue; + +public class JavaCamelServerCodegenTest { + + @Test + public void doesNotAdvertiseRequestBodyExtraAnnotation() { + // The Camel REST DSL templates do not render a request-body parameter annotation, so this + // generator must not advertise the extension it inherits from SpringCodegen. + assertFalse(new JavaCamelServerCodegen().getSupportedVendorExtensions() + .contains(VendorExtension.X_REQUEST_BODY_EXTRA_ANNOTATION)); + // SpringCodegen itself still supports it. + assertTrue(new SpringCodegen().getSupportedVendorExtensions() + .contains(VendorExtension.X_REQUEST_BODY_EXTRA_ANNOTATION)); + } +} diff --git a/modules/openapi-generator/src/test/resources/3_0/inject-operation-vendor-extensions.yaml b/modules/openapi-generator/src/test/resources/3_0/inject-operation-vendor-extensions.yaml new file mode 100644 index 000000000000..b59f3c24ad23 --- /dev/null +++ b/modules/openapi-generator/src/test/resources/3_0/inject-operation-vendor-extensions.yaml @@ -0,0 +1,95 @@ +openapi: 3.0.3 +info: + title: inject operation vendor extensions + version: 1.0.0 +paths: + /orgs/{orgId}/employees: + post: + tags: + - employee + operationId: createEmployee + parameters: + - name: orgId + in: path + required: true + schema: + $ref: '#/components/schemas/OrgId' + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/Employee' + responses: + '201': + description: created + /orgs/{orgId}/employees/plain: + post: + tags: + - employee + operationId: createEmployeePlain + parameters: + - name: orgId + in: path + required: true + schema: + $ref: '#/components/schemas/OrgId' + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/Employee' + responses: + '201': + description: created + /orgs/{orgId}/employees/snake: + post: + tags: + - employee + operationId: create_employee_snake + parameters: + - name: orgId + in: path + required: true + schema: + $ref: '#/components/schemas/OrgId' + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/Employee' + responses: + '201': + description: created + /orgs/{orgId}/employees/blank: + post: + tags: + - employee + operationId: "" + parameters: + - name: orgId + in: path + required: true + schema: + $ref: '#/components/schemas/OrgId' + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/Employee' + responses: + '201': + description: created +components: + schemas: + OrgId: + type: string + format: uuid + Employee: + type: object + properties: + name: + type: string diff --git a/modules/openapi-generator/src/test/resources/3_0/issue_18224.yaml b/modules/openapi-generator/src/test/resources/3_0/issue_18224.yaml index ea8a4db8adfd..8267a2b94a89 100644 --- a/modules/openapi-generator/src/test/resources/3_0/issue_18224.yaml +++ b/modules/openapi-generator/src/test/resources/3_0/issue_18224.yaml @@ -12,6 +12,7 @@ paths: - $ref: '#/components/parameters/groupObj' - $ref: '#/components/parameters/token' - $ref: '#/components/parameters/clientId' + - $ref: '#/components/parameters/sessionId' requestBody: content: application/json: @@ -49,6 +50,13 @@ components: pattern: "\\d" x-pattern-message: "Only numbers" x-field-extra-annotation: '@com.test.MyAnnotationInHeader' + sessionId: + in: cookie + name: sessionId + required: true + schema: + type: string + x-field-extra-annotation: '@com.test.MyAnnotationInCookie' schemas: ObjTest: description: A model to return diff --git a/modules/openapi-generator/src/test/resources/3_0/kotlin/kotlin-spring-param-extra-annotation.yaml b/modules/openapi-generator/src/test/resources/3_0/kotlin/kotlin-spring-param-extra-annotation.yaml new file mode 100644 index 000000000000..b43b3a3f3f33 --- /dev/null +++ b/modules/openapi-generator/src/test/resources/3_0/kotlin/kotlin-spring-param-extra-annotation.yaml @@ -0,0 +1,83 @@ +openapi: 3.0.3 +info: + title: kotlin-spring param extra annotation + version: 1.0.0 +paths: + /orgs/{orgId}/locations/{locId}: + get: + tags: + - location + operationId: getLocation + parameters: + - name: orgId + in: path + required: true + x-field-extra-annotation: "@com.example.ValidOrgId" + schema: + $ref: '#/components/schemas/OrgId' + - name: locId + in: path + required: true + x-field-extra-annotation: + - "@com.example.ValidLocId" + - "@com.example.Trimmed" + schema: + $ref: '#/components/schemas/LocId' + - name: filter + in: query + required: false + x-field-extra-annotation: "@com.example.ValidFilter" + schema: + $ref: '#/components/schemas/Filter' + - name: X-Trace-Id + in: header + required: true + x-field-extra-annotation: "@com.example.ValidTrace" + schema: + $ref: '#/components/schemas/TraceId' + - name: session + in: cookie + required: true + x-field-extra-annotation: + - "@com.example.ValidSession" + - "@com.example.Trimmed" + schema: + $ref: '#/components/schemas/SessionId' + responses: + '200': + description: ok + /devices: + post: + tags: + - device + operationId: registerDevice + requestBody: + content: + application/x-www-form-urlencoded: + schema: + type: object + required: + - deviceId + properties: + deviceId: + x-field-extra-annotation: "@com.example.ValidDeviceId" + allOf: + - $ref: '#/components/schemas/DeviceId' + responses: + '200': + description: ok +components: + schemas: + OrgId: + type: string + format: uuid + LocId: + type: string + DeviceId: + type: string + Filter: + type: string + TraceId: + type: string + SessionId: + type: string diff --git a/modules/openapi-generator/src/test/resources/3_0/kotlin/petstore-with-extra-annotation.yaml b/modules/openapi-generator/src/test/resources/3_0/kotlin/petstore-with-extra-annotation.yaml new file mode 100644 index 000000000000..2827ae31565b --- /dev/null +++ b/modules/openapi-generator/src/test/resources/3_0/kotlin/petstore-with-extra-annotation.yaml @@ -0,0 +1,752 @@ +openapi: 3.0.0 +servers: + - url: 'http://petstore.swagger.io/v2' +info: + description: >- + This is a sample server Petstore server. For this sample, you can use the api key + `special-key` to test the authorization filters. + version: 1.0.0 + title: OpenAPI Petstore + license: + name: Apache-2.0 + url: 'https://www.apache.org/licenses/LICENSE-2.0.html' +tags: + - name: pet + description: Everything about your Pets + - name: store + description: Access to Petstore orders + - name: user + description: Operations about user +paths: + /pet: + post: + tags: + - pet + summary: Add a new pet to the store + description: '' + operationId: addPet + x-extra-imports: org.springframework.lang.NonNull + x-request-body-extra-annotation: "@NonNull" + responses: + '200': + description: successful operation + content: + application/xml: + schema: + $ref: '#/components/schemas/Pet' + application/json: + schema: + $ref: '#/components/schemas/Pet' + '405': + description: Invalid input + security: + - petstore_auth: + - 'write:pets' + - 'read:pets' + requestBody: + $ref: '#/components/requestBodies/Pet' + put: + tags: + - pet + summary: Update an existing pet + description: '' + operationId: updatePet + externalDocs: + url: "http://petstore.swagger.io/v2/doc/updatePet" + description: "API documentation for the updatePet operation" + responses: + '200': + description: successful operation + content: + application/xml: + schema: + $ref: '#/components/schemas/Pet' + application/json: + schema: + $ref: '#/components/schemas/Pet' + '400': + description: Invalid ID supplied + '404': + description: Pet not found + '405': + description: Validation exception + security: + - petstore_auth: + - 'write:pets' + - 'read:pets' + requestBody: + $ref: '#/components/requestBodies/Pet' + /pet/findByStatus: + get: + tags: + - pet + summary: Finds Pets by status + description: Multiple status values can be provided with comma separated strings + operationId: findPetsByStatus + parameters: + - name: status + in: query + description: Status values that need to be considered for filter + required: true + style: form + explode: false + deprecated: true + x-extra-imports: + - org.springframework.lang.NonNull + - javax.validation.constraints.NotEmpty + x-field-extra-annotation: + - "@NonNull" + - "@Size(min = 1)" + - "@NotEmpty" + schema: + type: array + items: + type: string + enum: + - available + - pending + - sold + default: available + responses: + '200': + description: successful operation + content: + application/xml: + schema: + type: array + items: + $ref: '#/components/schemas/Pet' + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/Pet' + '400': + description: Invalid status value + security: + - petstore_auth: + - 'read:pets' + /pet/findByTags: + get: + tags: + - pet + summary: Finds Pets by tags + description: >- + Multiple tags can be provided with comma separated strings. Use tag1, + tag2, tag3 for testing. + operationId: findPetsByTags + parameters: + - name: tags + in: query + description: Tags to filter by + required: true + style: form + explode: false + schema: + type: array + items: + type: string + responses: + '200': + description: successful operation + content: + application/xml: + schema: + type: array + items: + $ref: '#/components/schemas/Pet' + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/Pet' + '400': + description: Invalid tag value + security: + - petstore_auth: + - 'read:pets' + deprecated: true + '/pet/{petId}': + get: + tags: + - pet + summary: Find pet by ID + description: Returns a single pet + operationId: getPetById + parameters: + - name: petId + in: path + description: ID of pet to return + required: true + x-extra-imports: org.springframework.lang.NonNull + x-field-extra-annotation: "@NonNull" + schema: + type: integer + format: int64 + responses: + '200': + description: successful operation + content: + application/xml: + schema: + $ref: '#/components/schemas/Pet' + application/json: + schema: + $ref: '#/components/schemas/Pet' + '400': + description: Invalid ID supplied + '404': + description: Pet not found + security: + - api_key: [] + post: + tags: + - pet + summary: Updates a pet in the store with form data + description: '' + operationId: updatePetWithForm + parameters: + - name: petId + in: path + description: ID of pet that needs to be updated + required: true + schema: + type: integer + format: int64 + responses: + '405': + description: Invalid input + security: + - petstore_auth: + - 'write:pets' + - 'read:pets' + requestBody: + content: + application/x-www-form-urlencoded: + schema: + type: object + properties: + name: + description: Updated name of the pet + type: string + status: + description: Updated status of the pet + type: string + delete: + tags: + - pet + summary: Deletes a pet + description: '' + operationId: deletePet + parameters: + - name: api_key + in: header + required: false + schema: + type: string + - name: petId + in: path + description: Pet id to delete + required: true + schema: + type: integer + format: int64 + responses: + '400': + description: Invalid pet value + security: + - petstore_auth: + - 'write:pets' + - 'read:pets' + '/pet/{petId}/uploadImage': + post: + tags: + - pet + summary: uploads an image + description: '' + operationId: uploadFile + parameters: + - name: petId + in: path + description: ID of pet to update + required: true + schema: + type: integer + format: int64 + responses: + '200': + description: successful operation + content: + application/json: + schema: + $ref: '#/components/schemas/ApiResponse' + security: + - petstore_auth: + - 'write:pets' + - 'read:pets' + requestBody: + content: + multipart/form-data: + schema: + type: object + properties: + additionalMetadata: + description: Additional data to pass to server + type: string + file: + description: file to upload + type: string + format: binary + /store/inventory: + get: + tags: + - store + summary: Returns pet inventories by status + description: Returns a map of status codes to quantities + operationId: getInventory + responses: + '200': + description: successful operation + content: + application/json: + schema: + type: object + additionalProperties: + type: integer + format: int32 + security: + - api_key: [] + /store/order: + post: + tags: + - store + summary: Place an order for a pet + description: '' + operationId: placeOrder + responses: + '200': + description: successful operation + content: + application/xml: + schema: + $ref: '#/components/schemas/Order' + application/json: + schema: + $ref: '#/components/schemas/Order' + '400': + description: Invalid Order + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/Order' + description: order placed for purchasing the pet + required: true + '/store/order/{orderId}': + get: + tags: + - store + summary: Find purchase order by ID + description: >- + For valid response try integer IDs with value <= 5 or > 10. Other values + will generate exceptions + operationId: getOrderById + parameters: + - name: orderId + in: path + description: ID of pet that needs to be fetched + required: true + schema: + type: integer + format: int64 + minimum: 1 + maximum: 5 + responses: + '200': + description: successful operation + content: + application/xml: + schema: + $ref: '#/components/schemas/Order' + application/json: + schema: + $ref: '#/components/schemas/Order' + '400': + description: Invalid ID supplied + '404': + description: Order not found + delete: + tags: + - store + summary: Delete purchase order by ID + description: >- + For valid response try integer IDs with value < 1000. Anything above + 1000 or nonintegers will generate API errors + operationId: deleteOrder + parameters: + - name: orderId + in: path + description: ID of the order that needs to be deleted + required: true + schema: + type: string + responses: + '400': + description: Invalid ID supplied + '404': + description: Order not found + /user: + post: + tags: + - user + summary: Create user + description: This can only be done by the logged in user. + operationId: createUser + responses: + default: + description: successful operation + security: + - api_key: [] + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/User' + description: Created user object + required: true + /user/createWithArray: + post: + tags: + - user + summary: Creates list of users with given input array + description: '' + operationId: createUsersWithArrayInput + responses: + default: + description: successful operation + security: + - api_key: [] + requestBody: + $ref: '#/components/requestBodies/UserArray' + /user/createWithList: + post: + tags: + - user + summary: Creates list of users with given input array + description: '' + operationId: createUsersWithListInput + responses: + default: + description: successful operation + security: + - api_key: [] + requestBody: + $ref: '#/components/requestBodies/UserArray' + /user/login: + get: + tags: + - user + summary: Logs user into the system + description: '' + operationId: loginUser + parameters: + - name: username + in: query + description: The user name for login + required: true + schema: + type: string + pattern: '^[a-zA-Z0-9]+[a-zA-Z0-9\.\-_]*[a-zA-Z0-9]+$' + - name: password + in: query + description: The password for login in clear text + required: true + schema: + type: string + responses: + '200': + description: successful operation + headers: + Set-Cookie: + description: >- + Cookie authentication key for use with the `api_key` + apiKey authentication. + schema: + type: string + example: AUTH_KEY=abcde12345; Path=/; HttpOnly + X-Rate-Limit: + description: calls per hour allowed by the user + schema: + type: integer + format: int32 + X-Expires-After: + description: date in UTC when token expires + schema: + type: string + format: date-time + content: + application/xml: + schema: + type: string + application/json: + schema: + type: string + '400': + description: Invalid username/password supplied + /user/logout: + get: + tags: + - user + summary: Logs out current logged in user session + description: '' + operationId: logoutUser + responses: + default: + description: successful operation + security: + - api_key: [] + '/user/{username}': + get: + tags: + - user + summary: Get user by user name + description: '' + operationId: getUserByName + parameters: + - name: username + in: path + description: The name that needs to be fetched. Use user1 for testing. + required: true + schema: + type: string + responses: + '200': + description: successful operation + content: + application/xml: + schema: + $ref: '#/components/schemas/User' + application/json: + schema: + $ref: '#/components/schemas/User' + '400': + description: Invalid username supplied + '404': + description: User not found + put: + tags: + - user + summary: Updated user + description: This can only be done by the logged in user. + operationId: updateUser + parameters: + - name: username + in: path + description: name that need to be deleted + required: true + schema: + type: string + responses: + '400': + description: Invalid user supplied + '404': + description: User not found + security: + - api_key: [] + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/User' + description: Updated user object + required: true + delete: + tags: + - user + summary: Delete user + description: This can only be done by the logged in user. + operationId: deleteUser + parameters: + - name: username + in: path + description: The name that needs to be deleted + required: true + schema: + type: string + responses: + '400': + description: Invalid username supplied + '404': + description: User not found + security: + - api_key: [] +externalDocs: + description: Find out more about Swagger + url: 'http://swagger.io' +components: + requestBodies: + UserArray: + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/User' + description: List of user object + required: true + Pet: + content: + application/json: + schema: + $ref: '#/components/schemas/Pet' + application/xml: + schema: + $ref: '#/components/schemas/Pet' + description: Pet object that needs to be added to the store + required: true + securitySchemes: + petstore_auth: + type: oauth2 + flows: + implicit: + authorizationUrl: 'http://petstore.swagger.io/api/oauth/dialog' + scopes: + 'write:pets': modify pets in your account + 'read:pets': read your pets + api_key: + type: apiKey + name: api_key + in: header + schemas: + Order: + title: Pet Order + description: An order for a pets from the pet store + type: object + properties: + id: + type: integer + format: int64 + petId: + type: integer + format: int64 + quantity: + type: integer + format: int32 + shipDate: + type: string + format: date-time + status: + type: string + description: Order Status + enum: + - placed + - approved + - delivered + complete: + type: boolean + default: false + xml: + name: Order + Category: + title: Pet category + description: A category for a pet + type: object + properties: + id: + type: integer + format: int64 + name: + type: string + pattern: '^[a-zA-Z0-9]+[a-zA-Z0-9\.\-_]*[a-zA-Z0-9]+$' + xml: + name: Category + User: + title: a User + description: A User who is purchasing from the pet store + type: object + properties: + id: + type: integer + format: int64 + username: + type: string + firstName: + type: string + lastName: + type: string + email: + type: string + password: + type: string + phone: + type: string + userStatus: + type: integer + format: int32 + description: User Status + xml: + name: User + Tag: + title: Pet Tag + description: A tag for a pet + type: object + properties: + id: + type: integer + format: int64 + name: + type: string + xml: + name: Tag + Pet: + title: a Pet + description: A pet for sale in the pet store + type: object + required: + - name + - photoUrls + properties: + id: + type: integer + format: int64 + category: + $ref: '#/components/schemas/Category' + name: + type: string + example: doggie + photoUrls: + type: array + xml: + name: photoUrl + wrapped: true + items: + type: string + tags: + type: array + xml: + name: tag + wrapped: true + items: + $ref: '#/components/schemas/Tag' + status: + type: string + description: pet status in the store + deprecated: true + enum: + - available + - pending + - sold + xml: + name: Pet + ApiResponse: + title: An uploaded response + description: Describes the result of uploading an image resource + type: object + properties: + code: + type: integer + format: int32 + type: + type: string + message: + type: string \ No newline at end of file diff --git a/modules/openapi-generator/src/test/resources/3_0/request-body-extra-annotation.yaml b/modules/openapi-generator/src/test/resources/3_0/request-body-extra-annotation.yaml new file mode 100644 index 000000000000..f42f48bddccf --- /dev/null +++ b/modules/openapi-generator/src/test/resources/3_0/request-body-extra-annotation.yaml @@ -0,0 +1,105 @@ +openapi: 3.0.3 +info: + title: request body extra annotation + version: 1.0.0 +paths: + /employees: + post: + tags: + - employee + operationId: createEmployee + x-request-body-extra-annotation: "@com.example.MyValidation" + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/Employee' + responses: + '201': + description: created + /employees/bulk: + post: + tags: + - employee + operationId: createEmployeeBulk + x-request-body-extra-annotation: + - "@com.example.MyValidation" + - "@com.example.AuditLogged" + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/Employee' + responses: + '201': + description: created + /employees/plain: + post: + tags: + - employee + operationId: createEmployeePlain + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/Employee' + responses: + '201': + description: created + /employees/inline-annotation: + post: + tags: + - employee + operationId: createEmployeeInlineAnnotation + # annotation placed directly on the requestBody object (per-operation, selective) + requestBody: + x-field-extra-annotation: "@com.example.InlineBodyValidation" + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/Employee' + responses: + '201': + description: created + /employees/reusable-a: + post: + tags: + - employee + operationId: createEmployeeReusableA + requestBody: + $ref: '#/components/requestBodies/AnnotatedEmployeeBody' + responses: + '201': + description: created + /employees/reusable-b: + post: + tags: + - employee + operationId: createEmployeeReusableB + requestBody: + $ref: '#/components/requestBodies/AnnotatedEmployeeBody' + responses: + '201': + description: created +components: + requestBodies: + # reusable request body: the annotation applies to every operation that $refs it + AnnotatedEmployeeBody: + x-field-extra-annotation: + - "@com.example.ReusableBodyValidation" + - "@com.example.ReusableAuditLogged" + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/Employee' + schemas: + Employee: + type: object + properties: + name: + type: string diff --git a/modules/openapi-generator/src/test/resources/3_0/spring/petstore-with-fake-endpoints-models-for-testing-extra-annotation.yaml b/modules/openapi-generator/src/test/resources/3_0/spring/petstore-with-fake-endpoints-models-for-testing-extra-annotation.yaml new file mode 100644 index 000000000000..82f149cd9c4e --- /dev/null +++ b/modules/openapi-generator/src/test/resources/3_0/spring/petstore-with-fake-endpoints-models-for-testing-extra-annotation.yaml @@ -0,0 +1,2101 @@ +openapi: 3.0.0 +info: + description: 'This spec is mainly for testing Petstore server and contains fake + endpoints, models. Please do not use this for any other purpose. Special + characters: " \' + version: 1.0.0 + title: OpenAPI Petstore + license: + name: Apache-2.0 + url: https://www.apache.org/licenses/LICENSE-2.0.html +tags: + - name: pet + description: Everything about your Pets + - name: store + description: Access to Petstore orders + - name: user + description: Operations about user +paths: + /pet: + post: + tags: + - pet + summary: Add a new pet to the store + description: "" + operationId: addPet + x-request-body-extra-annotation: "@org.springframework.lang.NonNull" + requestBody: + $ref: "#/components/requestBodies/Pet" + responses: + "200": + description: successful operation + "405": + description: Invalid input + security: + - petstore_auth: + - write:pets + - read:pets + put: + tags: + - pet + summary: Update an existing pet + description: "" + operationId: updatePet + requestBody: + $ref: "#/components/requestBodies/Pet" + responses: + "200": + description: successful operation + "400": + description: Invalid ID supplied + "404": + description: Pet not found + "405": + description: Validation exception + security: + - petstore_auth: + - write:pets + - read:pets + /pet/findByStatus: + get: + tags: + - pet + summary: Finds Pets by status + description: Multiple status values can be provided with comma separated strings + operationId: findPetsByStatus + parameters: + - name: status + in: query + description: Status values that need to be considered for filter + required: true + style: form + explode: false + x-field-extra-annotation: "@org.springframework.lang.NonNull" + schema: + type: array + items: + type: string + enum: + - available + - pending + - sold + default: available + responses: + "200": + description: successful operation + content: + application/xml: + schema: + type: array + items: + $ref: "#/components/schemas/Pet" + application/json: + schema: + type: array + items: + $ref: "#/components/schemas/Pet" + "400": + description: Invalid status value + security: + - petstore_auth: + - write:pets + - read:pets + /pet/findByTags: + get: + tags: + - pet + summary: Finds Pets by tags + description: Multiple tags can be provided with comma separated strings. Use tag1, + tag2, tag3 for testing. + operationId: findPetsByTags + parameters: + - name: tags + in: query + description: Tags to filter by + required: true + style: form + explode: false + schema: + type: array + items: + type: string + uniqueItems: true + responses: + "200": + description: successful operation + content: + application/xml: + schema: + type: array + uniqueItems: true + items: + $ref: "#/components/schemas/Pet" + application/json: + schema: + type: array + uniqueItems: true + items: + $ref: "#/components/schemas/Pet" + "400": + description: Invalid tag value + security: + - petstore_auth: + - write:pets + - read:pets + deprecated: true + "/pet/{petId}": + get: + tags: + - pet + summary: Find pet by ID + description: Returns a single pet + operationId: getPetById + parameters: + - name: petId + in: path + description: ID of pet to return + required: true + x-field-extra-annotation: "@org.springframework.lang.NonNull" + schema: + type: integer + format: int64 + responses: + "200": + description: successful operation + content: + application/xml: + schema: + $ref: "#/components/schemas/Pet" + application/json: + schema: + $ref: "#/components/schemas/Pet" + "400": + description: Invalid ID supplied + "404": + description: Pet not found + security: + - api_key: [] + post: + tags: + - pet + summary: Updates a pet in the store with form data + description: "" + operationId: updatePetWithForm + parameters: + - name: petId + in: path + description: ID of pet that needs to be updated + required: true + schema: + type: integer + format: int64 + requestBody: + content: + application/x-www-form-urlencoded: + schema: + type: object + properties: + name: + description: Updated name of the pet + type: string + status: + description: Updated status of the pet + type: string + responses: + "405": + description: Invalid input + security: + - petstore_auth: + - write:pets + - read:pets + delete: + tags: + - pet + summary: Deletes a pet + description: "" + operationId: deletePet + parameters: + - name: api_key + in: header + required: false + schema: + type: string + - name: petId + in: path + description: Pet id to delete + required: true + schema: + type: integer + format: int64 + responses: + "200": + description: successful operation + "400": + description: Invalid pet value + security: + - petstore_auth: + - write:pets + - read:pets + "/pet/{petId}/uploadImage": + post: + tags: + - pet + summary: uploads an image + description: "" + operationId: uploadFile + parameters: + - name: petId + in: path + description: ID of pet to update + required: true + schema: + type: integer + format: int64 + requestBody: + content: + multipart/form-data: + schema: + type: object + properties: + additionalMetadata: + description: Additional data to pass to server + type: string + file: + description: file to upload + type: string + format: binary + responses: + "200": + description: successful operation + content: + application/json: + schema: + $ref: "#/components/schemas/ApiResponse" + security: + - petstore_auth: + - write:pets + - read:pets + /store/inventory: + get: + tags: + - store + summary: Returns pet inventories by status + description: Returns a map of status codes to quantities + operationId: getInventory + responses: + "200": + description: successful operation + content: + application/json: + schema: + type: object + additionalProperties: + type: integer + format: int32 + security: + - api_key: [] + /store/order: + post: + tags: + - store + summary: Place an order for a pet + description: "" + operationId: placeOrder + requestBody: + content: + application/json: + schema: + $ref: "#/components/schemas/Order" + description: order placed for purchasing the pet + required: true + responses: + "200": + description: successful operation + content: + application/xml: + schema: + $ref: "#/components/schemas/Order" + application/json: + schema: + $ref: "#/components/schemas/Order" + "400": + description: Invalid Order + "/store/order/{order_id}": + get: + tags: + - store + summary: Find purchase order by ID + description: For valid response try integer IDs with value <= 5 or > 10. Other values + will generate exceptions + operationId: getOrderById + parameters: + - name: order_id + in: path + description: ID of pet that needs to be fetched + required: true + schema: + type: integer + format: int64 + minimum: 1 + maximum: 5 + responses: + "200": + description: successful operation + content: + application/xml: + schema: + $ref: "#/components/schemas/Order" + application/json: + schema: + $ref: "#/components/schemas/Order" + "400": + description: Invalid ID supplied + "404": + description: Order not found + delete: + tags: + - store + summary: Delete purchase order by ID + description: For valid response try integer IDs with value < 1000. Anything above + 1000 or nonintegers will generate API errors + operationId: deleteOrder + parameters: + - name: order_id + in: path + description: ID of the order that needs to be deleted + required: true + schema: + type: string + responses: + "400": + description: Invalid ID supplied + "404": + description: Order not found + /user: + post: + tags: + - user + summary: Create user + description: This can only be done by the logged in user. + operationId: createUser + requestBody: + content: + application/json: + schema: + $ref: "#/components/schemas/User" + description: Created user object + required: true + responses: + default: + description: successful operation + /user/createWithArray: + post: + tags: + - user + summary: Creates list of users with given input array + description: "" + operationId: createUsersWithArrayInput + requestBody: + $ref: "#/components/requestBodies/UserArray" + responses: + default: + description: successful operation + /user/createWithList: + post: + tags: + - user + summary: Creates list of users with given input array + description: "" + operationId: createUsersWithListInput + requestBody: + $ref: "#/components/requestBodies/UserArray" + responses: + default: + description: successful operation + /user/login: + get: + tags: + - user + summary: Logs user into the system + description: "" + operationId: loginUser + parameters: + - name: username + in: query + description: The user name for login + required: true + schema: + type: string + - name: password + in: query + description: The password for login in clear text + required: true + schema: + type: string + responses: + "200": + description: successful operation + headers: + X-Rate-Limit: + description: calls per hour allowed by the user + schema: + type: integer + format: int32 + X-Expires-After: + description: date in UTC when token expires + schema: + type: string + format: date-time + content: + application/xml: + schema: + type: string + application/json: + schema: + type: string + "400": + description: Invalid username/password supplied + /user/logout: + get: + tags: + - user + summary: Logs out current logged in user session + description: "" + operationId: logoutUser + responses: + default: + description: successful operation + "/user/{username}": + get: + tags: + - user + summary: Get user by user name + description: "" + operationId: getUserByName + parameters: + - name: username + in: path + description: The name that needs to be fetched. Use user1 for testing. + required: true + schema: + type: string + responses: + "200": + description: successful operation + content: + application/xml: + schema: + $ref: "#/components/schemas/User" + application/json: + schema: + $ref: "#/components/schemas/User" + "400": + description: Invalid username supplied + "404": + description: User not found + put: + tags: + - user + summary: Updated user + description: This can only be done by the logged in user. + operationId: updateUser + parameters: + - name: username + in: path + description: name that need to be deleted + required: true + schema: + type: string + requestBody: + content: + application/json: + schema: + $ref: "#/components/schemas/User" + description: Updated user object + required: true + responses: + "400": + description: Invalid user supplied + "404": + description: User not found + delete: + tags: + - user + summary: Delete user + description: This can only be done by the logged in user. + operationId: deleteUser + parameters: + - name: username + in: path + description: The name that needs to be deleted + required: true + schema: + type: string + responses: + "400": + description: Invalid username supplied + "404": + description: User not found + /fake_classname_test: + patch: + tags: + - fake_classname_tags 123#$%^ + summary: To test class name in snake case + description: To test class name in snake case + operationId: testClassname + requestBody: + $ref: "#/components/requestBodies/Client" + responses: + "200": + description: successful operation + content: + application/json: + schema: + $ref: "#/components/schemas/Client" + security: + - api_key_query: [] + /fake: + patch: + tags: + - fake + summary: To test "client" model + description: To test "client" model + operationId: testClientModel + requestBody: + $ref: "#/components/requestBodies/Client" + responses: + "200": + description: successful operation + content: + application/json: + schema: + $ref: "#/components/schemas/Client" + get: + tags: + - fake + summary: To test enum parameters + description: To test enum parameters + operationId: testEnumParameters + parameters: + - name: enum_header_string_array + in: header + description: Header parameter enum test (string array) + schema: + type: array + items: + type: string + default: $ + enum: + - ">" + - $ + - name: enum_header_string + in: header + description: Header parameter enum test (string) + schema: + type: string + enum: + - _abc + - -efg + - (xyz) + default: -efg + - name: enum_query_string_array + in: query + description: Query parameter enum test (string array) + schema: + type: array + items: + type: string + default: $ + enum: + - ">" + - $ + - name: enum_query_string + in: query + description: Query parameter enum test (string) + schema: + type: string + enum: + - _abc + - -efg + - (xyz) + default: -efg + - name: enum_query_integer + in: query + description: Query parameter enum test (double) + schema: + type: integer + format: int32 + enum: + - 1 + - -2 + - name: enum_query_double + in: query + description: Query parameter enum test (double) + schema: + type: number + format: double + enum: + - 1.1 + - -1.2 + requestBody: + content: + application/x-www-form-urlencoded: + schema: + type: object + properties: + enum_form_string_array: + description: Form parameter enum test (string array) + type: array + items: + type: string + default: $ + enum: + - ">" + - $ + enum_form_string: + description: Form parameter enum test (string) + type: string + enum: + - _abc + - -efg + - (xyz) + default: -efg + responses: + "400": + description: Invalid request + "404": + description: Not found + post: + tags: + - fake + summary: |- + Fake endpoint for testing various parameters + 假端點 + 偽のエンドポイント + 가짜 엔드 포인트 + description: |- + Fake endpoint for testing various parameters + 假端點 + 偽のエンドポイント + 가짜 엔드 포인트 + operationId: testEndpointParameters + requestBody: + content: + application/x-www-form-urlencoded: + schema: + type: object + properties: + integer: + description: None + type: integer + minimum: 10 + maximum: 100 + int32: + description: None + type: integer + format: int32 + minimum: 20 + maximum: 200 + int64: + description: None + type: integer + format: int64 + number: + description: None + type: number + minimum: 32.1 + maximum: 543.2 + float: + description: None + type: number + format: float + maximum: 987.6 + double: + description: None + type: number + format: double + minimum: 67.8 + maximum: 123.4 + string: + description: None + type: string + pattern: "[a-zA-Z]" + pattern_without_delimiter: + description: None + type: string + pattern: ^[A-Z].* + byte: + description: None + type: string + format: byte + binary: + description: None + type: string + format: binary + date: + description: None + type: string + format: date + dateTime: + description: None + type: string + format: date-time + password: + description: None + type: string + format: password + minLength: 10 + maxLength: 64 + callback: + description: None + type: string + required: + - number + - double + - pattern_without_delimiter + - byte + responses: + "400": + description: Invalid username supplied + "404": + description: User not found + security: + - http_basic_test: [] + delete: + tags: + - fake + summary: Fake endpoint to test group parameters (optional) + description: Fake endpoint to test group parameters (optional) + operationId: testGroupParameters + x-group-parameters: true + parameters: + - name: required_string_group + in: query + description: Required String in group parameters + required: true + schema: + type: integer + - name: required_boolean_group + in: header + description: Required Boolean in group parameters + required: true + schema: + type: boolean + - name: required_int64_group + in: query + description: Required Integer in group parameters + required: true + schema: + type: integer + format: int64 + - name: string_group + in: query + description: String in group parameters + schema: + type: integer + - name: boolean_group + in: header + description: Boolean in group parameters + schema: + type: boolean + - name: int64_group + in: query + description: Integer in group parameters + schema: + type: integer + format: int64 + responses: + "400": + description: Something wrong + /fake/outer/number: + post: + tags: + - fake + description: Test serialization of outer number types + operationId: fakeOuterNumberSerialize + requestBody: + content: + application/json: + schema: + $ref: "#/components/schemas/OuterNumber" + description: Input number as post body + responses: + "200": + description: Output number + content: + "*/*": + schema: + $ref: "#/components/schemas/OuterNumber" + /fake/outer/string: + post: + tags: + - fake + description: Test serialization of outer string types + operationId: fakeOuterStringSerialize + requestBody: + content: + application/json: + schema: + $ref: "#/components/schemas/OuterString" + description: Input string as post body + responses: + "200": + description: Output string + content: + "*/*": + schema: + $ref: "#/components/schemas/OuterString" + /fake/outer/boolean: + post: + tags: + - fake + description: Test serialization of outer boolean types + operationId: fakeOuterBooleanSerialize + requestBody: + content: + application/json: + schema: + $ref: "#/components/schemas/OuterBoolean" + description: Input boolean as post body + responses: + "200": + description: Output boolean + content: + "*/*": + schema: + $ref: "#/components/schemas/OuterBoolean" + /fake/outer/composite: + post: + tags: + - fake + description: Test serialization of object with outer number type + operationId: fakeOuterCompositeSerialize + requestBody: + content: + application/json: + schema: + $ref: "#/components/schemas/OuterComposite" + description: Input composite as post body + responses: + "200": + description: Output composite + content: + "*/*": + schema: + $ref: "#/components/schemas/OuterComposite" + /fake/jsonFormData: + get: + tags: + - fake + summary: test json serialization of form data + description: "" + operationId: testJsonFormData + requestBody: + content: + application/x-www-form-urlencoded: + schema: + type: object + properties: + param: + description: field1 + type: string + param2: + description: field2 + type: string + required: + - param + - param2 + responses: + "200": + description: successful operation + /fake/inline-additionalProperties: + post: + tags: + - fake + summary: test inline additionalProperties + description: "" + operationId: testInlineAdditionalProperties + requestBody: + content: + application/json: + schema: + type: object + additionalProperties: + type: string + description: request body + required: true + responses: + "200": + description: successful operation + /fake/nullable: + post: + tags: + - fake + summary: test nullable parent property + description: "" + operationId: testNullable + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/ChildWithNullable' + description: request body + required: true + responses: + "200": + description: successful operation + /fake/body-with-query-params: + put: + tags: + - fake + operationId: testBodyWithQueryParams + parameters: + - name: query + in: query + required: true + schema: + type: string + requestBody: + content: + application/json: + schema: + $ref: "#/components/schemas/User" + required: true + responses: + "200": + description: Success + /fake/create_xml_item: + post: + tags: + - fake + operationId: createXmlItem + summary: creates an XmlItem + description: this route creates an XmlItem + requestBody: + content: + application/xml: + schema: + $ref: "#/components/schemas/XmlItem" + application/xml; charset=utf-8: + schema: + $ref: "#/components/schemas/XmlItem" + application/xml; charset=utf-16: + schema: + $ref: "#/components/schemas/XmlItem" + text/xml: + schema: + $ref: "#/components/schemas/XmlItem" + text/xml; charset=utf-8: + schema: + $ref: "#/components/schemas/XmlItem" + text/xml; charset=utf-16: + schema: + $ref: "#/components/schemas/XmlItem" + description: XmlItem Body + required: true + responses: + "200": + description: successful operation + /another-fake/dummy: + patch: + tags: + - $another-fake? + summary: To test special tags + description: To test special tags and operation ID starting with number + operationId: 123_test_@#$%_special_tags + requestBody: + $ref: "#/components/requestBodies/Client" + responses: + "200": + description: successful operation + content: + application/json: + schema: + $ref: "#/components/schemas/Client" + /fake/body-with-file-schema: + put: + tags: + - fake + description: For this test, the body for this request much reference a schema named + `File`. + operationId: testBodyWithFileSchema + requestBody: + content: + application/json: + schema: + $ref: "#/components/schemas/FileSchemaTestClass" + required: true + responses: + "200": + description: Success + /fake/response-with-example: + get: + tags: + - fake + description: This endpoint defines an example value for its response schema. + operationId: testWithResultExample + responses: + "200": + content: + application/json: + schema: + example: 42 + type: integer + description: Success + /fake/test-query-parameters: + put: + tags: + - fake + description: To test the collection format in query parameters + operationId: testQueryParameterCollectionFormat + parameters: + - name: pipe + in: query + required: true + schema: + type: array + items: + type: string + - name: http + in: query + required: true + style: spaceDelimited + schema: + type: array + items: + type: string + - name: url + in: query + required: true + style: form + explode: false + schema: + type: array + items: + type: string + - name: context + in: query + required: true + explode: true + schema: + type: array + items: + type: string + responses: + "200": + description: Success + "/fake/{petId}/uploadImageWithRequiredFile": + post: + tags: + - pet + summary: uploads an image (required) + description: "" + operationId: uploadFileWithRequiredFile + parameters: + - name: petId + in: path + description: ID of pet to update + required: true + schema: + type: integer + format: int64 + requestBody: + content: + multipart/form-data: + schema: + type: object + properties: + additionalMetadata: + description: Additional data to pass to server + type: string + requiredFile: + description: file to upload + type: string + format: binary + required: + - requiredFile + responses: + "200": + description: successful operation + content: + application/json: + schema: + $ref: "#/components/schemas/ApiResponse" + security: + - petstore_auth: + - write:pets + - read:pets + /fake/{petId}/response-object-different-names: + get: + tags: + - pet + operationId: responseObjectDifferentNames + parameters: + - name: petId + in: path + description: ID of pet to update + required: true + schema: + type: integer + format: int64 + responses: + 200: + description: successful operation + content: + application/json: + schema: + $ref: "#/components/schemas/ResponseObjectWithDifferentFieldNames" +servers: + - url: http://petstore.swagger.io:80/v2 +components: + requestBodies: + UserArray: + content: + application/json: + schema: + type: array + items: + $ref: "#/components/schemas/User" + description: List of user object + required: true + Client: + content: + application/json: + schema: + $ref: "#/components/schemas/Client" + description: client model + required: true + Pet: + content: + application/json: + schema: + $ref: "#/components/schemas/Pet" + application/xml: + schema: + $ref: "#/components/schemas/Pet" + description: Pet object that needs to be added to the store + required: true + securitySchemes: + petstore_auth: + type: oauth2 + flows: + implicit: + authorizationUrl: http://petstore.swagger.io/api/oauth/dialog + scopes: + write:pets: modify pets in your account + read:pets: read your pets + api_key: + type: apiKey + name: api_key + in: header + api_key_query: + type: apiKey + name: api_key_query + in: query + http_basic_test: + type: http + scheme: basic + schemas: + Order: + type: object + properties: + id: + type: integer + format: int64 + petId: + type: integer + format: int64 + quantity: + type: integer + format: int32 + shipDate: + type: string + format: date-time + status: + type: string + description: Order Status + enum: + - placed + - approved + - delivered + complete: + type: boolean + default: false + xml: + name: Order + Category: + type: object + required: + - name + properties: + id: + type: integer + format: int64 + name: + type: string + default: default-name + xml: + name: Category + User: + type: object + properties: + id: + type: integer + format: int64 + x-is-unique: true + username: + type: string + firstName: + type: string + lastName: + type: string + email: + type: string + password: + type: string + phone: + type: string + userStatus: + type: integer + format: int32 + description: User Status + xml: + name: User + Tag: + type: object + properties: + id: + type: integer + format: int64 + name: + type: string + xml: + name: Tag + Pet: + type: object + required: + - name + - photoUrls + properties: + id: + type: integer + format: int64 + x-is-unique: true + category: + $ref: "#/components/schemas/Category" + name: + type: string + example: doggie + photoUrls: + type: array + uniqueItems: true + xml: + name: photoUrl + wrapped: true + items: + type: string + tags: + type: array + xml: + name: tag + wrapped: true + items: + $ref: "#/components/schemas/Tag" + status: + type: string + description: pet status in the store + deprecated: true + enum: + - available + - pending + - sold + xml: + name: Pet + ApiResponse: + type: object + properties: + code: + type: integer + format: int32 + type: + type: string + message: + type: string + Return: + description: Model for testing reserved words + properties: + return: + type: integer + format: int32 + xml: + name: Return + Name: + description: Model for testing model name same as property name + required: + - name + properties: + name: + type: integer + format: int32 + snake_case: + readOnly: true + type: integer + format: int32 + property: + type: string + 123Number: + type: integer + readOnly: true + xml: + name: Name + 200_response: + description: Model for testing model name starting with number + properties: + name: + type: integer + format: int32 + class: + type: string + xml: + name: Name + ClassModel: + description: Model for testing model with "_class" property + properties: + _class: + type: string + Dog: + allOf: + - $ref: "#/components/schemas/Animal" + - type: object + properties: + breed: + type: string + Cat: + allOf: + - $ref: "#/components/schemas/Animal" + - type: object + properties: + declawed: + type: boolean + BigCat: + allOf: + - $ref: "#/components/schemas/Cat" + - type: object + properties: + kind: + type: string + enum: + - lions + - tigers + - leopards + - jaguars + Animal: + type: object + discriminator: + propertyName: className + required: + - className + properties: + className: + type: string + color: + type: string + default: red + AnimalFarm: + type: array + items: + $ref: "#/components/schemas/Animal" + format_test: + type: object + required: + - number + - byte + - date + - password + properties: + integer: + type: integer + maximum: 100 + minimum: 10 + int32: + type: integer + format: int32 + maximum: 200 + minimum: 20 + int64: + type: integer + format: int64 + number: + maximum: 543.2 + minimum: 32.1 + type: number + float: + type: number + format: float + maximum: 987.6 + minimum: 54.3 + double: + type: number + format: double + maximum: 123.4 + minimum: 67.8 + string: + type: string + pattern: "[a-zA-Z]" + byte: + type: string + format: byte + binary: + type: string + format: binary + date: + type: string + format: date + dateTime: + type: string + format: date-time + uuid: + type: string + format: uuid + maxLength: 36 + example: 72f98069-206d-4f12-9f12-3d1e525a8e84 + password: + type: string + format: password + maxLength: 64 + minLength: 10 + BigDecimal: + type: string + format: number + EnumClass: + type: string + default: -efg + enum: + - _abc + - -efg + - (xyz) + Enum_Test: + type: object + required: + - enum_string_required + properties: + enum_string: + type: string + enum: + - UPPER + - lower + - "" + enum_string_required: + type: string + enum: + - UPPER + - lower + - "" + enum_integer: + type: integer + format: int32 + enum: + - 1 + - -1 + enum_number: + type: number + format: double + enum: + - 1.1 + - -1.2 + outerEnum: + $ref: "#/components/schemas/OuterEnum" + AdditionalPropertiesClass: + type: object + properties: + map_string: + type: object + additionalProperties: + type: string + map_number: + type: object + additionalProperties: + type: number + map_integer: + type: object + additionalProperties: + type: integer + map_boolean: + type: object + additionalProperties: + type: boolean + map_array_integer: + type: object + additionalProperties: + type: array + items: + type: integer + map_array_anytype: + type: object + additionalProperties: + type: array + items: + type: object + map_map_string: + type: object + additionalProperties: + type: object + additionalProperties: + type: string + map_map_anytype: + type: object + additionalProperties: + type: object + additionalProperties: + type: object + anytype_1: + type: object + anytype_2: {} + anytype_3: + type: object + properties: {} + AdditionalPropertiesString: + type: object + properties: + name: + type: string + additionalProperties: + type: string + AdditionalPropertiesInteger: + type: object + properties: + name: + type: string + additionalProperties: + type: integer + AdditionalPropertiesNumber: + type: object + properties: + name: + type: string + additionalProperties: + type: number + AdditionalPropertiesBoolean: + type: object + properties: + name: + type: string + additionalProperties: + type: boolean + AdditionalPropertiesArray: + type: object + properties: + name: + type: string + additionalProperties: + type: array + items: + type: object + AdditionalPropertiesObject: + type: object + properties: + name: + type: string + additionalProperties: + type: object + additionalProperties: + type: object + AdditionalPropertiesAnyType: + type: object + properties: + name: + type: string + additionalProperties: + type: object + MixedPropertiesAndAdditionalPropertiesClass: + type: object + properties: + uuid: + type: string + format: uuid + dateTime: + type: string + format: date-time + map: + type: object + additionalProperties: + $ref: "#/components/schemas/Animal" + List: + type: object + properties: + 123-list: + type: string + Client: + type: object + properties: + client: + type: string + ReadOnlyFirst: + type: object + properties: + bar: + type: string + readOnly: true + baz: + type: string + hasOnlyReadOnly: + type: object + properties: + bar: + type: string + readOnly: true + foo: + type: string + readOnly: true + Capitalization: + type: object + properties: + smallCamel: + type: string + CapitalCamel: + type: string + small_Snake: + type: string + Capital_Snake: + type: string + SCA_ETH_Flow_Points: + type: string + ATT_NAME: + description: | + Name of the pet + type: string + MapTest: + type: object + properties: + map_map_of_string: + type: object + additionalProperties: + type: object + additionalProperties: + type: string + map_of_enum_string: + type: object + additionalProperties: + type: string + enum: + - UPPER + - lower + direct_map: + type: object + additionalProperties: + type: boolean + indirect_map: + $ref: "#/components/schemas/StringBooleanMap" + ArrayTest: + type: object + properties: + array_of_string: + type: array + items: + type: string + array_array_of_integer: + type: array + items: + type: array + items: + type: integer + format: int64 + array_array_of_model: + type: array + items: + type: array + items: + $ref: "#/components/schemas/ReadOnlyFirst" + NumberOnly: + type: object + properties: + JustNumber: + type: number + ArrayOfNumberOnly: + type: object + properties: + ArrayNumber: + type: array + items: + type: number + ArrayOfArrayOfNumberOnly: + type: object + properties: + ArrayArrayNumber: + type: array + items: + type: array + items: + type: number + EnumArrays: + type: object + properties: + just_symbol: + type: string + enum: + - ">=" + - $ + array_enum: + type: array + items: + type: string + enum: + - fish + - crab + OuterEnum: + type: string + enum: + - placed + - approved + - delivered + OuterComposite: + type: object + properties: + my_number: + $ref: "#/components/schemas/OuterNumber" + my_string: + $ref: "#/components/schemas/OuterString" + my_boolean: + $ref: "#/components/schemas/OuterBoolean" + OuterNumber: + type: number + OuterString: + type: string + OuterBoolean: + type: boolean + x-codegen-body-parameter-name: boolean_post_body + ParentWithNullable: + type: object + discriminator: + propertyName: type + properties: + type: + type: string + enum: + - ChildWithNullable + nullableProperty: + type: string + nullable: true + ChildWithNullable: + allOf: + - $ref: '#/components/schemas/ParentWithNullable' + - type: object + properties: + otherProperty: + type: string + StringBooleanMap: + additionalProperties: + type: boolean + FileSchemaTestClass: + type: object + properties: + file: + $ref: "#/components/schemas/File" + files: + type: array + items: + $ref: "#/components/schemas/File" + File: + type: object + description: Must be named `File` for test. + properties: + sourceURI: + description: Test capitalization + type: string + TypeHolderDefault: + type: object + required: + - string_item + - number_item + - integer_item + - bool_item + - array_item + properties: + string_item: + type: string + default: what + number_item: + type: number + default: 1.234 + integer_item: + type: integer + default: -2 + bool_item: + type: boolean + default: true + array_item: + type: array + items: + type: integer + default: + - 0 + - 1 + - 2 + - 3 + TypeHolderExample: + type: object + required: + - string_item + - number_item + - float_item + - integer_item + - bool_item + - array_item + properties: + string_item: + type: string + example: what + number_item: + type: number + example: 1.234 + float_item: + type: number + example: 1.234 + format: float + integer_item: + type: integer + example: -2 + bool_item: + type: boolean + example: true + array_item: + type: array + items: + type: integer + example: + - 0 + - 1 + - 2 + - 3 + XmlItem: + type: object + xml: + namespace: http://a.com/schema + prefix: pre + properties: + attribute_string: + type: string + example: string + xml: + attribute: true + attribute_number: + type: number + example: 1.234 + xml: + attribute: true + attribute_integer: + type: integer + example: -2 + xml: + attribute: true + attribute_boolean: + type: boolean + example: true + xml: + attribute: true + wrapped_array: + type: array + xml: + wrapped: true + items: + type: integer + name_string: + type: string + example: string + xml: + name: xml_name_string + name_number: + type: number + example: 1.234 + xml: + name: xml_name_number + name_integer: + type: integer + example: -2 + xml: + name: xml_name_integer + name_boolean: + type: boolean + example: true + xml: + name: xml_name_boolean + name_array: + type: array + items: + type: integer + xml: + name: xml_name_array_item + name_wrapped_array: + type: array + xml: + wrapped: true + name: xml_name_wrapped_array + items: + type: integer + xml: + name: xml_name_wrapped_array_item + prefix_string: + type: string + example: string + xml: + prefix: ab + prefix_number: + type: number + example: 1.234 + xml: + prefix: cd + prefix_integer: + type: integer + example: -2 + xml: + prefix: ef + prefix_boolean: + type: boolean + example: true + xml: + prefix: gh + prefix_array: + type: array + items: + type: integer + xml: + prefix: ij + prefix_wrapped_array: + type: array + xml: + wrapped: true + prefix: kl + items: + type: integer + xml: + prefix: mn + namespace_string: + type: string + example: string + xml: + namespace: http://a.com/schema + namespace_number: + type: number + example: 1.234 + xml: + namespace: http://b.com/schema + namespace_integer: + type: integer + example: -2 + xml: + namespace: http://c.com/schema + namespace_boolean: + type: boolean + example: true + xml: + namespace: http://d.com/schema + namespace_array: + type: array + items: + type: integer + xml: + namespace: http://e.com/schema + namespace_wrapped_array: + type: array + xml: + wrapped: true + namespace: http://f.com/schema + items: + type: integer + xml: + namespace: http://g.com/schema + prefix_ns_string: + type: string + example: string + xml: + namespace: http://a.com/schema + prefix: a + prefix_ns_number: + type: number + example: 1.234 + xml: + namespace: http://b.com/schema + prefix: b + prefix_ns_integer: + type: integer + example: -2 + xml: + namespace: http://c.com/schema + prefix: c + prefix_ns_boolean: + type: boolean + example: true + xml: + namespace: http://d.com/schema + prefix: d + prefix_ns_array: + type: array + items: + type: integer + xml: + namespace: http://e.com/schema + prefix: e + prefix_ns_wrapped_array: + type: array + xml: + wrapped: true + namespace: http://f.com/schema + prefix: f + items: + type: integer + xml: + namespace: http://g.com/schema + prefix: g + _special_model.name_: + properties: + "$special[property.name]": + type: integer + format: int64 + xml: + name: $special[model.name] + ContainerDefaultValue: + type: object + required: + - required_array + - nullable_required_array + properties: + nullable_array: + type: array + nullable: true + items: + type: string + nullable_required_array: + type: array + nullable: true + items: + type: string + required_array: + type: array + nullable: false + items: + type: string + nullable_array_with_default: + type: array + nullable: true + items: + type: string + default: ["foo", "bar"] + ResponseObjectWithDifferentFieldNames: + type: object + properties: + normalPropertyName: + type: string + UPPER_CASE_PROPERTY_SNAKE: + type: string + lower-case-property-dashes: + type: string + property name with spaces: + type: string + NullableMapProperty: + type: object + properties: + languageValues: + nullable: true + type: object + additionalProperties: + type: string diff --git a/samples/server/petstore/kotlin-springboot-delegate/src/main/kotlin/org/openapitools/api/PetApi.kt b/samples/server/petstore/kotlin-springboot-delegate/src/main/kotlin/org/openapitools/api/PetApi.kt index 8acb5303e965..6977352fe3d3 100644 --- a/samples/server/petstore/kotlin-springboot-delegate/src/main/kotlin/org/openapitools/api/PetApi.kt +++ b/samples/server/petstore/kotlin-springboot-delegate/src/main/kotlin/org/openapitools/api/PetApi.kt @@ -7,6 +7,8 @@ package org.openapitools.api import org.openapitools.model.ModelApiResponse import org.openapitools.model.Pet +import org.springframework.lang.NonNull +import javax.validation.constraints.NotEmpty import io.swagger.v3.oas.annotations.* import io.swagger.v3.oas.annotations.enums.* import io.swagger.v3.oas.annotations.media.* @@ -59,7 +61,7 @@ interface PetApi { consumes = ["application/json", "application/xml"] ) fun addPet( - @Parameter(description = "Pet object that needs to be added to the store", required = true) @Valid @RequestBody pet: Pet + @Parameter(description = "Pet object that needs to be added to the store", required = true) @NonNull @Valid @RequestBody pet: Pet ): ResponseEntity { return getDelegate().addPet(pet) } @@ -104,7 +106,7 @@ interface PetApi { produces = ["application/xml", "application/json"] ) fun findPetsByStatus( - @NotNull @Parameter(description = "Status values that need to be considered for filter", required = true, schema = Schema(allowableValues = ["available", "pending", "sold"])) @Valid @RequestParam(value = "status", required = true) status: kotlin.collections.List + @NonNull @Size(min = 1) @NotEmpty @NotNull @Parameter(description = "Status values that need to be considered for filter", required = true, schema = Schema(allowableValues = ["available", "pending", "sold"])) @Valid @RequestParam(value = "status", required = true) status: kotlin.collections.List ): ResponseEntity> { return getDelegate().findPetsByStatus(status) } @@ -152,7 +154,7 @@ interface PetApi { produces = ["application/xml", "application/json"] ) fun getPetById( - @Parameter(description = "ID of pet to return", required = true) @PathVariable("petId") petId: kotlin.Long + @NonNull @Parameter(description = "ID of pet to return", required = true) @PathVariable("petId") petId: kotlin.Long ): ResponseEntity { return getDelegate().getPetById(petId) } diff --git a/samples/server/petstore/kotlin-springboot-delegate/src/main/kotlin/org/openapitools/api/StoreApi.kt b/samples/server/petstore/kotlin-springboot-delegate/src/main/kotlin/org/openapitools/api/StoreApi.kt index 485509730aff..9d821b638239 100644 --- a/samples/server/petstore/kotlin-springboot-delegate/src/main/kotlin/org/openapitools/api/StoreApi.kt +++ b/samples/server/petstore/kotlin-springboot-delegate/src/main/kotlin/org/openapitools/api/StoreApi.kt @@ -6,6 +6,7 @@ package org.openapitools.api import org.openapitools.model.Order +import org.springframework.lang.NonNull import io.swagger.v3.oas.annotations.* import io.swagger.v3.oas.annotations.enums.* import io.swagger.v3.oas.annotations.media.* @@ -98,7 +99,7 @@ interface StoreApi { produces = ["application/xml", "application/json"] ) fun getOrderById( - @Min(value=1L) @Max(value=5L) @Parameter(description = "ID of pet that needs to be fetched", required = true) @PathVariable("orderId") orderId: kotlin.Long + @NonNull @Min(value=1L) @Max(value=5L) @Parameter(description = "ID of pet that needs to be fetched", required = true) @PathVariable("orderId") orderId: kotlin.Long ): ResponseEntity { return getDelegate().getOrderById(orderId) } @@ -121,7 +122,7 @@ interface StoreApi { consumes = ["application/json"] ) fun placeOrder( - @Parameter(description = "order placed for purchasing the pet", required = true) @Valid @RequestBody order: Order + @Parameter(description = "order placed for purchasing the pet", required = true) @NonNull @Valid @RequestBody order: Order ): ResponseEntity { return getDelegate().placeOrder(order) } diff --git a/samples/server/petstore/kotlin-springboot-delegate/src/main/resources/openapi.yaml b/samples/server/petstore/kotlin-springboot-delegate/src/main/resources/openapi.yaml index 885b64671054..dc850248ada4 100644 --- a/samples/server/petstore/kotlin-springboot-delegate/src/main/resources/openapi.yaml +++ b/samples/server/petstore/kotlin-springboot-delegate/src/main/resources/openapi.yaml @@ -45,6 +45,8 @@ paths: summary: Add a new pet to the store tags: - pet + x-extra-imports: org.springframework.lang.NonNull + x-request-body-extra-annotation: '@NonNull' put: description: "" externalDocs: @@ -97,6 +99,13 @@ paths: type: string type: array style: form + x-extra-imports: + - org.springframework.lang.NonNull + - javax.validation.constraints.NotEmpty + x-field-extra-annotation: + - '@NonNull' + - '@Size(min = 1)' + - '@NotEmpty' responses: "200": content: @@ -202,6 +211,8 @@ paths: format: int64 type: integer style: simple + x-extra-imports: org.springframework.lang.NonNull + x-field-extra-annotation: '@NonNull' responses: "200": content: diff --git a/samples/server/petstore/kotlin-springboot-reactive/src/main/kotlin/org/openapitools/api/PetApiController.kt b/samples/server/petstore/kotlin-springboot-reactive/src/main/kotlin/org/openapitools/api/PetApiController.kt index 9e04cc7c297a..b630f2913d87 100644 --- a/samples/server/petstore/kotlin-springboot-reactive/src/main/kotlin/org/openapitools/api/PetApiController.kt +++ b/samples/server/petstore/kotlin-springboot-reactive/src/main/kotlin/org/openapitools/api/PetApiController.kt @@ -2,6 +2,8 @@ package org.openapitools.api import org.openapitools.model.ModelApiResponse import org.openapitools.model.Pet +import org.springframework.lang.NonNull +import javax.validation.constraints.NotEmpty import io.swagger.v3.oas.annotations.* import io.swagger.v3.oas.annotations.enums.* import io.swagger.v3.oas.annotations.media.* @@ -52,7 +54,7 @@ class PetApiController(@Autowired(required = true) val service: PetApiService) { consumes = ["application/json", "application/xml"] ) suspend fun addPet( - @Parameter(description = "Pet object that needs to be added to the store", required = true) @Valid @RequestBody pet: Pet + @Parameter(description = "Pet object that needs to be added to the store", required = true) @NonNull @Valid @RequestBody pet: Pet ): ResponseEntity { return ResponseEntity(service.addPet(pet), HttpStatus.valueOf(200)) } @@ -93,7 +95,7 @@ class PetApiController(@Autowired(required = true) val service: PetApiService) { produces = ["application/xml", "application/json"] ) fun findPetsByStatus( - @NotNull @Parameter(description = "Status values that need to be considered for filter", required = true, schema = Schema(allowableValues = ["available", "pending", "sold"])) @Valid @RequestParam(value = "status", required = true) status: kotlin.collections.List + @NonNull @Size(min = 1) @NotEmpty @NotNull @Parameter(description = "Status values that need to be considered for filter", required = true, schema = Schema(allowableValues = ["available", "pending", "sold"])) @Valid @RequestParam(value = "status", required = true) status: kotlin.collections.List ): ResponseEntity> { return ResponseEntity(service.findPetsByStatus(status), HttpStatus.valueOf(200)) } @@ -137,7 +139,7 @@ class PetApiController(@Autowired(required = true) val service: PetApiService) { produces = ["application/xml", "application/json"] ) suspend fun getPetById( - @Parameter(description = "ID of pet to return", required = true) @PathVariable("petId") petId: kotlin.Long + @NonNull @Parameter(description = "ID of pet to return", required = true) @PathVariable("petId") petId: kotlin.Long ): ResponseEntity { return ResponseEntity(service.getPetById(petId), HttpStatus.valueOf(200)) } diff --git a/samples/server/petstore/kotlin-springboot-reactive/src/main/kotlin/org/openapitools/api/StoreApiController.kt b/samples/server/petstore/kotlin-springboot-reactive/src/main/kotlin/org/openapitools/api/StoreApiController.kt index 03750f1b7577..f156825366fe 100644 --- a/samples/server/petstore/kotlin-springboot-reactive/src/main/kotlin/org/openapitools/api/StoreApiController.kt +++ b/samples/server/petstore/kotlin-springboot-reactive/src/main/kotlin/org/openapitools/api/StoreApiController.kt @@ -1,6 +1,7 @@ package org.openapitools.api import org.openapitools.model.Order +import org.springframework.lang.NonNull import io.swagger.v3.oas.annotations.* import io.swagger.v3.oas.annotations.enums.* import io.swagger.v3.oas.annotations.media.* @@ -87,7 +88,7 @@ class StoreApiController(@Autowired(required = true) val service: StoreApiServic produces = ["application/xml", "application/json"] ) suspend fun getOrderById( - @Min(value=1L) @Max(value=5L) @Parameter(description = "ID of pet that needs to be fetched", required = true) @PathVariable("orderId") orderId: kotlin.Long + @NonNull @Min(value=1L) @Max(value=5L) @Parameter(description = "ID of pet that needs to be fetched", required = true) @PathVariable("orderId") orderId: kotlin.Long ): ResponseEntity { return ResponseEntity(service.getOrderById(orderId), HttpStatus.valueOf(200)) } @@ -108,7 +109,7 @@ class StoreApiController(@Autowired(required = true) val service: StoreApiServic consumes = ["application/json"] ) suspend fun placeOrder( - @Parameter(description = "order placed for purchasing the pet", required = true) @Valid @RequestBody order: Order + @Parameter(description = "order placed for purchasing the pet", required = true) @NonNull @Valid @RequestBody order: Order ): ResponseEntity { return ResponseEntity(service.placeOrder(order), HttpStatus.valueOf(200)) } diff --git a/samples/server/petstore/kotlin-springboot-reactive/src/main/resources/openapi.yaml b/samples/server/petstore/kotlin-springboot-reactive/src/main/resources/openapi.yaml index 885b64671054..dc850248ada4 100644 --- a/samples/server/petstore/kotlin-springboot-reactive/src/main/resources/openapi.yaml +++ b/samples/server/petstore/kotlin-springboot-reactive/src/main/resources/openapi.yaml @@ -45,6 +45,8 @@ paths: summary: Add a new pet to the store tags: - pet + x-extra-imports: org.springframework.lang.NonNull + x-request-body-extra-annotation: '@NonNull' put: description: "" externalDocs: @@ -97,6 +99,13 @@ paths: type: string type: array style: form + x-extra-imports: + - org.springframework.lang.NonNull + - javax.validation.constraints.NotEmpty + x-field-extra-annotation: + - '@NonNull' + - '@Size(min = 1)' + - '@NotEmpty' responses: "200": content: @@ -202,6 +211,8 @@ paths: format: int64 type: integer style: simple + x-extra-imports: org.springframework.lang.NonNull + x-field-extra-annotation: '@NonNull' responses: "200": content: diff --git a/samples/server/petstore/springboot-reactive/src/main/java/org/openapitools/api/PetApi.java b/samples/server/petstore/springboot-reactive/src/main/java/org/openapitools/api/PetApi.java index b3ab6be11be1..adabbef42046 100644 --- a/samples/server/petstore/springboot-reactive/src/main/java/org/openapitools/api/PetApi.java +++ b/samples/server/petstore/springboot-reactive/src/main/java/org/openapitools/api/PetApi.java @@ -73,7 +73,7 @@ default PetApiDelegate getDelegate() { consumes = { "application/json", "application/xml" } ) default Mono> addPet( - @Parameter(name = "Pet", description = "Pet object that needs to be added to the store", required = true) @Valid @RequestBody Mono pet, + @Parameter(name = "Pet", description = "Pet object that needs to be added to the store", required = true) @org.springframework.lang.NonNull @Valid @RequestBody Mono pet, @Parameter(hidden = true) final ServerWebExchange exchange ) { return getDelegate().addPet(pet, exchange); @@ -147,7 +147,7 @@ default Mono> deletePet( produces = { "application/xml", "application/json" } ) default Mono>> findPetsByStatus( - @NotNull @Parameter(name = "status", description = "Status values that need to be considered for filter", required = true, in = ParameterIn.QUERY) @RequestParam(value = "status", required = true) List status, + @org.springframework.lang.NonNull @NotNull @Parameter(name = "status", description = "Status values that need to be considered for filter", required = true, in = ParameterIn.QUERY) @RequestParam(value = "status", required = true) List status, @Parameter(hidden = true) final ServerWebExchange exchange ) { return getDelegate().findPetsByStatus(status, exchange); @@ -228,7 +228,7 @@ default Mono>> findPetsByTags( produces = { "application/xml", "application/json" } ) default Mono> getPetById( - @Parameter(name = "petId", description = "ID of pet to return", required = true, in = ParameterIn.PATH) @PathVariable("petId") Long petId, + @org.springframework.lang.NonNull @Parameter(name = "petId", description = "ID of pet to return", required = true, in = ParameterIn.PATH) @PathVariable("petId") Long petId, @Parameter(hidden = true) final ServerWebExchange exchange ) { return getDelegate().getPetById(petId, exchange); diff --git a/samples/server/petstore/springboot-reactive/src/main/java/org/openapitools/api/StoreApi.java b/samples/server/petstore/springboot-reactive/src/main/java/org/openapitools/api/StoreApi.java index 2c52ce3dddf3..08ef69a3df87 100644 --- a/samples/server/petstore/springboot-reactive/src/main/java/org/openapitools/api/StoreApi.java +++ b/samples/server/petstore/springboot-reactive/src/main/java/org/openapitools/api/StoreApi.java @@ -137,7 +137,7 @@ default Mono>> getInventory( produces = { "application/xml", "application/json" } ) default Mono> getOrderById( - @Min(value = 1L) @Max(value = 5L) @Parameter(name = "order_id", description = "ID of pet that needs to be fetched", required = true, in = ParameterIn.PATH) @PathVariable("order_id") Long orderId, + @org.springframework.lang.NonNull @Min(value = 1L) @Max(value = 5L) @Parameter(name = "order_id", description = "ID of pet that needs to be fetched", required = true, in = ParameterIn.PATH) @PathVariable("order_id") Long orderId, @Parameter(hidden = true) final ServerWebExchange exchange ) { return getDelegate().getOrderById(orderId, exchange); @@ -173,7 +173,7 @@ default Mono> getOrderById( consumes = { "application/json" } ) default Mono> placeOrder( - @Parameter(name = "Order", description = "order placed for purchasing the pet", required = true) @Valid @RequestBody Mono order, + @Parameter(name = "Order", description = "order placed for purchasing the pet", required = true) @org.springframework.lang.NonNull @Valid @RequestBody Mono order, @Parameter(hidden = true) final ServerWebExchange exchange ) { return getDelegate().placeOrder(order, exchange); diff --git a/samples/server/petstore/springboot-reactive/src/main/resources/openapi.yaml b/samples/server/petstore/springboot-reactive/src/main/resources/openapi.yaml index 53ec8083c77d..28da4d940572 100644 --- a/samples/server/petstore/springboot-reactive/src/main/resources/openapi.yaml +++ b/samples/server/petstore/springboot-reactive/src/main/resources/openapi.yaml @@ -36,6 +36,7 @@ paths: summary: Add a new pet to the store tags: - pet + x-request-body-extra-annotation: '@org.springframework.lang.NonNull' x-content-type: application/json x-accepts: - application/json @@ -87,6 +88,7 @@ paths: type: string type: array style: form + x-field-extra-annotation: '@org.springframework.lang.NonNull' responses: "200": content: @@ -213,6 +215,7 @@ paths: format: int64 type: integer style: simple + x-field-extra-annotation: '@org.springframework.lang.NonNull' responses: "200": content: diff --git a/samples/server/petstore/springboot-useoptional/src/main/java/org/openapitools/api/PetApi.java b/samples/server/petstore/springboot-useoptional/src/main/java/org/openapitools/api/PetApi.java index e1db1fdc66b3..0df41d5cc22c 100644 --- a/samples/server/petstore/springboot-useoptional/src/main/java/org/openapitools/api/PetApi.java +++ b/samples/server/petstore/springboot-useoptional/src/main/java/org/openapitools/api/PetApi.java @@ -73,7 +73,7 @@ default Optional getRequest() { consumes = { "application/json", "application/xml" } ) default ResponseEntity addPet( - @Parameter(name = "Pet", description = "Pet object that needs to be added to the store", required = true) @Valid @RequestBody Pet pet + @Parameter(name = "Pet", description = "Pet object that needs to be added to the store", required = true) @org.springframework.lang.NonNull @Valid @RequestBody Pet pet ) { return new ResponseEntity<>(HttpStatus.NOT_IMPLEMENTED); @@ -147,7 +147,7 @@ default ResponseEntity deletePet( produces = { "application/xml", "application/json" } ) default ResponseEntity> findPetsByStatus( - @NotNull @Parameter(name = "status", description = "Status values that need to be considered for filter", required = true, in = ParameterIn.QUERY) @RequestParam(value = "status", required = true) List status + @org.springframework.lang.NonNull @NotNull @Parameter(name = "status", description = "Status values that need to be considered for filter", required = true, in = ParameterIn.QUERY) @RequestParam(value = "status", required = true) List status ) { getRequest().ifPresent(request -> { for (MediaType mediaType: MediaType.parseMediaTypes(request.getHeader("Accept"))) { @@ -256,7 +256,7 @@ default ResponseEntity> findPetsByTags( produces = { "application/xml", "application/json" } ) default ResponseEntity getPetById( - @Parameter(name = "petId", description = "ID of pet to return", required = true, in = ParameterIn.PATH) @PathVariable("petId") Long petId + @org.springframework.lang.NonNull @Parameter(name = "petId", description = "ID of pet to return", required = true, in = ParameterIn.PATH) @PathVariable("petId") Long petId ) { getRequest().ifPresent(request -> { for (MediaType mediaType: MediaType.parseMediaTypes(request.getHeader("Accept"))) { diff --git a/samples/server/petstore/springboot-useoptional/src/main/java/org/openapitools/api/StoreApi.java b/samples/server/petstore/springboot-useoptional/src/main/java/org/openapitools/api/StoreApi.java index 0813a3064040..f394ffcaf309 100644 --- a/samples/server/petstore/springboot-useoptional/src/main/java/org/openapitools/api/StoreApi.java +++ b/samples/server/petstore/springboot-useoptional/src/main/java/org/openapitools/api/StoreApi.java @@ -138,7 +138,7 @@ default ResponseEntity> getInventory( produces = { "application/xml", "application/json" } ) default ResponseEntity getOrderById( - @Min(value = 1L) @Max(value = 5L) @Parameter(name = "order_id", description = "ID of pet that needs to be fetched", required = true, in = ParameterIn.PATH) @PathVariable("order_id") Long orderId + @org.springframework.lang.NonNull @Min(value = 1L) @Max(value = 5L) @Parameter(name = "order_id", description = "ID of pet that needs to be fetched", required = true, in = ParameterIn.PATH) @PathVariable("order_id") Long orderId ) { getRequest().ifPresent(request -> { for (MediaType mediaType: MediaType.parseMediaTypes(request.getHeader("Accept"))) { @@ -188,7 +188,7 @@ default ResponseEntity getOrderById( consumes = { "application/json" } ) default ResponseEntity placeOrder( - @Parameter(name = "Order", description = "order placed for purchasing the pet", required = true) @Valid @RequestBody Order order + @Parameter(name = "Order", description = "order placed for purchasing the pet", required = true) @org.springframework.lang.NonNull @Valid @RequestBody Order order ) { getRequest().ifPresent(request -> { for (MediaType mediaType: MediaType.parseMediaTypes(request.getHeader("Accept"))) { diff --git a/samples/server/petstore/springboot-useoptional/src/main/resources/openapi.yaml b/samples/server/petstore/springboot-useoptional/src/main/resources/openapi.yaml index 53ec8083c77d..28da4d940572 100644 --- a/samples/server/petstore/springboot-useoptional/src/main/resources/openapi.yaml +++ b/samples/server/petstore/springboot-useoptional/src/main/resources/openapi.yaml @@ -36,6 +36,7 @@ paths: summary: Add a new pet to the store tags: - pet + x-request-body-extra-annotation: '@org.springframework.lang.NonNull' x-content-type: application/json x-accepts: - application/json @@ -87,6 +88,7 @@ paths: type: string type: array style: form + x-field-extra-annotation: '@org.springframework.lang.NonNull' responses: "200": content: @@ -213,6 +215,7 @@ paths: format: int64 type: integer style: simple + x-field-extra-annotation: '@org.springframework.lang.NonNull' responses: "200": content: