diff --git a/multiapi-engine/pom.xml b/multiapi-engine/pom.xml
index ec41b1ee..6c668e8a 100644
--- a/multiapi-engine/pom.xml
+++ b/multiapi-engine/pom.xml
@@ -4,7 +4,7 @@
com.sngular
multiapi-engine
- 6.4.2
+ 6.5.0
jar
diff --git a/multiapi-engine/src/main/java/com/sngular/api/generator/plugin/common/model/SchemaFieldObject.java b/multiapi-engine/src/main/java/com/sngular/api/generator/plugin/common/model/SchemaFieldObject.java
index 4d546b85..117a998f 100644
--- a/multiapi-engine/src/main/java/com/sngular/api/generator/plugin/common/model/SchemaFieldObject.java
+++ b/multiapi-engine/src/main/java/com/sngular/api/generator/plugin/common/model/SchemaFieldObject.java
@@ -28,4 +28,8 @@ public class SchemaFieldObject {
private Map enumValues;
private Object constValue;
+
+ private String description;
+
+ private String example;
}
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 2f260e18..7de70e5c 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
@@ -42,6 +42,8 @@ public final class ApiTool {
public static final String SCHEMAS = "schemas";
+ public static final String DEFS = "$defs";
+
public static final String REQUIRED = "required";
public static final String PARAMETERS = "parameters";
@@ -153,7 +155,21 @@ public static JsonNode getItems(final JsonNode schema) {
}
public static Map getComponentSchemas(final JsonNode openApi) {
- return getComponentSchemasByType(openApi, SCHEMAS);
+ final var schemasMap = getComponentSchemasByType(openApi, SCHEMAS);
+ // JSON Schema 2020-12 / OpenAPI 3.1: reusable schemas may live under a
+ // top-level `$defs` object instead of `components/schemas`.
+ schemasMap.putAll(getDefsSchemas(openApi));
+ return schemasMap;
+ }
+
+ private static Map getDefsSchemas(final JsonNode openApi) {
+ final var schemasMap = new HashMap();
+ if (hasNode(openApi, DEFS)) {
+ final var defs = getNode(openApi, DEFS);
+ defs.fieldNames().forEachRemaining(name -> schemasMap.put(
+ DEFS.toUpperCase() + "/" + StringCaseUtils.titleToSnakeCase(name), getNode(defs, name)));
+ }
+ return schemasMap;
}
private static Map getComponentSchemasByType(final JsonNode openApi, final String schemaType) {
@@ -226,7 +242,15 @@ public static String getType(final JsonNode schema) {
return "";
}
final JsonNode typeNode = getNode(schema, "type");
- return typeNode.isArray() ? getArrayType(typeNode) : StringUtils.defaultIfEmpty(typeNode.textValue(), "");
+ if (typeNode.isArray()) {
+ return getArrayType(typeNode);
+ }
+ // OpenAPI 3.1 / JSON Schema 2020-12: a scalar `type: "null"` carries no concrete
+ // Java type; degrade to object so we never emit an invalid `null` type.
+ if (TypeConstants.NULL.equalsIgnoreCase(typeNode.textValue())) {
+ return TypeConstants.OBJECT;
+ }
+ return StringUtils.defaultIfEmpty(typeNode.textValue(), "");
}
private static String getArrayType(final JsonNode typeNode) {
@@ -347,10 +371,12 @@ public static boolean isDateTime(final JsonNode schema) {
public static boolean isBinary(final JsonNode schema) {
final boolean isMultipartFile;
if (hasType(schema) && TypeConstants.STRING.equalsIgnoreCase(getType(schema))) {
- if (hasNode(schema, FORMAT)) {
- isMultipartFile = "binary".equalsIgnoreCase(getNode(schema, FORMAT).textValue());
+ if (hasNode(schema, FORMAT) && "binary".equalsIgnoreCase(getNode(schema, FORMAT).textValue())) {
+ isMultipartFile = true;
} else {
- isMultipartFile = false;
+ // OpenAPI 3.1 / JSON Schema 2020-12 replace `format: binary` with the
+ // `contentEncoding` / `contentMediaType` keywords for binary payloads.
+ isMultipartFile = hasNode(schema, "contentEncoding") || hasNode(schema, "contentMediaType");
}
} else {
isMultipartFile = false;
@@ -358,6 +384,46 @@ public static boolean isBinary(final JsonNode schema) {
return isMultipartFile;
}
+ public static boolean isNullable(final JsonNode schema) {
+ if (Objects.isNull(schema)) {
+ return false;
+ }
+ // OpenAPI 3.0 `nullable: true`.
+ if (getNodeAsBoolean(schema, "nullable")) {
+ return true;
+ }
+ // OpenAPI 3.1 idiom: `type: ["", "null"]`.
+ if (hasType(schema)) {
+ final JsonNode typeNode = getNode(schema, "type");
+ if (typeNode.isArray()) {
+ for (final JsonNode element : typeNode) {
+ if (TypeConstants.NULL.equalsIgnoreCase(element.asText())) {
+ return true;
+ }
+ }
+ } else {
+ return TypeConstants.NULL.equalsIgnoreCase(typeNode.textValue());
+ }
+ }
+ return false;
+ }
+
+ public static boolean hasPatternProperties(final JsonNode schema) {
+ return hasNode(schema, "patternProperties");
+ }
+
+ public static JsonNode getPatternProperties(final JsonNode schema) {
+ return getNode(schema, "patternProperties");
+ }
+
+ public static boolean hasPrefixItems(final JsonNode schema) {
+ return hasNode(schema, "prefixItems");
+ }
+
+ public static JsonNode getPrefixItems(final JsonNode schema) {
+ return getNode(schema, "prefixItems");
+ }
+
public static List findContentSchemas(final JsonNode schema) {
return hasNode(schema, "content") ? schema.findValues("schema") : Collections.emptyList();
}
@@ -414,6 +480,32 @@ public static JsonNode nodeFromFile(final FileLocation ymlParent, final String f
return om.readTree(file);
}
+ public static String getDescription(final JsonNode schema) {
+ return getNodeAsString(schema, "description");
+ }
+
+ public static String getExample(final JsonNode schema) {
+ // OpenAPI 3.0 uses a single `example`; OpenAPI 3.1 / JSON Schema 2020-12 use an
+ // `examples` array. Prefer `example`, otherwise take the first `examples` entry.
+ if (hasNode(schema, "example")) {
+ return asExampleText(getNode(schema, "example"));
+ }
+ if (hasNode(schema, "examples")) {
+ final JsonNode examples = getNode(schema, "examples");
+ if (examples.isArray() && examples.elements().hasNext()) {
+ return asExampleText(examples.elements().next());
+ }
+ }
+ return null;
+ }
+
+ private static String asExampleText(final JsonNode example) {
+ if (Objects.isNull(example) || example.isNull()) {
+ return null;
+ }
+ return example.isValueNode() ? example.asText() : example.toString();
+ }
+
public static boolean hasConst(final JsonNode fieldBody) {
return hasNode(fieldBody, "const");
}
diff --git a/multiapi-engine/src/main/java/com/sngular/api/generator/plugin/common/tools/ModelBuilder.java b/multiapi-engine/src/main/java/com/sngular/api/generator/plugin/common/tools/ModelBuilder.java
index 5e05f831..736f476e 100644
--- a/multiapi-engine/src/main/java/com/sngular/api/generator/plugin/common/tools/ModelBuilder.java
+++ b/multiapi-engine/src/main/java/com/sngular/api/generator/plugin/common/tools/ModelBuilder.java
@@ -169,12 +169,12 @@ private static Set getFields(
baseDir));
}
} else if (TypeConstants.ARRAY.equalsIgnoreCase(ApiTool.getType(schema))) {
- final String itemType = ApiTool.hasRef(ApiTool.getItems(schema)) ? MapperUtil.getPojoNameFromRef(ApiTool.getItems(schema), specFile, null)
- : ApiTool.getType(ApiTool.getItems(schema));
fieldObjectArrayList.add(SchemaFieldObject.builder()
.baseName("items")
- .dataType(SchemaFieldObjectType.fromTypeList(TypeConstants.ARRAY, itemType))
+ .dataType(SchemaFieldObjectType.fromTypeList(TypeConstants.ARRAY, resolveArrayItemType(schema, specFile)))
.build());
+ } else if (ApiTool.hasPatternProperties(schema)) {
+ fieldObjectArrayList.add(buildPatternPropertiesField(ApiTool.getName(schema), schema, specFile));
} else if (ApiTool.isAllOf(schema)) {
fieldObjectArrayList.addAll(processAllOf(totalSchemas, ApiTool.getAllOf(schema), specFile, compositedSchemas, antiLoopList, baseDir));
} else if (ApiTool.isAnyOf(schema)) {
@@ -209,6 +209,8 @@ private static List processFieldObjectList(
fieldObjectArrayList.addAll(processArray(fieldName, className, schema, specFile, totalSchemas, compositedSchemas, antiLoopList, baseDir));
} else if (ApiTool.hasAdditionalProperties(schema)) {
fieldObjectArrayList.addAll(processMap(fieldName, schema, specFile, totalSchemas, compositedSchemas, antiLoopList, baseDir));
+ } else if (ApiTool.hasPatternProperties(schema)) {
+ fieldObjectArrayList.add(buildPatternPropertiesField(fieldName, schema, specFile));
} else if (ApiTool.hasRef(schema)) {
fieldObjectArrayList.add(
processRef(fieldName, schema, new SchemaFieldObjectType(MapperUtil.getSimpleType(schema, specFile)), totalSchemas, compositedSchemas, antiLoopList, specFile, baseDir));
@@ -337,9 +339,28 @@ private static List processObjectProperty(
} else {
fieldObjectArrayList.addAll(processFieldObjectList(buildingSchema, fieldName, fieldName, fieldBody, specFile, totalSchemas, compositedSchemas, antiLoopList, baseDir));
}
+ applyMetadata(fieldObjectArrayList, fieldName, fieldBody);
return fieldObjectArrayList;
}
+ private static void applyMetadata(final List fields, final String fieldName, final JsonNode fieldBody) {
+ final String description = ApiTool.getDescription(fieldBody);
+ final String example = ApiTool.getExample(fieldBody);
+ if (Objects.isNull(description) && Objects.isNull(example)) {
+ return;
+ }
+ for (final var field : fields) {
+ if (Objects.equals(field.getBaseName(), fieldName)) {
+ if (Objects.nonNull(description)) {
+ field.setDescription(description);
+ }
+ if (Objects.nonNull(example)) {
+ field.setExample(example);
+ }
+ }
+ }
+ }
+
private static Object getConst(final JsonNode fieldBody) {
return ApiTool.hasConst(fieldBody) ? ApiTool.getConst(fieldBody) : null;
}
@@ -426,11 +447,17 @@ private static List processArray(
final Map compositedSchemas, final Set antiLoopList, final Path baseDir) {
final List fieldObjectArrayList = new LinkedList<>();
- if (!ApiTool.hasItems(schema)) {
+ if (!ApiTool.hasItems(schema) || ApiTool.getItems(schema).isBoolean()) {
+ // No `items` schema, or `items: false` (JSON Schema 2020-12). If the array
+ // declares positional `prefixItems` (tuple), or nothing typable at all, the
+ // element type degrades to Object -> List