diff --git a/multiapi-engine/pom.xml b/multiapi-engine/pom.xml
index af0d2207..ef7ec3f6 100644
--- a/multiapi-engine/pom.xml
+++ b/multiapi-engine/pom.xml
@@ -4,7 +4,7 @@
com.sngular
multiapi-engine
- 6.3.3
+ 6.4.0
jar
diff --git a/multiapi-engine/src/main/java/com/sngular/api/generator/plugin/common/model/TypeConstants.java b/multiapi-engine/src/main/java/com/sngular/api/generator/plugin/common/model/TypeConstants.java
index a4fd82a7..7b98ed51 100644
--- a/multiapi-engine/src/main/java/com/sngular/api/generator/plugin/common/model/TypeConstants.java
+++ b/multiapi-engine/src/main/java/com/sngular/api/generator/plugin/common/model/TypeConstants.java
@@ -18,6 +18,8 @@ public final class TypeConstants {
public static final String ARRAY = "array";
+ public static final String NULL = "null";
+
public static final String MAP = "map";
public static final String BIG_DECIMAL = "bigDecimal";
diff --git a/multiapi-engine/src/main/java/com/sngular/api/generator/plugin/common/tools/ApiTool.java b/multiapi-engine/src/main/java/com/sngular/api/generator/plugin/common/tools/ApiTool.java
index 11333ecf..c271dc9e 100644
--- a/multiapi-engine/src/main/java/com/sngular/api/generator/plugin/common/tools/ApiTool.java
+++ b/multiapi-engine/src/main/java/com/sngular/api/generator/plugin/common/tools/ApiTool.java
@@ -220,7 +220,23 @@ public static boolean hasType(final JsonNode schema) {
}
public static String getType(final JsonNode schema) {
- return hasType(schema) ? StringUtils.defaultIfEmpty(getNodeAsString(schema, "type"), "") : "";
+ if (!hasType(schema)) {
+ return "";
+ }
+ final JsonNode typeNode = getNode(schema, "type");
+ if (typeNode.isArray()) {
+ // OpenAPI 3.1 / JSON Schema 2020-12: "type" may be an array (e.g. ["string", "null"]).
+ // Resolve to the first non-"null" entry; "null" only marks the type as nullable.
+ String resolvedType = "";
+ for (final JsonNode element : typeNode) {
+ if (!TypeConstants.NULL.equalsIgnoreCase(element.asText())) {
+ resolvedType = element.asText();
+ break;
+ }
+ }
+ return resolvedType;
+ }
+ return StringUtils.defaultIfEmpty(typeNode.textValue(), "");
}
public static boolean hasItems(final JsonNode schema) {
diff --git a/multiapi-engine/src/main/java/com/sngular/api/generator/plugin/common/tools/MapperUtil.java b/multiapi-engine/src/main/java/com/sngular/api/generator/plugin/common/tools/MapperUtil.java
index b7734089..0850c200 100644
--- a/multiapi-engine/src/main/java/com/sngular/api/generator/plugin/common/tools/MapperUtil.java
+++ b/multiapi-engine/src/main/java/com/sngular/api/generator/plugin/common/tools/MapperUtil.java
@@ -50,7 +50,7 @@ private static boolean checkIfNumber(final String nodeType) {
private static String processNumber(final JsonNode schema) {
- final var nodeType = schema.get("type").asText();
+ final var nodeType = ApiTool.getType(schema);
final var formatType = schema.has("format") ? schema.get("format").asText() : null;
String type = TypeConstants.INTEGER;
if (TypeConstants.NUMBER.equalsIgnoreCase(nodeType)) {
diff --git a/multiapi-engine/src/test/java/com/sngular/api/generator/plugin/openapi/OpenApiGeneratorFixtures.java b/multiapi-engine/src/test/java/com/sngular/api/generator/plugin/openapi/OpenApiGeneratorFixtures.java
index 8e92e4e4..6299c6f9 100644
--- a/multiapi-engine/src/test/java/com/sngular/api/generator/plugin/openapi/OpenApiGeneratorFixtures.java
+++ b/multiapi-engine/src/test/java/com/sngular/api/generator/plugin/openapi/OpenApiGeneratorFixtures.java
@@ -147,6 +147,13 @@ public final class OpenApiGeneratorFixtures {
.clientPackage("com.sngular.multifileplugin.externalref.client").modelNamePrefix("Api")
.modelNameSuffix("DTO").build());
+ static final List TEST_OPEN_API_31_TYPES = List
+ .of(SpecFile.builder().filePath("openapigenerator/testOpenApi31Types/api-test.yml")
+ .apiPackage("com.sngular.multifileplugin.openapi31types")
+ .modelPackage("com.sngular.multifileplugin.openapi31types.model")
+ .clientPackage("com.sngular.multifileplugin.openapi31types.client")
+ .modelNameSuffix("DTO").build());
+
static final List TEST_EXTERNAL_PATH_ITEM_REF_GENERATION = List
.of(SpecFile.builder().filePath("openapigenerator/testExternalPathItemRefsGeneration/api-test.yml")
.apiPackage("com.sngular.multifileplugin.externalpathitemref")
@@ -770,6 +777,24 @@ static Function validateExternalRefGeneration() {
DEFAULT_MODEL_API, expectedExceptionFiles, DEFAULT_EXCEPTION_API);
}
+ static Function validateOpenApi31Types() {
+
+ final String DEFAULT_TARGET_API = "generated/com/sngular/multifileplugin/openapi31types";
+
+ final String DEFAULT_MODEL_API = "generated/com/sngular/multifileplugin/openapi31types/model";
+
+ final String COMMON_PATH = "openapigenerator/testOpenApi31Types/";
+
+ final String ASSETS_PATH = COMMON_PATH + "assets/";
+
+ final List expectedTestApiFile = List.of(ASSETS_PATH + "ProfileApi.java");
+
+ final List expectedTestApiModelFiles = List.of(ASSETS_PATH + "ProfileDTO.java");
+
+ return path -> commonTest(path, expectedTestApiFile, expectedTestApiModelFiles, DEFAULT_TARGET_API,
+ DEFAULT_MODEL_API, Collections.emptyList(), null);
+ }
+
static Function validateExternalPathItemRefGeneration() {
final String DEFAULT_TARGET_API = "generated/com/sngular/multifileplugin/externalpathitemref";
diff --git a/multiapi-engine/src/test/java/com/sngular/api/generator/plugin/openapi/OpenApiGeneratorTest.java b/multiapi-engine/src/test/java/com/sngular/api/generator/plugin/openapi/OpenApiGeneratorTest.java
index 2bf48a1d..9ff2db7c 100644
--- a/multiapi-engine/src/test/java/com/sngular/api/generator/plugin/openapi/OpenApiGeneratorTest.java
+++ b/multiapi-engine/src/test/java/com/sngular/api/generator/plugin/openapi/OpenApiGeneratorTest.java
@@ -83,6 +83,8 @@ static Stream fileSpecToProcess() {
OpenApiGeneratorFixtures.validateEnumsLombokGeneration()),
Arguments.of("testExternalRefsGeneration", OpenApiGeneratorFixtures.TEST_EXTERNAL_REF_GENERATION,
OpenApiGeneratorFixtures.validateExternalRefGeneration()),
+ Arguments.of("testOpenApi31Types", OpenApiGeneratorFixtures.TEST_OPEN_API_31_TYPES,
+ OpenApiGeneratorFixtures.validateOpenApi31Types()),
Arguments.of("testExternalPathItemRefsGeneration", OpenApiGeneratorFixtures.TEST_EXTERNAL_PATH_ITEM_REF_GENERATION,
OpenApiGeneratorFixtures.validateExternalPathItemRefGeneration()),
Arguments.of("testAnyOfInResponse", OpenApiGeneratorFixtures.TEST_ANY_OF_IN_RESPONSE,
diff --git a/multiapi-engine/src/test/resources/openapigenerator/testOpenApi31Types/api-test.yml b/multiapi-engine/src/test/resources/openapigenerator/testOpenApi31Types/api-test.yml
new file mode 100644
index 00000000..352d08d2
--- /dev/null
+++ b/multiapi-engine/src/test/resources/openapigenerator/testOpenApi31Types/api-test.yml
@@ -0,0 +1,49 @@
+---
+# Reproduction/regression for OpenAPI 3.1.x support (issue #375).
+# Exercises JSON Schema 2020-12 array-valued `type`, including the ["", "null"]
+# nullable idiom that replaces 3.0's `nullable: true`, plus a int64 and a plain array.
+openapi: "3.1.0"
+info:
+ version: 1.0.0
+ title: Profile API (OpenAPI 3.1)
+ license:
+ name: MIT
+servers:
+ - url: http://localhost:8080/v1
+tags:
+ - name: profile
+paths:
+ /profile:
+ get:
+ summary: Get the profile
+ operationId: getProfile
+ tags:
+ - profile
+ responses:
+ '200':
+ description: The requested profile
+ content:
+ application/json:
+ schema:
+ $ref: "#/components/schemas/Profile"
+components:
+ schemas:
+ Profile:
+ type: object
+ properties:
+ id:
+ type: string
+ nickname:
+ type: ["string", "null"]
+ age:
+ type: ["integer", "null"]
+ score:
+ type: ["number", "null"]
+ format: double
+ loginCount:
+ type: integer
+ format: int64
+ tags:
+ type: array
+ items:
+ type: string
diff --git a/multiapi-engine/src/test/resources/openapigenerator/testOpenApi31Types/assets/ProfileApi.java b/multiapi-engine/src/test/resources/openapigenerator/testOpenApi31Types/assets/ProfileApi.java
new file mode 100644
index 00000000..549f239b
--- /dev/null
+++ b/multiapi-engine/src/test/resources/openapigenerator/testOpenApi31Types/assets/ProfileApi.java
@@ -0,0 +1,46 @@
+package com.sngular.multifileplugin.openapi31types;
+
+import java.util.Optional;
+import java.util.List;
+import java.util.Map;
+import javax.validation.Valid;
+
+import io.swagger.v3.oas.annotations.Operation;
+import io.swagger.v3.oas.annotations.Parameter;
+import io.swagger.v3.oas.annotations.media.Content;
+import io.swagger.v3.oas.annotations.media.Schema;
+import io.swagger.v3.oas.annotations.responses.ApiResponse;
+import org.springframework.http.MediaType;
+import org.springframework.http.HttpStatus;
+import org.springframework.http.ResponseEntity;
+import org.springframework.web.bind.annotation.*;
+import org.springframework.web.context.request.NativeWebRequest;
+
+import com.sngular.multifileplugin.openapi31types.model.ProfileDTO;
+
+public interface ProfileApi {
+
+ /**
+ * GET /profile: Get the profile
+ * @return The requested profile; (status code 200)
+ */
+
+ @Operation(
+ operationId = "getProfile",
+ summary = "Get the profile",
+ tags = {"profile"},
+ responses = {
+ @ApiResponse(responseCode = "200", description = "The requested profile", content = @Content(mediaType = "application/json", schema = @Schema(implementation = ProfileDTO.class)))
+ }
+ )
+ @RequestMapping(
+ method = RequestMethod.GET,
+ value = "/profile",
+ produces = {"application/json"}
+ )
+
+ default ResponseEntity getProfile() {
+ return new ResponseEntity<>(HttpStatus.NOT_IMPLEMENTED);
+ }
+
+}
diff --git a/multiapi-engine/src/test/resources/openapigenerator/testOpenApi31Types/assets/ProfileDTO.java b/multiapi-engine/src/test/resources/openapigenerator/testOpenApi31Types/assets/ProfileDTO.java
new file mode 100644
index 00000000..7a0671d0
--- /dev/null
+++ b/multiapi-engine/src/test/resources/openapigenerator/testOpenApi31Types/assets/ProfileDTO.java
@@ -0,0 +1,177 @@
+package com.sngular.multifileplugin.openapi31types.model;
+
+import java.util.Objects;
+
+import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
+import com.fasterxml.jackson.databind.annotation.JsonPOJOBuilder;
+import com.fasterxml.jackson.annotation.JsonProperty;
+import io.swagger.v3.oas.annotations.media.Schema;
+import java.util.List;
+import java.util.ArrayList;
+
+@JsonDeserialize(builder = ProfileDTO.ProfileDTOBuilder.class)
+public class ProfileDTO {
+
+ @JsonProperty(value ="tags")
+ private List tags;
+ @JsonProperty(value ="id")
+ private String id;
+ @JsonProperty(value ="loginCount")
+ private Long loginCount;
+ @JsonProperty(value ="age")
+ private Integer age;
+ @JsonProperty(value ="score")
+ private Double score;
+ @JsonProperty(value ="nickname")
+ private String nickname;
+
+ private ProfileDTO(ProfileDTOBuilder builder) {
+ this.tags = builder.tags;
+ this.id = builder.id;
+ this.loginCount = builder.loginCount;
+ this.age = builder.age;
+ this.score = builder.score;
+ this.nickname = builder.nickname;
+
+ }
+
+ public static ProfileDTO.ProfileDTOBuilder builder() {
+ return new ProfileDTO.ProfileDTOBuilder();
+ }
+
+ @JsonPOJOBuilder(buildMethodName = "build", withPrefix = "")
+ public static class ProfileDTOBuilder {
+
+ private List tags = new ArrayList();
+ private String id;
+ private Long loginCount;
+ private Integer age;
+ private Double score;
+ private String nickname;
+
+ public ProfileDTO.ProfileDTOBuilder tags(List tags) {
+ if (!tags.isEmpty()) {
+ this.tags.addAll(tags);
+ }
+ return this;
+ }
+
+ public ProfileDTO.ProfileDTOBuilder tag(String tag) {
+ if (Objects.nonNull(tag)) {
+ this.tags.add(tag);
+ }
+ return this;
+ }
+
+ public ProfileDTO.ProfileDTOBuilder id(String id) {
+ this.id = id;
+ return this;
+ }
+
+ public ProfileDTO.ProfileDTOBuilder loginCount(Long loginCount) {
+ this.loginCount = loginCount;
+ return this;
+ }
+
+ public ProfileDTO.ProfileDTOBuilder age(Integer age) {
+ this.age = age;
+ return this;
+ }
+
+ public ProfileDTO.ProfileDTOBuilder score(Double score) {
+ this.score = score;
+ return this;
+ }
+
+ public ProfileDTO.ProfileDTOBuilder nickname(String nickname) {
+ this.nickname = nickname;
+ return this;
+ }
+
+ public ProfileDTO build() {
+ ProfileDTO profileDTO = new ProfileDTO(this);
+ return profileDTO;
+ }
+ }
+
+ @Schema(name = "tags", required = false)
+ public List getTags() {
+ return tags;
+ }
+ public void setTags(List tags) {
+ this.tags = tags;
+ }
+
+ @Schema(name = "id", required = false)
+ public String getId() {
+ return id;
+ }
+ public void setId(String id) {
+ this.id = id;
+ }
+
+ @Schema(name = "loginCount", required = false)
+ public Long getLoginCount() {
+ return loginCount;
+ }
+ public void setLoginCount(Long loginCount) {
+ this.loginCount = loginCount;
+ }
+
+ @Schema(name = "age", required = false)
+ public Integer getAge() {
+ return age;
+ }
+ public void setAge(Integer age) {
+ this.age = age;
+ }
+
+ @Schema(name = "score", required = false)
+ public Double getScore() {
+ return score;
+ }
+ public void setScore(Double score) {
+ this.score = score;
+ }
+
+ @Schema(name = "nickname", required = false)
+ public String getNickname() {
+ return nickname;
+ }
+ public void setNickname(String nickname) {
+ this.nickname = nickname;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) {
+ return true;
+ }
+ if (o == null || getClass() != o.getClass()) {
+ return false;
+ }
+ ProfileDTO profileDTO = (ProfileDTO) o;
+ return Objects.equals(this.tags, profileDTO.tags) && Objects.equals(this.id, profileDTO.id) && Objects.equals(this.loginCount, profileDTO.loginCount) && Objects.equals(this.age, profileDTO.age) && Objects.equals(this.score, profileDTO.score) && Objects.equals(this.nickname, profileDTO.nickname);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(tags, id, loginCount, age, score, nickname);
+ }
+
+ @Override
+ public String toString() {
+ StringBuilder sb = new StringBuilder();
+ sb.append("ProfileDTO{");
+ sb.append(" tags:").append(tags).append(",");
+ sb.append(" id:").append(id).append(",");
+ sb.append(" loginCount:").append(loginCount).append(",");
+ sb.append(" age:").append(age).append(",");
+ sb.append(" score:").append(score).append(",");
+ sb.append(" nickname:").append(nickname);
+ sb.append("}");
+ return sb.toString();
+ }
+
+
+}
diff --git a/scs-multiapi-gradle-plugin/build.gradle b/scs-multiapi-gradle-plugin/build.gradle
index 410ffb23..67976a57 100644
--- a/scs-multiapi-gradle-plugin/build.gradle
+++ b/scs-multiapi-gradle-plugin/build.gradle
@@ -21,7 +21,7 @@ repositories {
}
group = 'com.sngular'
-version = '6.3.3'
+version = '6.4.0'
def SCSMultiApiPluginGroupId = group
def SCSMultiApiPluginVersion = version
@@ -31,7 +31,7 @@ dependencies {
shadow localGroovy()
shadow gradleApi()
- implementation 'com.sngular:multiapi-engine:6.3.3'
+ implementation 'com.sngular:multiapi-engine:6.4.0'
testImplementation 'org.assertj:assertj-core:3.24.2'
testImplementation 'com.puppycrawl.tools:checkstyle:10.12.3'
testImplementation 'org.junit.platform:junit-platform-launcher:1.9.2'
@@ -100,7 +100,7 @@ testing {
integrationTest(JvmTestSuite) {
dependencies {
- implementation 'com.sngular:scs-multiapi-gradle-plugin:6.3.1'
+ implementation 'com.sngular:scs-multiapi-gradle-plugin:6.4.0'
implementation 'org.assertj:assertj-core:3.24.2'
}
diff --git a/scs-multiapi-gradle-plugin/src/main/groovy/com/sngular/api/generator/plugin/AsyncApiTask.groovy b/scs-multiapi-gradle-plugin/src/main/groovy/com/sngular/api/generator/plugin/AsyncApiTask.groovy
index 74c39b13..2eab7796 100644
--- a/scs-multiapi-gradle-plugin/src/main/groovy/com/sngular/api/generator/plugin/AsyncApiTask.groovy
+++ b/scs-multiapi-gradle-plugin/src/main/groovy/com/sngular/api/generator/plugin/AsyncApiTask.groovy
@@ -17,7 +17,9 @@ import org.gradle.api.file.DirectoryProperty
import org.gradle.api.tasks.Optional
import org.gradle.api.tasks.OutputDirectory
import org.gradle.api.tasks.TaskAction
+import org.gradle.work.DisableCachingByDefault
+@DisableCachingByDefault(because = "Generation depends on external spec files that are not declared as cacheable inputs")
abstract class AsyncApiTask extends DefaultTask {
@Optional
diff --git a/scs-multiapi-gradle-plugin/src/main/groovy/com/sngular/api/generator/plugin/OpenApiTask.groovy b/scs-multiapi-gradle-plugin/src/main/groovy/com/sngular/api/generator/plugin/OpenApiTask.groovy
index b7b21fe0..f5b13937 100644
--- a/scs-multiapi-gradle-plugin/src/main/groovy/com/sngular/api/generator/plugin/OpenApiTask.groovy
+++ b/scs-multiapi-gradle-plugin/src/main/groovy/com/sngular/api/generator/plugin/OpenApiTask.groovy
@@ -16,7 +16,9 @@ import org.gradle.api.file.DirectoryProperty
import org.gradle.api.tasks.Optional
import org.gradle.api.tasks.OutputDirectory
import org.gradle.api.tasks.TaskAction
+import org.gradle.work.DisableCachingByDefault
+@DisableCachingByDefault(because = "Generation depends on external spec files that are not declared as cacheable inputs")
abstract class OpenApiTask extends DefaultTask {
@Optional
diff --git a/scs-multiapi-maven-plugin/pom.xml b/scs-multiapi-maven-plugin/pom.xml
index 80a69f6c..b9e8fd8b 100644
--- a/scs-multiapi-maven-plugin/pom.xml
+++ b/scs-multiapi-maven-plugin/pom.xml
@@ -4,7 +4,7 @@
com.sngular
scs-multiapi-maven-plugin
- 6.3.3
+ 6.4.0
maven-plugin
AsyncApi - OpenApi Code Generator Maven Plugin
@@ -271,7 +271,7 @@
com.sngular
multiapi-engine
- 6.3.3
+ 6.4.0
org.apache.maven