diff --git a/README.md b/README.md
index e0104c40e..d6936e371 100644
--- a/README.md
+++ b/README.md
@@ -103,6 +103,12 @@ To force the AP to generate with `@javax.inject.Singleton`(in the case where you
```
+OpenAPI output is generated as OpenAPI 3.1.
+
+The generated document sets:
+
+- `openapi: 3.1.2`
+
### Usage with Javalin
The annotation processor will generate controller classes implementing the `AvajeJavalinPlugin` interface, which we can register in javalin using:
diff --git a/http-generator-client/pom.xml b/http-generator-client/pom.xml
index 38aa5f7a9..6fbec804c 100644
--- a/http-generator-client/pom.xml
+++ b/http-generator-client/pom.xml
@@ -63,6 +63,10 @@
io.avaje.http.generator.core
io.avaje.http.client.generator.core
+
+ io.avaje.http.openapi
+ io.avaje.http.client.generator.openapi
+
io.swagger.v3
io.avaje.http.client.generator.swagger.v3
diff --git a/http-generator-core/pom.xml b/http-generator-core/pom.xml
index b4d05025d..4028ff4f4 100644
--- a/http-generator-core/pom.xml
+++ b/http-generator-core/pom.xml
@@ -35,6 +35,12 @@
provided
+
+ io.avaje
+ avaje-http-openapi-core
+ ${project.version}
+
+
io.swagger.core.v3
swagger-models
diff --git a/http-generator-core/src/main/java/io/avaje/http/generator/core/openapi/DocContext.java b/http-generator-core/src/main/java/io/avaje/http/generator/core/openapi/DocContext.java
index c82b411dd..218264c09 100644
--- a/http-generator-core/src/main/java/io/avaje/http/generator/core/openapi/DocContext.java
+++ b/http-generator-core/src/main/java/io/avaje/http/generator/core/openapi/DocContext.java
@@ -31,6 +31,7 @@
import io.swagger.v3.oas.models.Operation;
import io.swagger.v3.oas.models.PathItem;
import io.swagger.v3.oas.models.Paths;
+import io.swagger.v3.oas.models.SpecVersion;
import io.swagger.v3.oas.models.info.Info;
import io.swagger.v3.oas.models.media.Content;
import io.swagger.v3.oas.models.media.Schema;
@@ -65,6 +66,9 @@ public boolean isOpenApiAvailable() {
private OpenAPI initOpenAPI() {
OpenAPI openAPI = new OpenAPI();
+ openAPI.setOpenapi("3.1.2");
+ openAPI.setSpecVersion(SpecVersion.V31);
+ openAPI.setJsonSchemaDialect("https://spec.openapis.org/oas/3.1/dialect/base");
openAPI.setPaths(new Paths());
Server server = new Server();
diff --git a/http-generator-core/src/main/java/io/avaje/http/generator/core/openapi/KnownTypes.java b/http-generator-core/src/main/java/io/avaje/http/generator/core/openapi/KnownTypes.java
index 5888f85b6..d2d9dcda9 100644
--- a/http-generator-core/src/main/java/io/avaje/http/generator/core/openapi/KnownTypes.java
+++ b/http-generator-core/src/main/java/io/avaje/http/generator/core/openapi/KnownTypes.java
@@ -101,7 +101,7 @@ public Schema> createSchema() {
private class BoolType implements KnownType {
@Override
public Schema> createSchema() {
- return new BooleanSchema().nullable(Boolean.FALSE);
+ return new BooleanSchema();
}
}
@@ -115,14 +115,14 @@ public Schema> createSchema() {
private class IntType implements KnownType {
@Override
public Schema> createSchema() {
- return new IntegerSchema().nullable(Boolean.FALSE);
+ return new IntegerSchema();
}
}
private class IntArrayType implements KnownType {
@Override
public Schema> createSchema() {
- return new ArraySchema().items(new IntegerSchema().nullable(Boolean.FALSE));
+ return new ArraySchema().items(new IntegerSchema());
}
}
@@ -136,14 +136,14 @@ public Schema> createSchema() {
private class PLongType implements KnownType {
@Override
public Schema> createSchema() {
- return new IntegerSchema().format("int64").nullable(Boolean.FALSE);
+ return new IntegerSchema().format("int64");
}
}
private class PLongArrayType implements KnownType {
@Override
public Schema> createSchema() {
- return new ArraySchema().items(new IntegerSchema().format("int64").nullable(Boolean.FALSE));
+ return new ArraySchema().items(new IntegerSchema().format("int64"));
}
}
@@ -157,14 +157,14 @@ public Schema> createSchema() {
private class PNumberType implements KnownType {
@Override
public Schema> createSchema() {
- return new NumberSchema().nullable(Boolean.FALSE);
+ return new NumberSchema();
}
}
private class PNumberArrayType implements KnownType {
@Override
public Schema> createSchema() {
- return new ArraySchema().items(new NumberSchema().nullable(Boolean.FALSE));
+ return new ArraySchema().items(new NumberSchema());
}
}
diff --git a/http-generator-core/src/main/java/io/avaje/http/generator/core/openapi/OpenAPISerializer.java b/http-generator-core/src/main/java/io/avaje/http/generator/core/openapi/OpenAPISerializer.java
index 9536eb9c8..4b280208f 100644
--- a/http-generator-core/src/main/java/io/avaje/http/generator/core/openapi/OpenAPISerializer.java
+++ b/http-generator-core/src/main/java/io/avaje/http/generator/core/openapi/OpenAPISerializer.java
@@ -1,10 +1,14 @@
package io.avaje.http.generator.core.openapi;
import java.lang.reflect.Field;
+import java.lang.reflect.Modifier;
import java.util.Collection;
import java.util.Map;
import java.util.Set;
+import io.avaje.http.openapi.OpenApiSchemaNormalizer;
+import io.swagger.v3.oas.models.media.Schema;
+
final class OpenAPISerializer {
private static final Set IGNORED_FIELDS = Set.of(
@@ -17,7 +21,7 @@ final class OpenAPISerializer {
"USE_ARBITRARY_SCHEMA_PROPERTY",
"exampleSetFlag",
"defaultSetFlag",
- "types",
+ "nullable",
"specVersion");
private OpenAPISerializer() {}
@@ -77,6 +81,9 @@ static String serialize(Object obj) throws IllegalAccessException {
var firstField = true;
for (final Field field : fields) {
+ if (Modifier.isStatic(field.getModifiers())) {
+ continue;
+ }
// skip JsonIgnored fields
if (IGNORED_FIELDS
@@ -85,14 +92,31 @@ static String serialize(Object obj) throws IllegalAccessException {
}
field.setAccessible(true);
- final var value = field.get(obj);
+ Object value = field.get(obj);
+ if (obj instanceof Schema>) {
+ final Schema> schema = (Schema>) obj;
+ if ("type".equals(field.getName()) && OpenApiSchemaNormalizer.hasTypeSet(schema)) {
+ continue;
+ }
+ if (skipSchemaField(schema, field.getName())) {
+ continue;
+ }
+ if ("types".equals(field.getName())) {
+ value = OpenApiSchemaNormalizer.schemaTypeValue(schema);
+ } else if ("exclusiveMaximumValue".equals(field.getName())) {
+ value = OpenApiSchemaNormalizer.exclusiveMaximumValue(schema);
+ } else if ("exclusiveMinimumValue".equals(field.getName())) {
+ value = OpenApiSchemaNormalizer.exclusiveMinimumValue(schema);
+ }
+ }
if (value != null) {
if (!firstField) {
sb.append(",");
}
+ final var jsonFieldName = jsonFieldName(obj, field.getName());
sb.append("\"");
- sb.append(field.getName().replace("_enum", "enum"));
+ sb.append(jsonFieldName);
sb.append("\" : ");
write(sb, value);
firstField = false;
@@ -128,6 +152,32 @@ static Field[] getAllFields(Class> clazz) {
}
}
+ private static boolean skipSchemaField(Schema> schema, String fieldName) {
+ if ("exclusiveMaximum".equals(fieldName) || "exclusiveMinimum".equals(fieldName)) {
+ return true;
+ }
+ if ("maximum".equals(fieldName) && OpenApiSchemaNormalizer.omitMaximum(schema)) {
+ return true;
+ }
+ return "minimum".equals(fieldName) && OpenApiSchemaNormalizer.omitMinimum(schema);
+ }
+
+ private static String jsonFieldName(Object container, String fieldName) {
+ if ("_enum".equals(fieldName)) {
+ return "enum";
+ }
+ if (container instanceof Schema> && "types".equals(fieldName)) {
+ return "type";
+ }
+ if (container instanceof Schema> && "exclusiveMaximumValue".equals(fieldName)) {
+ return "exclusiveMaximum";
+ }
+ if (container instanceof Schema> && "exclusiveMinimumValue".equals(fieldName)) {
+ return "exclusiveMinimum";
+ }
+ return fieldName;
+ }
+
static boolean isPrimitiveWrapperType(Object value) {
return value instanceof Boolean
@@ -178,7 +228,9 @@ static Object extractPrimitiveValue(Object object) {
static void write(StringBuilder sb, Object value) throws IllegalAccessException {
final var isPrimitiveWrapper = isPrimitiveWrapperType(value);
// Append primitive or string value as is
- if (value.getClass().isPrimitive() || value instanceof String || isPrimitiveWrapper) {
+ if (value instanceof Number) {
+ sb.append(value);
+ } else if (value.getClass().isPrimitive() || value instanceof String || isPrimitiveWrapper) {
if (isPrimitiveWrapper) {
sb.append(extractPrimitiveValue(value));
} else {
diff --git a/http-generator-core/src/main/java/io/avaje/http/generator/core/openapi/SchemaDocBuilder.java b/http-generator-core/src/main/java/io/avaje/http/generator/core/openapi/SchemaDocBuilder.java
index 44cee7780..7ac81ec8f 100644
--- a/http-generator-core/src/main/java/io/avaje/http/generator/core/openapi/SchemaDocBuilder.java
+++ b/http-generator-core/src/main/java/io/avaje/http/generator/core/openapi/SchemaDocBuilder.java
@@ -4,6 +4,7 @@
import io.avaje.http.generator.core.SchemaPrism;
import io.avaje.http.generator.core.javadoc.Javadoc;
+import io.avaje.http.openapi.OpenApiSchemaNormalizer;
import io.swagger.v3.oas.models.media.StringSchema;
import java.util.ArrayList;
import java.util.List;
@@ -139,11 +140,13 @@ private static TypeMirror typeArgument(TypeMirror type) {
}
Schema> toSchema(Element element) {
- final var schema = toSchema(element.asType());
+ var schema = toSchema(element.asType());
setLengthMinMax(element, schema);
setFormatFromValidation(element, schema);
- if (isNotNullable(element)) {
- schema.setNullable(Boolean.FALSE);
+ if (isNullable(element)) {
+ schema = OpenApiSchemaNormalizer.nullable(schema);
+ } else if (isNotNullable(element) && !isOptionalType(element.asType())) {
+ schema = OpenApiSchemaNormalizer.notNullable(schema);
}
return schema;
}
@@ -151,15 +154,17 @@ Schema> toSchema(Element element) {
Schema> toSchema(TypeMirror type) {
final Optional prism = Optional.ofNullable(APContext.asTypeElement(type))
.flatMap(SchemaPrism::getOptionalOn);
+ final Schema> schema;
if (prism.isPresent()) {
final SchemaPrism schemaPrism = prism.get();
- final Schema schema = SchemaPrismHelper.implementation(schemaPrism)
+ schema = SchemaPrismHelper.implementation(schemaPrism)
.map(this::toSchemaImpl)
.orElseGet(() -> (Schema) toSchemaImpl(type));
SchemaPrismHelper.overwriteFromPrism(schema, schemaPrism);
- return schema;
+ } else {
+ schema = toSchemaImpl(type);
}
- return toSchemaImpl(type);
+ return isNullable(type) ? OpenApiSchemaNormalizer.nullable(schema) : schema;
}
private Schema> toSchemaImpl(TypeMirror type) {
@@ -229,8 +234,7 @@ private Schema> buildOptionalSchema(TypeMirror type) {
}
}
// Since it's explicitly optional, we should explicitly say it's nullable
- itemSchema.nullable(Boolean.TRUE);
- return itemSchema;
+ return OpenApiSchemaNormalizer.nullable(itemSchema);
}
private Schema> buildIterableSchema(TypeMirror type) {
@@ -240,8 +244,10 @@ private Schema> buildIterableSchema(TypeMirror type) {
if (typeArguments.size() == 1) {
TypeMirror typeMirror = typeArguments.get(0);
itemSchema = toSchema(typeMirror);
- if (isNotNullable(typeMirror)) {
- itemSchema.setNullable(Boolean.FALSE);
+ if (isNullable(typeMirror)) {
+ itemSchema = OpenApiSchemaNormalizer.nullable(itemSchema);
+ } else if (isNotNullable(typeMirror)) {
+ itemSchema = OpenApiSchemaNormalizer.notNullable(itemSchema);
}
}
}
@@ -253,7 +259,11 @@ private Schema> buildIterableSchema(TypeMirror type) {
private Schema> buildArraySchema(TypeMirror type) {
ArrayType arrayType = (ArrayType) type;
- Schema> itemSchema = toSchema(arrayType.getComponentType());
+ final var componentType = arrayType.getComponentType();
+ Schema> itemSchema = toSchema(componentType);
+ if (isNullable(componentType)) {
+ itemSchema = OpenApiSchemaNormalizer.nullable(itemSchema);
+ }
ArraySchema arraySchema = new ArraySchema();
arraySchema.setItems(itemSchema);
@@ -269,8 +279,10 @@ private Schema> buildMapSchema(TypeMirror type) {
if (typeArguments.size() == 2) {
TypeMirror valueType = typeArguments.get(1);
valueSchema = toSchema(valueType);
- if (isNotNullable(valueType)) {
- valueSchema.setNullable(Boolean.FALSE);
+ if (isNullable(valueType)) {
+ valueSchema = OpenApiSchemaNormalizer.nullable(valueSchema);
+ } else if (isNotNullable(valueType)) {
+ valueSchema = OpenApiSchemaNormalizer.notNullable(valueSchema);
}
}
}
@@ -313,15 +325,18 @@ private void populateObjectSchema(TypeMirror objectType, Schema objectSch
.flatMap(SchemaPrismHelper::implementation)
.map(this::toSchema)
.orElseGet(() -> (Schema) toSchema(fieldType));
- if (isNotNullable(field)) {
- propSchema.setNullable(Boolean.FALSE);
+ if (isNullable(field)) {
+ propSchema = OpenApiSchemaNormalizer.nullable(propSchema);
+ } else if (isNotNullable(field) && !isOptionalType(fieldType)) {
+ propSchema = OpenApiSchemaNormalizer.notNullable(propSchema);
objectSchema.addRequiredItem(field.getSimpleName().toString());
}
setDescription(field, propSchema);
setLengthMinMax(field, propSchema);
setFormatFromValidation(field, propSchema);
+ final Schema> finalPropSchema = propSchema;
SchemaPrism.getOptionalOn(field)
- .ifPresent(schemaPrism -> SchemaPrismHelper.overwriteFromPrism(propSchema, schemaPrism));
+ .ifPresent(schemaPrism -> SchemaPrismHelper.overwriteFromPrism(finalPropSchema, schemaPrism));
objectSchema.addProperties(field.getSimpleName().toString(), propSchema);
}
}
@@ -377,19 +392,10 @@ private void setLengthMinMax(Element element, Schema> propSchema) {
}
private boolean isNotNullable(Element element) {
- List annotationMirrors = new ArrayList<>();
- if (element instanceof VariableElement) {
- annotationMirrors.addAll(element.asType().getAnnotationMirrors());
- } else {
- annotationMirrors.addAll(element.getAnnotationMirrors());
- }
-
+ final var annotationMirrors = annotationMirrors(element);
if (Util.nullMarked(element)) {
- for (var mirror : annotationMirrors) {
- if ("Nullable"
- .equals(APContext.asTypeElement(mirror.getAnnotationType()).getSimpleName().toString())) {
- return false;
- }
+ if (annotationMirrors.stream().anyMatch(this::isNullableAnnotation)) {
+ return false;
}
return true;
}
@@ -401,15 +407,33 @@ private boolean isNotNullable(Element element) {
);
}
- private boolean isNotNullable(TypeMirror type) {
- List extends AnnotationMirror> annotationMirrors = type.getAnnotationMirrors();
+ private boolean isNullable(Element element) {
+ return annotationMirrors(element).stream().anyMatch(this::isNullableAnnotation);
+ }
- for (AnnotationMirror annotationMirror : annotationMirrors) {
- if ("org.jspecify.annotations.Nullable".equals(annotationMirror.getAnnotationType().asElement().toString())) {
- return false;
- }
+ private List annotationMirrors(Element element) {
+ List annotationMirrors = new ArrayList<>();
+ annotationMirrors.addAll(element.getAnnotationMirrors());
+ if (element instanceof VariableElement) {
+ annotationMirrors.addAll(element.asType().getAnnotationMirrors());
}
- return true;
+ return annotationMirrors;
+ }
+
+ private boolean isNullableAnnotation(AnnotationMirror mirror) {
+ return "Nullable".equals(mirror.getAnnotationType().asElement().getSimpleName().toString());
+ }
+
+ private boolean isOptionalType(TypeMirror type) {
+ return types.isAssignable(type, optionalType);
+ }
+
+ private boolean isNotNullable(TypeMirror type) {
+ return !isNullable(type);
+ }
+
+ private boolean isNullable(TypeMirror type) {
+ return type.getAnnotationMirrors().stream().anyMatch(this::isNullableAnnotation);
}
/**
diff --git a/http-generator-core/src/main/java/module-info.java b/http-generator-core/src/main/java/module-info.java
index ba600ade0..3d612306c 100644
--- a/http-generator-core/src/main/java/module-info.java
+++ b/http-generator-core/src/main/java/module-info.java
@@ -8,6 +8,7 @@
requires java.compiler;
// SHADED: All content after this line will be removed at package time
+ requires io.avaje.http.openapi;
requires static io.avaje.prism;
requires static io.avaje.http.api;
requires static io.avaje.htmx.api;
diff --git a/http-generator-core/src/test/java/io/avaje/http/generator/core/openapi/OpenAPISerializerTest.java b/http-generator-core/src/test/java/io/avaje/http/generator/core/openapi/OpenAPISerializerTest.java
new file mode 100644
index 000000000..a2ab2da62
--- /dev/null
+++ b/http-generator-core/src/test/java/io/avaje/http/generator/core/openapi/OpenAPISerializerTest.java
@@ -0,0 +1,76 @@
+package io.avaje.http.generator.core.openapi;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+import io.swagger.v3.oas.models.media.Schema;
+import java.math.BigDecimal;
+import java.util.LinkedHashSet;
+import java.util.Map;
+import java.util.Set;
+import org.junit.jupiter.api.Test;
+
+class OpenAPISerializerTest {
+
+ @Test
+ void serializesSchemaTypesAsTypeArray() throws IllegalAccessException {
+ final var schema = new Schema<>().types(new LinkedHashSet<>(Set.of("string", "null")));
+
+ final var json = OpenAPISerializer.serialize(schema);
+
+ assertThat(json).contains("\"type\" : [");
+ assertThat(json).contains("\"null\"");
+ assertThat(json).doesNotContain("\"types\"");
+ }
+
+ @Test
+ void serializesSingleSchemaTypeAsScalar() throws IllegalAccessException {
+ final var schema = new Schema<>().types(new LinkedHashSet<>(Set.of("string")));
+
+ final var json = OpenAPISerializer.serialize(schema);
+
+ assertThat(json).contains("\"type\" : \"string\"");
+ assertThat(json).doesNotContain("\"type\" : [");
+ assertThat(json).doesNotContain("\"types\"");
+ }
+
+ @Test
+ void serializesNumericExclusiveBounds() throws IllegalAccessException {
+ final var schema = new Schema<>()
+ .type("number")
+ .exclusiveMinimumValue(new BigDecimal("1.5"))
+ .exclusiveMaximumValue(new BigDecimal("9.5"));
+
+ final var json = OpenAPISerializer.serialize(schema);
+
+ assertThat(json).contains("\"exclusiveMinimum\" : 1.5");
+ assertThat(json).contains("\"exclusiveMaximum\" : 9.5");
+ assertThat(json).doesNotContain("\"exclusiveMinimumValue\"");
+ assertThat(json).doesNotContain("\"exclusiveMaximumValue\"");
+ }
+
+ @Test
+ void serializesLegacyBooleanExclusiveBoundsAsNumericBounds() throws IllegalAccessException {
+ final var schema = new Schema<>()
+ .type("integer")
+ .minimum(BigDecimal.ONE)
+ .exclusiveMinimum(true)
+ .maximum(BigDecimal.TEN)
+ .exclusiveMaximum(true);
+
+ final var json = OpenAPISerializer.serialize(schema);
+
+ assertThat(json).contains("\"exclusiveMinimum\" : 1");
+ assertThat(json).contains("\"exclusiveMaximum\" : 10");
+ assertThat(json).doesNotContain("\"minimum\" : 1");
+ assertThat(json).doesNotContain("\"maximum\" : 10");
+ assertThat(json).doesNotContain("\"exclusiveMinimum\" : true");
+ assertThat(json).doesNotContain("\"exclusiveMaximum\" : true");
+ }
+
+ @Test
+ void serializesBigDecimalAsJsonNumber() throws IllegalAccessException {
+ final var json = OpenAPISerializer.serialize(Map.of("value", new BigDecimal("12.34")));
+
+ assertThat(json).isEqualTo("{\"value\" : 12.34}");
+ }
+}
diff --git a/openapi-core/pom.xml b/openapi-core/pom.xml
new file mode 100644
index 000000000..a6814448a
--- /dev/null
+++ b/openapi-core/pom.xml
@@ -0,0 +1,26 @@
+
+
+ 4.0.0
+
+ io.avaje
+ avaje-http-parent
+ 3.9-RC3
+
+
+ avaje-http-openapi-core
+ avaje-http-openapi-core
+
+
+
+ io.swagger.core.v3
+ swagger-models
+ ${swagger.version}
+
+
+ com.fasterxml.jackson.core
+ jackson-annotations
+
+
+
+
+
diff --git a/openapi-core/src/main/java/io/avaje/http/openapi/OpenApiSchemaNormalizer.java b/openapi-core/src/main/java/io/avaje/http/openapi/OpenApiSchemaNormalizer.java
new file mode 100644
index 000000000..e6b6d0ed6
--- /dev/null
+++ b/openapi-core/src/main/java/io/avaje/http/openapi/OpenApiSchemaNormalizer.java
@@ -0,0 +1,412 @@
+package io.avaje.http.openapi;
+
+import io.swagger.v3.oas.models.media.ArbitrarySchema;
+import io.swagger.v3.oas.models.media.ArraySchema;
+import io.swagger.v3.oas.models.media.BinarySchema;
+import io.swagger.v3.oas.models.media.BooleanSchema;
+import io.swagger.v3.oas.models.media.ByteArraySchema;
+import io.swagger.v3.oas.models.media.DateSchema;
+import io.swagger.v3.oas.models.media.DateTimeSchema;
+import io.swagger.v3.oas.models.media.EmailSchema;
+import io.swagger.v3.oas.models.media.IntegerSchema;
+import io.swagger.v3.oas.models.media.NumberSchema;
+import io.swagger.v3.oas.models.media.ObjectSchema;
+import io.swagger.v3.oas.models.media.PasswordSchema;
+import io.swagger.v3.oas.models.media.Schema;
+import io.swagger.v3.oas.models.media.StringSchema;
+import io.swagger.v3.oas.models.media.UUIDSchema;
+import java.math.BigDecimal;
+import java.util.ArrayList;
+import java.util.LinkedHashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+/** Shared OpenAPI 3.1 schema normalization helpers. */
+public final class OpenApiSchemaNormalizer {
+
+ private static final String NULL_TYPE = "null";
+
+ private OpenApiSchemaNormalizer() {}
+
+ /** Mark a schema as allowing JSON null using OpenAPI 3.1 / JSON Schema syntax. */
+ public static Schema> nullable(Schema> schema) {
+ if (schema == null) {
+ return null;
+ }
+ if (allowsNull(schema)) {
+ schema.setNullable(null);
+ return schema;
+ }
+ if (isReferenceOrComposite(schema)) {
+ return nullableWrapper(schema);
+ }
+ final var types = effectiveTypes(schema);
+ if (types.isEmpty()) {
+ return nullableWrapper(schema);
+ }
+ types.add(NULL_TYPE);
+ applyTypes(schema, types);
+ schema.setNullable(null);
+ return schema;
+ }
+
+ /** Mark a schema as not allowing JSON null. */
+ public static Schema> notNullable(Schema> schema) {
+ if (schema == null) {
+ return null;
+ }
+ schema.setNullable(null);
+ if (schema.getTypes() != null && schema.getTypes().contains(NULL_TYPE)) {
+ final var types = new LinkedHashSet(schema.getTypes());
+ types.remove(NULL_TYPE);
+ applyTypes(schema, types);
+ }
+ schema.setAnyOf(withoutNullSchema(schema.getAnyOf()));
+ schema.setOneOf(withoutNullSchema(schema.getOneOf()));
+ return unwrapSingleAnyOfOrOneOf(schema);
+ }
+
+ /** Normalize legacy nullable fields and nested schemas to OpenAPI 3.1 form. */
+ public static Schema> normalize(Schema> schema) {
+ if (schema == null) {
+ return null;
+ }
+ normalizeNested(schema);
+ final Boolean nullable = schema.getNullable();
+ if (Boolean.TRUE.equals(nullable)) {
+ return nullable(schema);
+ }
+ if (Boolean.FALSE.equals(nullable)) {
+ return notNullable(schema);
+ }
+ normalizeTypeFields(schema);
+ return schema;
+ }
+
+ /** Effective non-null first type from a schema. */
+ public static String firstNonNullType(Schema> schema) {
+ if (schema == null) {
+ return null;
+ }
+ final var type = schema.getType();
+ if (isType(type)) {
+ return type;
+ }
+ if (schema.getTypes() != null) {
+ for (final String candidate : schema.getTypes()) {
+ if (isType(candidate)) {
+ return candidate;
+ }
+ }
+ }
+ return null;
+ }
+
+ /** Normalize schema type data, including legacy nullable, to a type set. */
+ public static Set normalizeTypes(
+ final String type, final Set types, final Boolean nullable) {
+
+ final var normalized = effectiveTypes(type, types);
+ if (Boolean.TRUE.equals(nullable)) {
+ normalized.add(NULL_TYPE);
+ } else if (Boolean.FALSE.equals(nullable)) {
+ normalized.remove(NULL_TYPE);
+ }
+ return normalized.isEmpty() ? null : normalized;
+ }
+
+ /** Type to use when picking a concrete Swagger schema subclass. */
+ public static String schemaTypeForCreation(final String type, final Set types) {
+ if (isType(type)) {
+ return type;
+ }
+ if (types == null || types.isEmpty()) {
+ return null;
+ }
+ for (final String candidate : types) {
+ if (isType(candidate)) {
+ return candidate;
+ }
+ }
+ return null;
+ }
+
+ /** Return the scalar type when the normalized set contains exactly one type. */
+ public static String scalarSchemaType(final Set normalizedTypes) {
+ if (normalizedTypes == null || normalizedTypes.size() != 1) {
+ return null;
+ }
+ return normalizedTypes.iterator().next();
+ }
+
+ /** Return the JSON value for a Schema type keyword. */
+ public static Object schemaTypeValue(Schema> schema) {
+ final var types = normalizeTypes(schema.getType(), schema.getTypes(), schema.getNullable());
+ if (types == null || types.isEmpty()) {
+ return null;
+ }
+ if (types.size() == 1) {
+ return types.iterator().next();
+ }
+ return types;
+ }
+
+ /** True when schema has an OpenAPI/JSON Schema type set. */
+ public static boolean hasTypeSet(Schema> schema) {
+ return schema != null && schema.getTypes() != null && !schema.getTypes().isEmpty();
+ }
+
+ /** Whether the schema has explicit type/nullability information for merge precedence. */
+ public static boolean hasTypeInformation(Schema> schema) {
+ return schema != null
+ && (isType(schema.getType())
+ || (schema.getTypes() != null && !schema.getTypes().isEmpty())
+ || schema.getNullable() != null);
+ }
+
+ /** Effective type set for merging. Includes scalar type values. */
+ public static Set effectiveTypesForMerge(Schema> schema) {
+ if (schema == null) {
+ return null;
+ }
+ final var normalized = normalizeTypes(schema.getType(), schema.getTypes(), schema.getNullable());
+ return normalized == null || normalized.isEmpty() ? null : normalized;
+ }
+
+ /** Create the Swagger schema subclass matching an OpenAPI type and format. */
+ public static Schema> createSchemaFromInformation(final String type, final String format) {
+ if (type == null) {
+ return new ArbitrarySchema();
+ }
+ switch (type) {
+ case "array":
+ return new ArraySchema().format(format);
+ case "boolean":
+ return new BooleanSchema().format(format);
+ case "integer":
+ return new IntegerSchema().format(format);
+ case "object":
+ return new ObjectSchema().format(format);
+ case "number":
+ return new NumberSchema().format(format);
+ case "string":
+ return stringSchema(format);
+ default:
+ return new ArbitrarySchema().format(format).type(type);
+ }
+ }
+
+ /** Numeric exclusiveMaximum value for OpenAPI 3.1 output. */
+ public static BigDecimal exclusiveMaximumValue(Schema> schema) {
+ if (schema == null) {
+ return null;
+ }
+ final var value = schema.getExclusiveMaximumValue();
+ if (value != null) {
+ return value;
+ }
+ return Boolean.TRUE.equals(schema.getExclusiveMaximum()) ? schema.getMaximum() : null;
+ }
+
+ /** Numeric exclusiveMinimum value for OpenAPI 3.1 output. */
+ public static BigDecimal exclusiveMinimumValue(Schema> schema) {
+ if (schema == null) {
+ return null;
+ }
+ final var value = schema.getExclusiveMinimumValue();
+ if (value != null) {
+ return value;
+ }
+ return Boolean.TRUE.equals(schema.getExclusiveMinimum()) ? schema.getMinimum() : null;
+ }
+
+ /** Maximum should be omitted when a legacy exclusiveMaximum boolean consumed it. */
+ public static boolean omitMaximum(Schema> schema) {
+ return schema != null
+ && schema.getExclusiveMaximumValue() == null
+ && Boolean.TRUE.equals(schema.getExclusiveMaximum())
+ && schema.getMaximum() != null;
+ }
+
+ /** Minimum should be omitted when a legacy exclusiveMinimum boolean consumed it. */
+ public static boolean omitMinimum(Schema> schema) {
+ return schema != null
+ && schema.getExclusiveMinimumValue() == null
+ && Boolean.TRUE.equals(schema.getExclusiveMinimum())
+ && schema.getMinimum() != null;
+ }
+
+ private static Schema> stringSchema(String format) {
+ if (format == null) {
+ return new StringSchema();
+ }
+ switch (format) {
+ case "binary":
+ return new BinarySchema();
+ case "byte":
+ return new ByteArraySchema();
+ case "date":
+ return new DateSchema();
+ case "date-time":
+ return new DateTimeSchema();
+ case "email":
+ return new EmailSchema();
+ case "password":
+ return new PasswordSchema();
+ case "uuid":
+ return new UUIDSchema();
+ default:
+ return new StringSchema().format(format);
+ }
+ }
+
+ private static void normalizeNested(Schema> schema) {
+ schema.setNot(normalize(schema.getNot()));
+ schema.setItems(normalize(schema.getItems()));
+ schema.setContains(normalize(schema.getContains()));
+ schema.setContentSchema(normalize(schema.getContentSchema()));
+ schema.setPropertyNames(normalize(schema.getPropertyNames()));
+ schema.setUnevaluatedProperties(normalize(schema.getUnevaluatedProperties()));
+ schema.setAdditionalItems(normalize(schema.getAdditionalItems()));
+ schema.setUnevaluatedItems(normalize(schema.getUnevaluatedItems()));
+ schema.setIf(normalize(schema.getIf()));
+ schema.setElse(normalize(schema.getElse()));
+ schema.setThen(normalize(schema.getThen()));
+ schema.setProperties(normalizeMap(schema.getProperties()));
+ schema.setPatternProperties(normalizeMap(schema.getPatternProperties()));
+ schema.setDependentSchemas(normalizeMap(schema.getDependentSchemas()));
+ schema.setAdditionalProperties(normalizeAdditionalProperties(schema.getAdditionalProperties()));
+ schema.setPrefixItems(normalizeList(schema.getPrefixItems()));
+ schema.setAllOf(normalizeList(schema.getAllOf()));
+ schema.setAnyOf(normalizeList(schema.getAnyOf()));
+ schema.setOneOf(normalizeList(schema.getOneOf()));
+ }
+
+ private static Map normalizeMap(Map schemas) {
+ if (schemas == null) {
+ return null;
+ }
+ schemas.replaceAll((key, schema) -> (Schema) normalize(schema));
+ return schemas;
+ }
+
+ private static Object normalizeAdditionalProperties(Object additionalProperties) {
+ if (additionalProperties instanceof Schema>) {
+ return normalize((Schema>) additionalProperties);
+ }
+ return additionalProperties;
+ }
+
+ private static List normalizeList(List schemas) {
+ if (schemas == null) {
+ return null;
+ }
+ for (int i = 0; i < schemas.size(); i++) {
+ schemas.set(i, (Schema) normalize(schemas.get(i)));
+ }
+ return schemas;
+ }
+
+ private static void normalizeTypeFields(Schema> schema) {
+ final var types = normalizeTypes(schema.getType(), schema.getTypes(), null);
+ if (types != null && !types.isEmpty()) {
+ applyTypes(schema, types);
+ }
+ schema.setNullable(null);
+ }
+
+ private static LinkedHashSet effectiveTypes(Schema> schema) {
+ return effectiveTypes(schema.getType(), schema.getTypes());
+ }
+
+ private static LinkedHashSet effectiveTypes(String type, Set types) {
+ final var normalized = new LinkedHashSet();
+ if (isType(type)) {
+ normalized.add(type);
+ } else if (NULL_TYPE.equals(type)) {
+ normalized.add(NULL_TYPE);
+ }
+ if (types != null) {
+ for (final String candidate : types) {
+ if (isType(candidate) || NULL_TYPE.equals(candidate)) {
+ normalized.add(candidate);
+ }
+ }
+ }
+ return normalized;
+ }
+
+ private static void applyTypes(Schema> schema, Set types) {
+ if (types == null || types.isEmpty()) {
+ schema.setTypes(null);
+ return;
+ }
+ schema.setType(null);
+ schema.setTypes(new LinkedHashSet<>(types));
+ }
+
+ private static boolean isType(String type) {
+ return type != null && !type.isBlank() && !NULL_TYPE.equals(type);
+ }
+
+ private static boolean isReferenceOrComposite(Schema> schema) {
+ return schema.get$ref() != null
+ || notEmpty(schema.getAllOf())
+ || notEmpty(schema.getAnyOf())
+ || notEmpty(schema.getOneOf())
+ || schema.getNot() != null;
+ }
+
+ private static boolean notEmpty(List> value) {
+ return value != null && !value.isEmpty();
+ }
+
+ private static boolean allowsNull(Schema> schema) {
+ return NULL_TYPE.equals(schema.getType())
+ || (schema.getTypes() != null && schema.getTypes().contains(NULL_TYPE))
+ || hasNullSchema(schema.getAnyOf())
+ || hasNullSchema(schema.getOneOf());
+ }
+
+ private static Schema> nullableWrapper(Schema> schema) {
+ final var anyOf = new ArrayList();
+ schema.setNullable(null);
+ anyOf.add(schema);
+ anyOf.add(new Schema<>().type(NULL_TYPE));
+ return new Schema<>().anyOf(anyOf);
+ }
+
+ private static boolean hasNullSchema(List schemas) {
+ return schemas != null && schemas.stream().anyMatch(OpenApiSchemaNormalizer::isNullOnlySchema);
+ }
+
+ private static List withoutNullSchema(List schemas) {
+ if (schemas == null) {
+ return null;
+ }
+ schemas.removeIf(OpenApiSchemaNormalizer::isNullOnlySchema);
+ return schemas.isEmpty() ? null : schemas;
+ }
+
+ private static boolean isNullOnlySchema(Schema> schema) {
+ if (schema == null) {
+ return false;
+ }
+ if (NULL_TYPE.equals(schema.getType())) {
+ return true;
+ }
+ final var types = schema.getTypes();
+ return types != null && types.size() == 1 && types.contains(NULL_TYPE);
+ }
+
+ private static Schema> unwrapSingleAnyOfOrOneOf(Schema> schema) {
+ if (schema.getAnyOf() != null && schema.getAnyOf().size() == 1 && schema.getOneOf() == null) {
+ return schema.getAnyOf().get(0);
+ }
+ if (schema.getOneOf() != null && schema.getOneOf().size() == 1 && schema.getAnyOf() == null) {
+ return schema.getOneOf().get(0);
+ }
+ return schema;
+ }
+}
diff --git a/openapi-core/src/main/java/module-info.java b/openapi-core/src/main/java/module-info.java
new file mode 100644
index 000000000..8546346df
--- /dev/null
+++ b/openapi-core/src/main/java/module-info.java
@@ -0,0 +1,6 @@
+module io.avaje.http.openapi {
+
+ exports io.avaje.http.openapi;
+
+ requires io.swagger.v3.oas.models;
+}
diff --git a/openapi-core/src/test/java/io/avaje/http/openapi/OpenApiSchemaNormalizerTest.java b/openapi-core/src/test/java/io/avaje/http/openapi/OpenApiSchemaNormalizerTest.java
new file mode 100644
index 000000000..215689039
--- /dev/null
+++ b/openapi-core/src/test/java/io/avaje/http/openapi/OpenApiSchemaNormalizerTest.java
@@ -0,0 +1,91 @@
+package io.avaje.http.openapi;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+import io.swagger.v3.oas.models.media.NumberSchema;
+import io.swagger.v3.oas.models.media.Schema;
+import io.swagger.v3.oas.models.media.StringSchema;
+import java.math.BigDecimal;
+import java.util.LinkedHashSet;
+import java.util.List;
+import org.junit.jupiter.api.Test;
+
+class OpenApiSchemaNormalizerTest {
+
+ @Test
+ void nullableScalarUsesTypeUnion() {
+ final var schema = OpenApiSchemaNormalizer.nullable(new StringSchema());
+
+ assertThat(schema.getType()).isNull();
+ assertThat(schema.getTypes()).containsExactly("string", "null");
+ assertThat(schema.getNullable()).isNull();
+ }
+
+ @Test
+ void nullableReferenceUsesAnyOfWrapper() {
+ final var reference = new Schema<>().$ref("#/components/schemas/Foo").nullable(true);
+
+ final var schema = OpenApiSchemaNormalizer.nullable(reference);
+
+ assertThat(schema).isNotSameAs(reference);
+ assertThat(reference.getNullable()).isNull();
+ assertThat(schema.getAnyOf()).hasSize(2);
+ assertThat(schema.getAnyOf().get(0).get$ref()).isEqualTo("#/components/schemas/Foo");
+ assertThat(schema.getAnyOf().get(1).getType()).isEqualTo("null");
+ }
+
+ @Test
+ void nullableWrapperIsIdempotent() {
+ final var reference = new Schema<>().$ref("#/components/schemas/Foo");
+
+ final var schema = OpenApiSchemaNormalizer.nullable(OpenApiSchemaNormalizer.nullable(reference));
+
+ assertThat(schema.getAnyOf()).hasSize(2);
+ assertThat(schema.getAnyOf().get(0).get$ref()).isEqualTo("#/components/schemas/Foo");
+ assertThat(schema.getAnyOf().get(1).getType()).isEqualTo("null");
+ }
+
+ @Test
+ void notNullableRemovesNullFromTypeUnion() {
+ final var schema = new Schema<>().types(new LinkedHashSet<>(List.of("string", "null")));
+
+ final var result = OpenApiSchemaNormalizer.notNullable(schema);
+
+ assertThat(result).isSameAs(schema);
+ assertThat(schema.getType()).isNull();
+ assertThat(schema.getTypes()).containsExactly("string");
+ assertThat(schema.getNullable()).isNull();
+ }
+
+ @Test
+ void notNullableUnwrapsNullableReferenceWrapper() {
+ final var reference = new Schema<>().$ref("#/components/schemas/Foo");
+ final var wrapper = OpenApiSchemaNormalizer.nullable(reference);
+
+ final var schema = OpenApiSchemaNormalizer.notNullable(wrapper);
+
+ assertThat(schema.get$ref()).isEqualTo("#/components/schemas/Foo");
+ }
+
+ @Test
+ void legacyExclusiveBoundsConvertToNumericBounds() {
+ final var schema = new NumberSchema()
+ .minimum(new BigDecimal("1.5"))
+ .exclusiveMinimum(true)
+ .maximum(new BigDecimal("9.5"))
+ .exclusiveMaximum(true);
+
+ assertThat(OpenApiSchemaNormalizer.exclusiveMinimumValue(schema)).isEqualByComparingTo("1.5");
+ assertThat(OpenApiSchemaNormalizer.exclusiveMaximumValue(schema)).isEqualByComparingTo("9.5");
+ assertThat(OpenApiSchemaNormalizer.omitMinimum(schema)).isTrue();
+ assertThat(OpenApiSchemaNormalizer.omitMaximum(schema)).isTrue();
+ }
+
+ @Test
+ void normalizeTypesKeepsConcreteTypeBeforeNull() {
+ final var types = OpenApiSchemaNormalizer.normalizeTypes(
+ "string", new LinkedHashSet<>(List.of("null")), true);
+
+ assertThat(types).containsExactly("string", "null");
+ }
+}
diff --git a/openapi-maven-plugin/pom.xml b/openapi-maven-plugin/pom.xml
index f9f587d62..5119bf0d6 100644
--- a/openapi-maven-plugin/pom.xml
+++ b/openapi-maven-plugin/pom.xml
@@ -31,6 +31,12 @@
+
+ io.avaje
+ avaje-http-openapi-core
+ ${project.version}
+
+
org.apache.maven
maven-plugin-api
diff --git a/openapi-maven-plugin/src/main/java/io/avaje/http/maven/openapi/OpenAPIMergerUtil.java b/openapi-maven-plugin/src/main/java/io/avaje/http/maven/openapi/OpenAPIMergerUtil.java
index 63dcd9f96..536173b43 100644
--- a/openapi-maven-plugin/src/main/java/io/avaje/http/maven/openapi/OpenAPIMergerUtil.java
+++ b/openapi-maven-plugin/src/main/java/io/avaje/http/maven/openapi/OpenAPIMergerUtil.java
@@ -1,12 +1,13 @@
package io.avaje.http.maven.openapi;
-import io.avaje.http.maven.openapi.jsonb.SchemaCustomAdaptor;
+import io.avaje.http.openapi.OpenApiSchemaNormalizer;
import io.avaje.jsonb.Json;
import io.swagger.v3.oas.models.Components;
import io.swagger.v3.oas.models.ExternalDocumentation;
import io.swagger.v3.oas.models.OpenAPI;
import io.swagger.v3.oas.models.PathItem;
import io.swagger.v3.oas.models.Paths;
+import io.swagger.v3.oas.models.SpecVersion;
import io.swagger.v3.oas.models.info.Contact;
import io.swagger.v3.oas.models.info.Info;
import io.swagger.v3.oas.models.info.License;
@@ -24,6 +25,7 @@
import java.util.HashMap;
import java.util.List;
import java.util.Map;
+import java.util.Set;
import java.util.function.BiFunction;
import java.util.function.Function;
@@ -71,7 +73,7 @@ public static OpenAPI merge(final OpenAPI primary, final OpenAPI secondary) {
return secondary;
} else {
final OpenAPI merged = new OpenAPI();
- merged.setOpenapi(firstNotBlank(primary.getOpenapi(), secondary.getOpenapi()));
+ merged.setOpenapi(preferredOpenApiVersion(primary.getOpenapi(), secondary.getOpenapi()));
merged.setInfo(merge(primary.getInfo(), secondary.getInfo()));
merged.setExternalDocs(merge(primary.getExternalDocs(), secondary.getExternalDocs()));
merged.setServers(merge(primary.getServers(), secondary.getServers(), Server::getUrl));
@@ -81,11 +83,7 @@ public static OpenAPI merge(final OpenAPI primary, final OpenAPI secondary) {
merged.setComponents(merge(primary.getComponents(), secondary.getComponents()));
merged.setExtensions(merge(primary.getExtensions(), secondary.getExtensions()));
merged.setJsonSchemaDialect(firstNotBlank(primary.getJsonSchemaDialect(), secondary.getJsonSchemaDialect()));
- if (primary.getSpecVersion() != null) {
- merged.setSpecVersion(primary.getSpecVersion());
- } else if (secondary.getSpecVersion() != null) {
- merged.setSpecVersion(secondary.getSpecVersion());
- }
+ merged.setSpecVersion(preferredSpecVersion(primary.getSpecVersion(), secondary.getSpecVersion()));
return merged;
}
}
@@ -124,6 +122,74 @@ private static T firstNonNull(final T primary, final T secondary) {
return primary;
}
+ private static String preferredOpenApiVersion(final String primary, final String secondary) {
+ final var primaryVersion = firstNotBlank(primary, null);
+ final var secondaryVersion = firstNotBlank(secondary, null);
+ if (primaryVersion == null) {
+ return secondaryVersion;
+ }
+ if (secondaryVersion == null) {
+ return primaryVersion;
+ }
+ final int primaryRank = openApiVersionRank(primaryVersion);
+ final int secondaryRank = openApiVersionRank(secondaryVersion);
+ if (secondaryRank > primaryRank) {
+ return secondaryVersion;
+ }
+ return primaryVersion;
+ }
+
+ private static int openApiVersionRank(final String version) {
+ final var normalized = version.trim();
+ final var parts = normalized.split("\\.");
+ if (parts.length < 2) {
+ return 0;
+ }
+ try {
+ return Integer.parseInt(parts[0]) * 100 + Integer.parseInt(parts[1]);
+ } catch (NumberFormatException ignored) {
+ return 0;
+ }
+ }
+
+ private static SpecVersion preferredSpecVersion(final SpecVersion primary, final SpecVersion secondary) {
+ if (primary == SpecVersion.V31 || secondary == SpecVersion.V31) {
+ return SpecVersion.V31;
+ }
+ if (primary == SpecVersion.V30 || secondary == SpecVersion.V30) {
+ return SpecVersion.V30;
+ }
+ return firstNonNull(primary, secondary);
+ }
+
+ private static String preferredSchemaType(final Schema primary, final Schema secondary) {
+ return firstNotBlank(
+ OpenApiSchemaNormalizer.firstNonNullType(primary),
+ OpenApiSchemaNormalizer.firstNonNullType(secondary));
+ }
+
+ private static Set mergedSchemaTypes(
+ final Schema primary,
+ final Schema secondary,
+ final String preferredType) {
+
+ if (OpenApiSchemaNormalizer.hasTypeInformation(primary)) {
+ return schemaTypesForMerge(primary, preferredType);
+ }
+ if (OpenApiSchemaNormalizer.hasTypeInformation(secondary)) {
+ return schemaTypesForMerge(secondary, preferredType);
+ }
+ return null;
+ }
+
+ private static Set schemaTypesForMerge(final Schema schema, final String preferredType) {
+ final Set types = OpenApiSchemaNormalizer.effectiveTypesForMerge(schema);
+ if (types != null && types.stream().anyMatch(type -> !"null".equals(type))) {
+ return types;
+ }
+ return OpenApiSchemaNormalizer.normalizeTypes(preferredType, types, schema.getNullable());
+ }
+
private static Map merge(final Map primary, final Map secondary) {
if (secondary == null) {
return primary;
@@ -310,7 +376,7 @@ private static PathItem merge(final PathItem primary, final PathItem secondary)
merged.setTrace(firstNonNull(primary.getTrace(), secondary.getTrace()));
merged.setServers(merge(primary.getServers(), secondary.getServers(), Server::getUrl));
merged.setParameters(merge(primary.getParameters(), secondary.getParameters(), Parameter::getName));
- merged.set$ref(firstNotBlank(primary.get$ref(), primary.get$ref()));
+ merged.set$ref(firstNotBlank(primary.get$ref(), secondary.get$ref()));
merged.setExtensions(merge(primary.getExtensions(), secondary.getExtensions()));
return merged;
}
@@ -345,18 +411,27 @@ private static Schema merge(final Schema primary, final Schema secondary) {
return secondary;
} else {
// We will alter "primary" to match
- final String type = firstNotBlank(primary.getType(), secondary.getType());
+ final String type = preferredSchemaType(primary, secondary);
+ final Set types = mergedSchemaTypes(primary, secondary, type);
final String format = firstNotBlank(primary.getFormat(), secondary.getFormat());
- return SchemaCustomAdaptor.createSchemaFromInformation(type, format)
+ final var exclusiveMaximumValue = firstNonNull(
+ OpenApiSchemaNormalizer.exclusiveMaximumValue(primary),
+ OpenApiSchemaNormalizer.exclusiveMaximumValue(secondary));
+ final var exclusiveMinimumValue = firstNonNull(
+ OpenApiSchemaNormalizer.exclusiveMinimumValue(primary),
+ OpenApiSchemaNormalizer.exclusiveMinimumValue(secondary));
+ final var maximum = omitMaximum(primary, secondary) ? null : firstNonNull(primary.getMaximum(), secondary.getMaximum());
+ final var minimum = omitMinimum(primary, secondary) ? null : firstNonNull(primary.getMinimum(), secondary.getMinimum());
+ return OpenApiSchemaNormalizer.createSchemaFromInformation(type, format)
.type(type)
.format(format)
.name(firstNotBlank(primary.getName(), secondary.getName()))
.title(firstNotBlank(primary.getTitle(), secondary.getTitle()))
.multipleOf(firstNonNull(primary.getMultipleOf(), secondary.getMultipleOf()))
- .maximum(firstNonNull(primary.getMaximum(), secondary.getMaximum()))
- .exclusiveMaximum(firstNonNull(primary.getExclusiveMaximum(), secondary.getExclusiveMaximum()))
- .minimum(firstNonNull(primary.getMinimum(), secondary.getMinimum()))
- .exclusiveMaximum(firstNonNull(primary.getExclusiveMinimum(), secondary.getExclusiveMinimum()))
+ .maximum(maximum)
+ .exclusiveMaximum(null)
+ .minimum(minimum)
+ .exclusiveMinimum(null)
.maxLength(firstNonNull(primary.getMaxLength(), secondary.getMaxLength()))
.minLength(firstNonNull(primary.getMinLength(), secondary.getMinLength()))
.pattern(firstNotBlank(primary.getPattern(), secondary.getPattern()))
@@ -371,7 +446,7 @@ private static Schema merge(final Schema primary, final Schema secondary) {
.additionalProperties(firstNonNull(primary.getAdditionalProperties(), secondary.getAdditionalProperties()))
.description(firstNotBlank(primary.getDescription(), secondary.getDescription()))
.$ref(firstNotBlank(primary.get$ref(), secondary.get$ref()))
- .nullable(firstNonNull(primary.getNullable(), secondary.getNullable()))
+ .nullable(null)
.readOnly(firstNonNull(primary.getReadOnly(), secondary.getReadOnly()))
.writeOnly(firstNonNull(primary.getWriteOnly(), secondary.getWriteOnly()))
.externalDocs(merge(primary.getExternalDocs(), secondary.getExternalDocs()))
@@ -384,10 +459,10 @@ private static Schema merge(final Schema primary, final Schema secondary) {
.anyOf(merge(primary.getAnyOf(), secondary.getAnyOf()))
.oneOf(merge(primary.getOneOf(), secondary.getOneOf()))
.items(merge(primary.getItems(), secondary.getItems()))
- .types(firstNonNull(primary.getTypes(), secondary.getTypes()))
+ .types(types)
.patternProperties(merge(primary.getPatternProperties(), secondary.getPatternProperties(), OpenAPIMergerUtil::merge, Schema.class))
- .exclusiveMaximumValue(firstNonNull(primary.getExclusiveMaximumValue(), secondary.getExclusiveMaximumValue()))
- .exclusiveMinimumValue(firstNonNull(primary.getExclusiveMinimumValue(), secondary.getExclusiveMinimumValue()))
+ .exclusiveMaximumValue(exclusiveMaximumValue)
+ .exclusiveMinimumValue(exclusiveMinimumValue)
.contains(merge(primary.getContains(), secondary.getContains()))
.$id(firstNotBlank(primary.get$id(), secondary.get$id()))
.$schema(firstNotBlank(primary.get$schema(), secondary.get$schema()))
@@ -419,6 +494,20 @@ private static Schema merge(final Schema primary, final Schema secondary) {
}
}
+ private static boolean omitMaximum(final Schema primary, final Schema secondary) {
+ if (OpenApiSchemaNormalizer.omitMaximum(primary)) {
+ return true;
+ }
+ return primary.getMaximum() == null && OpenApiSchemaNormalizer.omitMaximum(secondary);
+ }
+
+ private static boolean omitMinimum(final Schema primary, final Schema secondary) {
+ if (OpenApiSchemaNormalizer.omitMinimum(primary)) {
+ return true;
+ }
+ return primary.getMinimum() == null && OpenApiSchemaNormalizer.omitMinimum(secondary);
+ }
+
private static Tag merge(final Tag primary, final Tag secondary) {
if (secondary == null) {
return primary;
diff --git a/openapi-maven-plugin/src/main/java/io/avaje/http/maven/openapi/jsonb/SchemaCustomAdaptor.java b/openapi-maven-plugin/src/main/java/io/avaje/http/maven/openapi/jsonb/SchemaCustomAdaptor.java
index 527725124..907ea5e99 100644
--- a/openapi-maven-plugin/src/main/java/io/avaje/http/maven/openapi/jsonb/SchemaCustomAdaptor.java
+++ b/openapi-maven-plugin/src/main/java/io/avaje/http/maven/openapi/jsonb/SchemaCustomAdaptor.java
@@ -7,6 +7,7 @@
import io.avaje.jsonb.CustomAdapter;
import io.avaje.jsonb.Jsonb;
import io.avaje.jsonb.Types;
+import io.avaje.http.openapi.OpenApiSchemaNormalizer;
import io.swagger.v3.oas.models.ExternalDocumentation;
import io.swagger.v3.oas.models.media.ArbitrarySchema;
import io.swagger.v3.oas.models.media.ArraySchema;
@@ -200,7 +201,16 @@ public Schema fromJson(JsonReader reader) {
final String fieldName = reader.nextField();
switch (fieldName) {
case "type":
- type = stringJsonAdapter.fromJson(reader);
+ switch (reader.currentToken()) {
+ case STRING:
+ type = stringJsonAdapter.fromJson(reader);
+ break;
+ case BEGIN_ARRAY:
+ types = setStringJsonAdaptor.fromJson(reader);
+ break;
+ default:
+ reader.skipValue();
+ }
break;
case "format":
format = stringJsonAdapter.fromJson(reader);
@@ -218,13 +228,31 @@ public Schema fromJson(JsonReader reader) {
maximum = bigDecimalJsonAdapter.fromJson(reader);
break;
case "exclusiveMaximum":
- exclusiveMaximum = booleanJsonAdapter.fromJson(reader);
+ switch (reader.currentToken()) {
+ case NUMBER:
+ exclusiveMaximumValue = bigDecimalJsonAdapter.fromJson(reader);
+ break;
+ case BOOLEAN:
+ exclusiveMaximum = booleanJsonAdapter.fromJson(reader);
+ break;
+ default:
+ reader.skipValue();
+ }
break;
case "minimum":
minimum = bigDecimalJsonAdapter.fromJson(reader);
break;
case "exclusiveMinimum":
- exclusiveMinimum = booleanJsonAdapter.fromJson(reader);
+ switch (reader.currentToken()) {
+ case NUMBER:
+ exclusiveMinimumValue = bigDecimalJsonAdapter.fromJson(reader);
+ break;
+ case BOOLEAN:
+ exclusiveMinimum = booleanJsonAdapter.fromJson(reader);
+ break;
+ default:
+ reader.skipValue();
+ }
break;
case "maxLength":
maxLength = integerJsonAdapter.fromJson(reader);
@@ -421,7 +449,9 @@ public Schema fromJson(JsonReader reader) {
}
}
reader.endObject();
- final Schema schema = createSchemaFromInformation(type, format);
+ final Set normalizedTypes = OpenApiSchemaNormalizer.normalizeTypes(type, types, nullable);
+ final Schema schema = (Schema) OpenApiSchemaNormalizer.createSchemaFromInformation(
+ OpenApiSchemaNormalizer.schemaTypeForCreation(type, normalizedTypes), format);
schema.name(name)
.title(title)
.multipleOf(multipleOf)
@@ -443,7 +473,7 @@ public Schema fromJson(JsonReader reader) {
.additionalProperties(additionalProperties)
.description(description)
.$ref($ref)
- .nullable(nullable)
+ .nullable(null)
.readOnly(readOnly)
.writeOnly(writeOnly)
.externalDocs(externalDocs)
@@ -456,7 +486,7 @@ public Schema fromJson(JsonReader reader) {
.anyOf(anyOf)
.oneOf(oneOf)
.items(items)
- .types(types)
+ .types(normalizedTypes)
.patternProperties(patternProperties)
.exclusiveMaximumValue(exclusiveMaximumValue)
.exclusiveMinimumValue(exclusiveMinimumValue)
@@ -492,45 +522,7 @@ public Schema fromJson(JsonReader reader) {
}
public static Schema createSchemaFromInformation(final String type, final String format) {
- if (type == null) {
- return new ArbitrarySchema();
- }
- switch (type) {
- case "array":
- return new ArraySchema().format(format);
- case "boolean":
- return new BooleanSchema().format(format);
- case "integer":
- return new IntegerSchema().format(format);
- case "object":
- return new ObjectSchema().format(format);
- case "number":
- return new NumberSchema().format(format);
- case "string":
- if (format == null) {
- return new StringSchema();
- }
- switch (format) {
- case "binary":
- return new BinarySchema();
- case "byte":
- return new ByteArraySchema();
- case "date":
- return new DateSchema();
- case "date-time":
- return new DateTimeSchema();
- case "email":
- return new EmailSchema();
- case "password":
- return new PasswordSchema();
- case "uuid":
- return new UUIDSchema();
- default:
- return new StringSchema().format(format);
- }
- default:
- return new ArbitrarySchema().format(format).type(type);
- }
+ return (Schema) OpenApiSchemaNormalizer.createSchemaFromInformation(type, format);
}
private Schema readSelf(JsonReader reader) {
@@ -577,7 +569,15 @@ private List readListSelf(JsonReader reader) {
private void toJsonImpl(JsonWriter writer, Schema> value) {
writer.name(0);
- stringJsonAdapter.toJson(writer, value.getType());
+ final Set serializedTypes = OpenApiSchemaNormalizer.normalizeTypes(value.getType(), value.getTypes(), value.getNullable());
+ final String scalarType = OpenApiSchemaNormalizer.scalarSchemaType(serializedTypes);
+ if (scalarType != null) {
+ stringJsonAdapter.toJson(writer, scalarType);
+ } else if (serializedTypes != null && !serializedTypes.isEmpty()) {
+ setStringJsonAdaptor.toJson(writer, serializedTypes);
+ } else {
+ stringJsonAdapter.toJson(writer, value.getType());
+ }
writer.name(1);
stringJsonAdapter.toJson(writer, value.getFormat());
writer.name(2);
@@ -587,13 +587,21 @@ private void toJsonImpl(JsonWriter writer, Schema> value) {
writer.name(4);
bigDecimalJsonAdapter.toJson(writer, value.getMultipleOf());
writer.name(5);
- bigDecimalJsonAdapter.toJson(writer, value.getMaximum());
+ if (OpenApiSchemaNormalizer.omitMaximum(value)) {
+ writer.nullValue();
+ } else {
+ bigDecimalJsonAdapter.toJson(writer, value.getMaximum());
+ }
writer.name(6);
- booleanJsonAdapter.toJson(writer, value.getExclusiveMaximum());
+ bigDecimalJsonAdapter.toJson(writer, OpenApiSchemaNormalizer.exclusiveMaximumValue(value));
writer.name(7);
- bigDecimalJsonAdapter.toJson(writer, value.getMinimum());
+ if (OpenApiSchemaNormalizer.omitMinimum(value)) {
+ writer.nullValue();
+ } else {
+ bigDecimalJsonAdapter.toJson(writer, value.getMinimum());
+ }
writer.name(8);
- booleanJsonAdapter.toJson(writer, value.getExclusiveMinimum());
+ bigDecimalJsonAdapter.toJson(writer, OpenApiSchemaNormalizer.exclusiveMinimumValue(value));
writer.name(9);
integerJsonAdapter.toJson(writer, value.getMaxLength());
writer.name(10);
@@ -631,7 +639,7 @@ private void toJsonImpl(JsonWriter writer, Schema> value) {
writer.name(22);
stringJsonAdapter.toJson(writer, value.get$ref());
writer.name(23);
- booleanJsonAdapter.toJson(writer, value.getNullable());
+ writer.nullValue();
writer.name(24);
booleanJsonAdapter.toJson(writer, value.getReadOnly());
writer.name(25);
@@ -666,13 +674,13 @@ private void toJsonImpl(JsonWriter writer, Schema> value) {
// specVersion - ignored
writer.nullValue();
writer.name(38);
- setStringJsonAdaptor.toJson(writer, value.getTypes());
+ writer.nullValue();
writer.name(39);
writeMapSelf(value.getPatternProperties(), writer);
writer.name(40);
- bigDecimalJsonAdapter.toJson(writer, value.getExclusiveMaximumValue());
+ writer.nullValue();
writer.name(41);
- bigDecimalJsonAdapter.toJson(writer, value.getExclusiveMinimumValue());
+ writer.nullValue();
writer.name(42);
writeSelfNullSafe(value.getContains(), writer);
writer.name(43);
diff --git a/openapi-maven-plugin/src/test/java/io/avaje/http/maven/openapi/OpenAPIMergerUtilTest.java b/openapi-maven-plugin/src/test/java/io/avaje/http/maven/openapi/OpenAPIMergerUtilTest.java
new file mode 100644
index 000000000..888c47bb4
--- /dev/null
+++ b/openapi-maven-plugin/src/test/java/io/avaje/http/maven/openapi/OpenAPIMergerUtilTest.java
@@ -0,0 +1,119 @@
+package io.avaje.http.maven.openapi;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+import io.swagger.v3.oas.models.Components;
+import io.swagger.v3.oas.models.OpenAPI;
+import io.swagger.v3.oas.models.media.NumberSchema;
+import io.swagger.v3.oas.models.media.Schema;
+import io.swagger.v3.oas.models.media.StringSchema;
+import java.math.BigDecimal;
+import java.util.LinkedHashSet;
+import java.util.List;
+import org.junit.jupiter.api.Test;
+
+class OpenAPIMergerUtilTest {
+
+ @Test
+ void mergeSchemaConvertsNullableToTypeUnion() {
+ final Schema> primarySchema = new StringSchema().nullable(true);
+ final Schema> secondarySchema = new StringSchema();
+
+ final OpenAPI primary =
+ new OpenAPI()
+ .openapi("3.1.2")
+ .components(new Components().addSchemas("Thing", primarySchema));
+ final OpenAPI secondary =
+ new OpenAPI()
+ .openapi("3.1.2")
+ .components(new Components().addSchemas("Thing", secondarySchema));
+
+ final OpenAPI merged = OpenAPIMergerUtil.merge(primary, secondary);
+ final Schema> schema = merged.getComponents().getSchemas().get("Thing");
+
+ assertThat(schema.getNullable()).isNull();
+ assertThat(schema.getTypes()).containsExactly("string", "null");
+ }
+
+ @Test
+ void mergeSchemaKeepsPrimaryNullabilityWhenSecondaryNullableFalse() {
+ final Schema> primarySchema =
+ new StringSchema().types(new LinkedHashSet<>(List.of("string", "null")));
+ final Schema> secondarySchema = new StringSchema().nullable(false);
+
+ final OpenAPI primary =
+ new OpenAPI()
+ .openapi("3.1.2")
+ .components(new Components().addSchemas("Thing", primarySchema));
+ final OpenAPI secondary =
+ new OpenAPI()
+ .openapi("3.1.2")
+ .components(new Components().addSchemas("Thing", secondarySchema));
+
+ final OpenAPI merged = OpenAPIMergerUtil.merge(primary, secondary);
+ final Schema> schema = merged.getComponents().getSchemas().get("Thing");
+
+ assertThat(schema.getNullable()).isNull();
+ assertThat(schema.getTypes()).containsExactly("string", "null");
+ }
+
+ @Test
+ void mergeSchemaUsesSecondaryNullabilityWhenPrimaryHasNoTypeInformation() {
+ final Schema> primarySchema = new Schema<>();
+ final Schema> secondarySchema = new StringSchema().nullable(false);
+
+ final OpenAPI primary =
+ new OpenAPI()
+ .openapi("3.1.2")
+ .components(new Components().addSchemas("Thing", primarySchema));
+ final OpenAPI secondary =
+ new OpenAPI()
+ .openapi("3.1.2")
+ .components(new Components().addSchemas("Thing", secondarySchema));
+
+ final OpenAPI merged = OpenAPIMergerUtil.merge(primary, secondary);
+ final Schema> schema = merged.getComponents().getSchemas().get("Thing");
+
+ assertThat(schema.getNullable()).isNull();
+ assertThat(schema.getTypes()).containsExactly("string");
+ }
+
+ @Test
+ void mergeOpenApiVersionPrefersHighestMinor() {
+ final var primary =
+ new OpenAPI().openapi("3.1.2");
+ final var secondary =
+ new OpenAPI().openapi("3.2.0");
+
+ final OpenAPI merged = OpenAPIMergerUtil.merge(primary, secondary);
+
+ assertThat(merged.getOpenapi()).isEqualTo("3.2.0");
+ }
+
+ @Test
+ void mergeSchemaConvertsExclusiveBoundsToNumericBounds() {
+ final Schema> primarySchema =
+ new NumberSchema().minimum(new BigDecimal("1.5")).exclusiveMinimum(true);
+ final Schema> secondarySchema =
+ new NumberSchema().maximum(new BigDecimal("9.5")).exclusiveMaximum(true);
+
+ final OpenAPI primary =
+ new OpenAPI()
+ .openapi("3.1.2")
+ .components(new Components().addSchemas("Thing", primarySchema));
+ final OpenAPI secondary =
+ new OpenAPI()
+ .openapi("3.1.2")
+ .components(new Components().addSchemas("Thing", secondarySchema));
+
+ final OpenAPI merged = OpenAPIMergerUtil.merge(primary, secondary);
+ final Schema> schema = merged.getComponents().getSchemas().get("Thing");
+
+ assertThat(schema.getMinimum()).isNull();
+ assertThat(schema.getMaximum()).isNull();
+ assertThat(schema.getExclusiveMinimum()).isNull();
+ assertThat(schema.getExclusiveMaximum()).isNull();
+ assertThat(schema.getExclusiveMinimumValue()).isEqualByComparingTo("1.5");
+ assertThat(schema.getExclusiveMaximumValue()).isEqualByComparingTo("9.5");
+ }
+}
diff --git a/openapi-maven-plugin/src/test/java/io/avaje/http/maven/openapi/jsonb/SchemaCustomAdaptorTest.java b/openapi-maven-plugin/src/test/java/io/avaje/http/maven/openapi/jsonb/SchemaCustomAdaptorTest.java
new file mode 100644
index 000000000..661ab863a
--- /dev/null
+++ b/openapi-maven-plugin/src/test/java/io/avaje/http/maven/openapi/jsonb/SchemaCustomAdaptorTest.java
@@ -0,0 +1,120 @@
+package io.avaje.http.maven.openapi.jsonb;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+import io.avaje.jsonb.JsonType;
+import io.avaje.jsonb.Jsonb;
+import io.swagger.v3.oas.models.media.Schema;
+import org.junit.jupiter.api.Test;
+
+class SchemaCustomAdaptorTest {
+
+ @Test
+ void parseAndWriteTypeArray() {
+ final Jsonb jsonb =
+ Jsonb.builder().serializeEmpty(true).serializeNulls(false).failOnUnknown(false).build();
+ final JsonType schemaType = jsonb.type(Schema.class);
+
+ final Schema schema = schemaType.fromJson("{\"type\":[\"string\",\"null\"]}");
+
+ assertThat(schema.getType()).isEqualTo("string");
+ assertThat(schema.getTypes()).isNotNull();
+ assertThat(schema.getTypes()).contains("string", "null");
+
+ final String json = schemaType.toJson(schema);
+ assertThat(json).contains("\"type\"");
+ assertThat(json).doesNotContain("\"types\"");
+ }
+
+ @Test
+ void parseLegacyTypesAliasAndWriteAsType() {
+ final Jsonb jsonb =
+ Jsonb.builder().serializeEmpty(true).serializeNulls(false).failOnUnknown(false).build();
+ final JsonType schemaType = jsonb.type(Schema.class);
+
+ final Schema schema = schemaType.fromJson("{\"types\":[\"integer\",\"null\"]}");
+
+ assertThat(schema.getType()).isEqualTo("integer");
+ assertThat(schema.getTypes()).isNotNull();
+ assertThat(schema.getTypes()).contains("integer", "null");
+
+ final String json = schemaType.toJson(schema);
+ assertThat(json).contains("\"type\"");
+ assertThat(json).doesNotContain("\"types\"");
+ }
+
+ @Test
+ void parseNullableTrueWritesTypeUnion() {
+ final Jsonb jsonb =
+ Jsonb.builder().serializeEmpty(true).serializeNulls(false).failOnUnknown(false).build();
+ final JsonType schemaType = jsonb.type(Schema.class);
+
+ final Schema schema = schemaType.fromJson("{\"type\":\"string\",\"nullable\":true}");
+
+ assertThat(schema.getNullable()).isNull();
+ assertThat(schema.getTypes()).containsExactly("string", "null");
+
+ final String json = schemaType.toJson(schema);
+ assertThat(json).contains("\"type\":[\"string\",\"null\"]");
+ assertThat(json).doesNotContain("\"nullable\"");
+ }
+
+ @Test
+ void parseNullableFalseRemovesNullFromTypeUnion() {
+ final Jsonb jsonb =
+ Jsonb.builder().serializeEmpty(true).serializeNulls(false).failOnUnknown(false).build();
+ final JsonType schemaType = jsonb.type(Schema.class);
+
+ final Schema schema =
+ schemaType.fromJson("{\"type\":[\"string\",\"null\"],\"nullable\":false}");
+
+ assertThat(schema.getNullable()).isNull();
+ assertThat(schema.getTypes()).containsExactly("string");
+
+ final String json = schemaType.toJson(schema);
+ assertThat(json).contains("\"type\":\"string\"");
+ assertThat(json).doesNotContain("\"nullable\"");
+ }
+
+ @Test
+ void parseAndWriteNumericExclusiveBounds() {
+ final Jsonb jsonb =
+ Jsonb.builder().serializeEmpty(true).serializeNulls(false).failOnUnknown(false).build();
+ final JsonType schemaType = jsonb.type(Schema.class);
+
+ final Schema schema =
+ schemaType.fromJson("{\"type\":\"number\",\"exclusiveMinimum\":1.5,\"exclusiveMaximum\":9.5}");
+
+ assertThat(schema.getExclusiveMinimum()).isNull();
+ assertThat(schema.getExclusiveMaximum()).isNull();
+ assertThat(schema.getExclusiveMinimumValue()).isEqualByComparingTo("1.5");
+ assertThat(schema.getExclusiveMaximumValue()).isEqualByComparingTo("9.5");
+
+ final String json = schemaType.toJson(schema);
+ assertThat(json).contains("\"exclusiveMinimum\":1.5");
+ assertThat(json).contains("\"exclusiveMaximum\":9.5");
+ assertThat(json).doesNotContain("\"exclusiveMinimumValue\"");
+ assertThat(json).doesNotContain("\"exclusiveMaximumValue\"");
+ }
+
+ @Test
+ void parseLegacyBooleanExclusiveBoundsWritesNumericBounds() {
+ final Jsonb jsonb =
+ Jsonb.builder().serializeEmpty(true).serializeNulls(false).failOnUnknown(false).build();
+ final JsonType schemaType = jsonb.type(Schema.class);
+
+ final Schema schema =
+ schemaType.fromJson("{\"type\":\"integer\",\"minimum\":5,\"exclusiveMinimum\":true,\"maximum\":10,\"exclusiveMaximum\":true}");
+
+ assertThat(schema.getExclusiveMinimum()).isTrue();
+ assertThat(schema.getExclusiveMaximum()).isTrue();
+
+ final String json = schemaType.toJson(schema);
+ assertThat(json).contains("\"exclusiveMinimum\":5");
+ assertThat(json).contains("\"exclusiveMaximum\":10");
+ assertThat(json).doesNotContain("\"minimum\":5");
+ assertThat(json).doesNotContain("\"maximum\":10");
+ assertThat(json).doesNotContain("\"exclusiveMinimumValue\"");
+ assertThat(json).doesNotContain("\"exclusiveMaximumValue\"");
+ }
+}
diff --git a/pom.xml b/pom.xml
index 1b4b1c0d4..e1a7c725b 100644
--- a/pom.xml
+++ b/pom.xml
@@ -38,6 +38,7 @@
+ openapi-core
openapi-maven-plugin
htmx-api
http-api
diff --git a/tests/test-javalin-jsonb/io.avaje.jsonb.spi.JsonbExtension b/tests/test-javalin-jsonb/io.avaje.jsonb.spi.JsonbExtension
index 05a41ae93..60abdf8d0 100644
--- a/tests/test-javalin-jsonb/io.avaje.jsonb.spi.JsonbExtension
+++ b/tests/test-javalin-jsonb/io.avaje.jsonb.spi.JsonbExtension
@@ -1 +1,2 @@
-org.example.myapp.web.jsonb.GeneratedJsonComponent
\ No newline at end of file
+org.example.myapp.web.jsonb.GeneratedJsonComponent
+org.example.myapp.web.test.TestJsonComponent
\ No newline at end of file
diff --git a/tests/test-javalin-jsonb/src/main/resources/moar-openapi.json b/tests/test-javalin-jsonb/src/main/resources/moar-openapi.json
index 2b488aac5..8bee34196 100644
--- a/tests/test-javalin-jsonb/src/main/resources/moar-openapi.json
+++ b/tests/test-javalin-jsonb/src/main/resources/moar-openapi.json
@@ -8,7 +8,6 @@
"id": {
"type": "integer",
"format": "int64",
- "nullable": false,
"description": "This is a number ... I think?"
},
"name": {
diff --git a/tests/test-javalin-jsonb/src/main/resources/public/openapi.json b/tests/test-javalin-jsonb/src/main/resources/public/openapi.json
index ba5bc7996..06b8f2a84 100644
--- a/tests/test-javalin-jsonb/src/main/resources/public/openapi.json
+++ b/tests/test-javalin-jsonb/src/main/resources/public/openapi.json
@@ -1,5 +1,5 @@
{
- "openapi": "3.0.1",
+ "openapi": "3.1.2",
"info": {
"title": "Example service",
"description": "Example Javalin controllers with Java and Maven",
@@ -66,8 +66,7 @@
"schema": {
"type": "array",
"items": {
- "$ref": "#/components/schemas/Bar",
- "nullable": false
+ "$ref": "#/components/schemas/Bar"
}
},
"exampleSetFlag": false
@@ -89,8 +88,7 @@
"required": true,
"schema": {
"type": "integer",
- "format": "int64",
- "nullable": false
+ "format": "int64"
}
}
],
@@ -123,8 +121,7 @@
"schema": {
"type": "array",
"items": {
- "$ref": "#/components/schemas/Baz",
- "nullable": false
+ "$ref": "#/components/schemas/Baz"
}
},
"exampleSetFlag": false
@@ -177,8 +174,7 @@
"required": true,
"schema": {
"type": "integer",
- "format": "int32",
- "nullable": false
+ "format": "int32"
}
},{
"name": "p1",
@@ -251,8 +247,7 @@
"schema": {
"type": "array",
"items": {
- "$ref": "#/components/schemas/Baz",
- "nullable": false
+ "$ref": "#/components/schemas/Baz"
}
},
"exampleSetFlag": false
@@ -360,8 +355,7 @@
"schema": {
"type": "array",
"items": {
- "$ref": "#/components/schemas/HelloDto",
- "nullable": false
+ "$ref": "#/components/schemas/HelloDto"
}
},
"exampleSetFlag": false
@@ -422,8 +416,7 @@
"schema": {
"type": "array",
"items": {
- "$ref": "#/components/schemas/HelloDto",
- "nullable": false
+ "$ref": "#/components/schemas/HelloDto"
}
},
"exampleSetFlag": false
@@ -537,8 +530,7 @@
"type": "object",
"properties": {
"name": {
- "type": "string",
- "nullable": false
+ "type": "string"
},
"email": {
"type": "string"
@@ -612,8 +604,7 @@
"type": "object",
"properties": {
"name": {
- "type": "string",
- "nullable": false
+ "type": "string"
},
"email": {
"type": "string"
@@ -755,8 +746,7 @@
"required": true,
"schema": {
"type": "integer",
- "format": "int32",
- "nullable": false
+ "format": "int32"
}
},{
"name": "author",
@@ -812,8 +802,7 @@
"name": "name",
"in": "query",
"schema": {
- "type": "string",
- "nullable": false
+ "type": "string"
}
},{
"name": "email",
@@ -827,8 +816,7 @@
"schema": {
"type": "array",
"items": {
- "type": "string",
- "nullable": false
+ "type": "string"
}
}
},{
@@ -844,7 +832,6 @@
"type": "array",
"items": {
"type": "string",
- "nullable": false,
"enum": ["PROXY","HIDE_N_SEEK","FFA"
]
}
@@ -878,8 +865,7 @@
"required": true,
"schema": {
"type": "integer",
- "format": "int32",
- "nullable": false
+ "format": "int32"
}
}
],
@@ -903,8 +889,7 @@
"required": true,
"schema": {
"type": "integer",
- "format": "int32",
- "nullable": false
+ "format": "int32"
}
},{
"name": "date",
@@ -1032,10 +1017,8 @@
"type": "object",
"additionalProperties": {
"type": "array",
- "nullable": false,
"items": {
- "type": "string",
- "nullable": false
+ "type": "string"
}
}
},
@@ -1264,8 +1247,7 @@
"schema": {
"type": "array",
"items": {
- "$ref": "#/components/schemas/Person",
- "nullable": false
+ "$ref": "#/components/schemas/Person"
}
},
"exampleSetFlag": false
@@ -1670,7 +1652,6 @@
"type": "array",
"items": {
"type": "string",
- "nullable": false,
"enum": ["PROXY","HIDE_N_SEEK","FFA"
]
}
@@ -1859,8 +1840,7 @@
"strings": {
"type": "array",
"items": {
- "type": "string",
- "nullable": false
+ "type": "string"
}
}
}
@@ -1981,8 +1961,7 @@
"application/json": {
"schema": {
"type": "integer",
- "format": "int32",
- "nullable": false
+ "format": "int32"
},
"exampleSetFlag": false
}
@@ -2004,8 +1983,7 @@
"application/json": {
"schema": {
"type": "integer",
- "format": "int64",
- "nullable": false
+ "format": "int64"
},
"exampleSetFlag": false
}
@@ -2079,8 +2057,7 @@
"schema": {
"type": "array",
"items": {
- "$ref": "#/components/schemas/Person",
- "nullable": false
+ "$ref": "#/components/schemas/Person"
}
},
"exampleSetFlag": false
@@ -2156,8 +2133,7 @@
"schema": {
"type": "array",
"items": {
- "$ref": "#/components/schemas/Person",
- "nullable": false
+ "$ref": "#/components/schemas/Person"
}
},
"exampleSetFlag": false
@@ -2190,8 +2166,7 @@
"schema": {
"type": "object",
"additionalProperties": {
- "$ref": "#/components/schemas/Person",
- "nullable": false
+ "$ref": "#/components/schemas/Person"
}
},
"exampleSetFlag": false
@@ -2224,8 +2199,7 @@
"schema": {
"type": "array",
"items": {
- "$ref": "#/components/schemas/Person",
- "nullable": false
+ "$ref": "#/components/schemas/Person"
}
},
"exampleSetFlag": false
@@ -2330,8 +2304,7 @@
"id": {
"type": "integer",
"format": "int64",
- "description": "This is a number ... I think?",
- "nullable": false
+ "description": "This is a number ... I think?"
}
},
"description": "But oh, what a description this schema has!"
@@ -2363,8 +2336,7 @@
},
"id": {
"type": "integer",
- "format": "int32",
- "nullable": false
+ "format": "int32"
}
}
},
@@ -2398,8 +2370,7 @@
},
"id": {
"type": "integer",
- "format": "int64",
- "nullable": false
+ "format": "int64"
}
}
},
@@ -2409,8 +2380,7 @@
"data": {
"type": "array",
"items": {
- "$ref": "#/components/schemas/Bar",
- "nullable": false
+ "$ref": "#/components/schemas/Bar"
}
}
}
@@ -2436,5 +2406,6 @@
}
}
},
+ "jsonSchemaDialect": "https://spec.openapis.org/oas/3.1/dialect/base",
"specVersion": "V30"
}
\ No newline at end of file
diff --git a/tests/test-javalin-jsonb/src/test/java/io/avaje/http/generator/JavalinProcessorTest.java b/tests/test-javalin-jsonb/src/test/java/io/avaje/http/generator/JavalinProcessorTest.java
index d181ff8fe..4d249d98b 100644
--- a/tests/test-javalin-jsonb/src/test/java/io/avaje/http/generator/JavalinProcessorTest.java
+++ b/tests/test-javalin-jsonb/src/test/java/io/avaje/http/generator/JavalinProcessorTest.java
@@ -172,10 +172,9 @@ public void testOpenAPIGeneration() throws Exception {
final var mapper = new ObjectMapper();
final var expectedOpenApiJson =
mapper.readTree(new File("src/test/resources/expectedOpenApi.json"));
- File file = new File("openapi.json");
- // Files.copy(file.toPath(), Paths.get("other.json"));
- final var generatedOpenApi = mapper.readTree(file);
+ final var generatedOpenApi = mapper.readTree(new File("openapi.json"));
+ assertOpenApi31(generatedOpenApi.toString());
assertThat(generatedOpenApi).isEqualTo(expectedOpenApiJson);
}
@@ -210,7 +209,8 @@ public void testInheritableOpenAPIGeneration() throws Exception {
mapper.readTree(new File("src/test/resources/expectedInheritedOpenApi.json"));
final var generatedOpenApi = mapper.readTree(new File("openapi.json"));
- assert expectedOpenApiJson.equals(generatedOpenApi);
+ assertOpenApi31(generatedOpenApi.toString());
+ assertThat(generatedOpenApi).isEqualTo(expectedOpenApiJson);
}
private Iterable getSourceFiles(String source) throws Exception {
@@ -222,4 +222,11 @@ private Iterable getSourceFiles(String source) throws Exception
final Set fileKinds = Collections.singleton(Kind.SOURCE);
return files.list(StandardLocation.SOURCE_PATH, "", fileKinds, true);
}
+
+ private static void assertOpenApi31(String json) {
+ assertThat(json).contains("\"openapi\":\"3.1.2\"");
+ assertThat(json).contains("\"jsonSchemaDialect\":\"https://spec.openapis.org/oas/3.1/dialect/base\"");
+ assertThat(json).doesNotContain("\"nullable\"");
+ assertThat(json).doesNotContain("\"types\":");
+ }
}
diff --git a/tests/test-javalin-jsonb/src/test/resources/expectedInheritedOpenApi.json b/tests/test-javalin-jsonb/src/test/resources/expectedInheritedOpenApi.json
index a0e6983f6..d0433be5a 100644
--- a/tests/test-javalin-jsonb/src/test/resources/expectedInheritedOpenApi.json
+++ b/tests/test-javalin-jsonb/src/test/resources/expectedInheritedOpenApi.json
@@ -1,5 +1,5 @@
{
- "openapi" : "3.0.1",
+ "openapi" : "3.1.2",
"info" : {
"title" : "Example service showing off the Path extension method of controller",
"version" : ""
@@ -66,5 +66,6 @@
}
}
}
- }
+ },
+ "jsonSchemaDialect" : "https://spec.openapis.org/oas/3.1/dialect/base"
}
diff --git a/tests/test-javalin-jsonb/src/test/resources/expectedOpenApi.json b/tests/test-javalin-jsonb/src/test/resources/expectedOpenApi.json
index 721210eaf..ace3a1468 100644
--- a/tests/test-javalin-jsonb/src/test/resources/expectedOpenApi.json
+++ b/tests/test-javalin-jsonb/src/test/resources/expectedOpenApi.json
@@ -1,315 +1,314 @@
{
- "openapi": "3.0.1",
- "info": {
- "title": "Example service",
- "description": "Example Javalin controllers with Java and Maven",
- "version": ""
- },
- "servers": [
- {
- "url": "localhost:8080",
- "description": "local testing"
- }
- ],
- "tags": [
- {
- "name": "tag1",
- "description": "this is added to openapi tags"
- }
- ],
- "paths": {
- "/openapi/delete/{type}": {
- "delete": {
- "tags": [],
- "summary": "",
- "description": "",
- "parameters": [
- {
- "name": "type",
- "in": "path",
- "required": true,
- "schema": {
- "type": "string"
- }
- },
- {
- "name": "lastName",
- "in": "query",
- "schema": {
- "type": "string"
- }
- },
- {
- "name": "q-2",
- "in": "query",
- "schema": {
- "type": "string"
- }
- },
- {
- "name": "Content-Length",
- "in": "header",
- "schema": {
- "type": "string"
- }
- },
- {
- "name": "x-oh",
- "in": "header",
- "schema": {
- "type": "string"
- }
- }
- ],
- "responses": {
- "200": {
- "description": "",
- "content": {
- "application/json": {
- "schema": {
- "type": "string"
- }
- }
- }
- }
- }
- }
- },
- "/openapi/form": {
- "post": {
- "tags": [],
- "summary": "",
- "description": "",
- "parameters": [
- {
- "name": "Head-String",
- "in": "header",
- "schema": {
- "type": "string"
- }
- }
- ],
- "requestBody": {
- "content": {
- "application/x-www-form-urlencoded": {
- "schema": {
- "type": "object",
- "properties": {
- "name": {
- "type": "string"
- },
- "email": {
- "type": "string"
- },
- "url": {
- "type": "string"
- }
- }
- }
- }
- },
- "required": true
- },
- "responses": {
- "201": {
- "description": "",
- "content": {
- "application/json": {
- "schema": {
- "type": "string"
- }
- }
- }
- }
- }
- }
- },
- "/openapi/get": {
- "get": {
- "tags": [],
- "summary": "Example of Open API Get (up to the first period is the summary)",
- "description": "When using Javalin Context only This Javadoc description is added to the generated openapi.json",
- "responses": {
- "200": {
- "description": "funny phrase (this part of the javadoc is added to the response desc)",
- "content": {
- "text/plain": {
- "schema": {
- "type": "string"
- }
- }
- }
- }
- }
- }
- },
- "/openapi/post": {
- "post": {
- "tags": [
- "tag1"
- ],
- "summary": "Standard Post",
- "description": "uses tag annotation to add tags to openapi json",
- "requestBody": {
- "description": "the body (this is used for generated request body desc)",
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/Person"
- }
- }
- },
- "required": true
- },
- "responses": {
- "200": {
- "description": "overrides @return javadoc description",
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/Person"
- }
- }
- }
- },
- "201": {
- "description": "the response body (from javadoc)",
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/Person"
- }
- }
- }
- },
- "400": {
- "description": "User not found (Will not have an associated response schema)"
- },
- "500": {
- "description": "Some other Error (Will have this error class as the response class)",
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/ErrorResponse"
- }
- }
- }
- }
- }
- }
- },
- "/openapi/post1": {
- "post": {
- "tags": [],
- "summary": "Standard Post",
- "description": "The Deprecated annotation adds \"deprecacted:true\" to the generated json",
- "requestBody": {
- "description": "the body",
- "content": {
- "application/json": {
- "schema": {
- "type": "array",
- "items": {
- "$ref": "#/components/schemas/Person",
- "nullable": false
- }
- }
- }
- },
- "required": true
- },
- "responses": {
- "400": {
- "description": "User not found"
- },
- "500": {
- "description": "Some other Error",
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/ErrorResponse"
- }
- }
- }
- },
- "201": {
- "description": "the response body (from javadoc)",
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/Person"
- }
- }
- }
- }
- },
- "deprecated": true
- }
- },
- "/openapi/put": {
- "put": {
- "tags": [],
- "summary": "",
- "description": "",
- "responses": {
- "204": {
- "description": "",
- "content": {
- "text/plain": {
- "schema": {
- "type": "string"
- }
- }
- }
- },
- "203": {
- "description": "",
- "content": {
- "text/plain": {
- "schema": {
- "type": "string"
- }
- }
- }
- }
- }
- }
- }
- },
- "components": {
- "schemas": {
- "ErrorResponse": {
- "type": "object",
- "properties": {
- "id": {
- "type": "string"
- },
- "text": {
- "type": "string"
- }
- }
- },
- "Person": {
- "type": "object",
- "properties": {
- "id": {
- "type": "integer",
- "format": "int64",
- "nullable": false
- },
- "name": {
- "type": "string"
- }
- }
- }
- },
- "securitySchemes": {
- "JWT": {
- "type": "apiKey",
- "description": "JSON Web Tokens are an open, industry standard RFC 7519 method for representing claims securely between two parties.",
- "name": "access_token",
- "in": "query"
- }
- }
- }
-}
\ No newline at end of file
+ "openapi": "3.1.2",
+ "info": {
+ "title": "Example service",
+ "description": "Example Javalin controllers with Java and Maven",
+ "version": ""
+ },
+ "servers": [
+ {
+ "url": "localhost:8080",
+ "description": "local testing"
+ }
+ ],
+ "tags": [
+ {
+ "name": "tag1",
+ "description": "this is added to openapi tags"
+ }
+ ],
+ "paths": {
+ "/openapi/delete/{type}": {
+ "delete": {
+ "tags": [],
+ "summary": "",
+ "description": "",
+ "parameters": [
+ {
+ "name": "type",
+ "in": "path",
+ "required": true,
+ "schema": {
+ "type": "string"
+ }
+ },
+ {
+ "name": "lastName",
+ "in": "query",
+ "schema": {
+ "type": "string"
+ }
+ },
+ {
+ "name": "q-2",
+ "in": "query",
+ "schema": {
+ "type": "string"
+ }
+ },
+ {
+ "name": "Content-Length",
+ "in": "header",
+ "schema": {
+ "type": "string"
+ }
+ },
+ {
+ "name": "x-oh",
+ "in": "header",
+ "schema": {
+ "type": "string"
+ }
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "",
+ "content": {
+ "application/json": {
+ "schema": {
+ "type": "string"
+ }
+ }
+ }
+ }
+ }
+ }
+ },
+ "/openapi/form": {
+ "post": {
+ "tags": [],
+ "summary": "",
+ "description": "",
+ "parameters": [
+ {
+ "name": "Head-String",
+ "in": "header",
+ "schema": {
+ "type": "string"
+ }
+ }
+ ],
+ "requestBody": {
+ "content": {
+ "application/x-www-form-urlencoded": {
+ "schema": {
+ "type": "object",
+ "properties": {
+ "name": {
+ "type": "string"
+ },
+ "email": {
+ "type": "string"
+ },
+ "url": {
+ "type": "string"
+ }
+ }
+ }
+ }
+ },
+ "required": true
+ },
+ "responses": {
+ "201": {
+ "description": "",
+ "content": {
+ "application/json": {
+ "schema": {
+ "type": "string"
+ }
+ }
+ }
+ }
+ }
+ }
+ },
+ "/openapi/get": {
+ "get": {
+ "tags": [],
+ "summary": "Example of Open API Get (up to the first period is the summary)",
+ "description": "When using Javalin Context only This Javadoc description is added to the generated openapi.json",
+ "responses": {
+ "200": {
+ "description": "funny phrase (this part of the javadoc is added to the response desc)",
+ "content": {
+ "text/plain": {
+ "schema": {
+ "type": "string"
+ }
+ }
+ }
+ }
+ }
+ }
+ },
+ "/openapi/post": {
+ "post": {
+ "tags": [
+ "tag1"
+ ],
+ "summary": "Standard Post",
+ "description": "uses tag annotation to add tags to openapi json",
+ "requestBody": {
+ "description": "the body (this is used for generated request body desc)",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/Person"
+ }
+ }
+ },
+ "required": true
+ },
+ "responses": {
+ "200": {
+ "description": "overrides @return javadoc description",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/Person"
+ }
+ }
+ }
+ },
+ "201": {
+ "description": "the response body (from javadoc)",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/Person"
+ }
+ }
+ }
+ },
+ "400": {
+ "description": "User not found (Will not have an associated response schema)"
+ },
+ "500": {
+ "description": "Some other Error (Will have this error class as the response class)",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/ErrorResponse"
+ }
+ }
+ }
+ }
+ }
+ }
+ },
+ "/openapi/post1": {
+ "post": {
+ "tags": [],
+ "summary": "Standard Post",
+ "description": "The Deprecated annotation adds \"deprecacted:true\" to the generated json",
+ "requestBody": {
+ "description": "the body",
+ "content": {
+ "application/json": {
+ "schema": {
+ "type": "array",
+ "items": {
+ "$ref": "#/components/schemas/Person"
+ }
+ }
+ }
+ },
+ "required": true
+ },
+ "responses": {
+ "201": {
+ "description": "the response body (from javadoc)",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/Person"
+ }
+ }
+ }
+ },
+ "400": {
+ "description": "User not found"
+ },
+ "500": {
+ "description": "Some other Error",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/ErrorResponse"
+ }
+ }
+ }
+ }
+ },
+ "deprecated": true
+ }
+ },
+ "/openapi/put": {
+ "put": {
+ "tags": [],
+ "summary": "",
+ "description": "",
+ "responses": {
+ "203": {
+ "description": "",
+ "content": {
+ "text/plain": {
+ "schema": {
+ "type": "string"
+ }
+ }
+ }
+ },
+ "204": {
+ "description": "",
+ "content": {
+ "text/plain": {
+ "schema": {
+ "type": "string"
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ },
+ "components": {
+ "schemas": {
+ "ErrorResponse": {
+ "type": "object",
+ "properties": {
+ "id": {
+ "type": "string"
+ },
+ "text": {
+ "type": "string"
+ }
+ }
+ },
+ "Person": {
+ "type": "object",
+ "properties": {
+ "id": {
+ "type": "integer",
+ "format": "int64"
+ },
+ "name": {
+ "type": "string"
+ }
+ }
+ }
+ },
+ "securitySchemes": {
+ "JWT": {
+ "type": "apiKey",
+ "description": "JSON Web Tokens are an open, industry standard RFC 7519 method for representing claims securely between two parties.",
+ "name": "access_token",
+ "in": "query"
+ }
+ }
+ },
+ "jsonSchemaDialect": "https://spec.openapis.org/oas/3.1/dialect/base"
+}
diff --git a/tests/test-javalin/src/main/resources/public/openapi.json b/tests/test-javalin/src/main/resources/public/openapi.json
index 698092360..c35f91efc 100644
--- a/tests/test-javalin/src/main/resources/public/openapi.json
+++ b/tests/test-javalin/src/main/resources/public/openapi.json
@@ -1,5 +1,5 @@
{
- "openapi" : "3.0.1",
+ "openapi" : "3.1.2",
"info" : {
"title" : "Example service",
"description" : "Example Javalin controllers with Java and Maven",
@@ -56,11 +56,10 @@
"content" : {
"application/json" : {
"schema" : {
- "type" : "array",
"items" : {
- "$ref" : "#/components/schemas/Bar",
- "nullable" : false
- }
+ "$ref" : "#/components/schemas/Bar"
+ },
+ "type" : "array"
}
}
}
@@ -81,9 +80,8 @@
"in" : "path",
"required" : true,
"schema" : {
- "type" : "integer",
"format" : "int64",
- "nullable" : false
+ "type" : "integer"
}
}
],
@@ -114,11 +112,10 @@
"content" : {
"application/json" : {
"schema" : {
- "type" : "array",
"items" : {
- "$ref" : "#/components/schemas/Baz",
- "nullable" : false
- }
+ "$ref" : "#/components/schemas/Baz"
+ },
+ "type" : "array"
}
}
}
@@ -147,8 +144,8 @@
"content" : {
"application/json" : {
"schema" : {
- "type" : "integer",
- "format" : "int64"
+ "format" : "int64",
+ "type" : "integer"
}
}
}
@@ -169,9 +166,8 @@
"in" : "path",
"required" : true,
"schema" : {
- "type" : "integer",
"format" : "int32",
- "nullable" : false
+ "type" : "integer"
}
},
{
@@ -192,8 +188,8 @@
"name" : "p3",
"in" : "query",
"schema" : {
- "type" : "integer",
- "format" : "int32"
+ "format" : "int32",
+ "type" : "integer"
}
},
{
@@ -248,11 +244,10 @@
"content" : {
"application/json" : {
"schema" : {
- "type" : "array",
"items" : {
- "$ref" : "#/components/schemas/Baz",
- "nullable" : false
- }
+ "$ref" : "#/components/schemas/Baz"
+ },
+ "type" : "array"
}
}
}
@@ -273,8 +268,8 @@
"in" : "path",
"required" : true,
"schema" : {
- "type" : "integer",
- "format" : "int64"
+ "format" : "int64",
+ "type" : "integer"
}
}
],
@@ -336,11 +331,10 @@
"content" : {
"application/json" : {
"schema" : {
- "type" : "array",
"items" : {
- "$ref" : "#/components/schemas/HelloDto",
- "nullable" : false
- }
+ "$ref" : "#/components/schemas/HelloDto"
+ },
+ "type" : "array"
}
}
}
@@ -380,11 +374,10 @@
"content" : {
"application/json" : {
"schema" : {
- "type" : "array",
"items" : {
- "$ref" : "#/components/schemas/HelloDto",
- "nullable" : false
- }
+ "$ref" : "#/components/schemas/HelloDto"
+ },
+ "type" : "array"
}
}
}
@@ -435,7 +428,8 @@
"url" : {
"type" : "string"
}
- }
+ },
+ "type" : "object"
}
}
},
@@ -498,8 +492,7 @@
"type" : "object",
"properties" : {
"name" : {
- "type" : "string",
- "nullable" : false
+ "type" : "string"
},
"email" : {
"type" : "string"
@@ -508,10 +501,11 @@
"type" : "string"
},
"startDate" : {
- "type" : "string",
- "format" : "date"
+ "format" : "date",
+ "type" : "string"
}
- }
+ },
+ "type" : "object"
}
}
},
@@ -546,7 +540,8 @@
"url" : {
"type" : "string"
}
- }
+ },
+ "type" : "object"
}
}
},
@@ -573,8 +568,7 @@
"type" : "object",
"properties" : {
"name" : {
- "type" : "string",
- "nullable" : false
+ "type" : "string"
},
"email" : {
"type" : "string"
@@ -583,10 +577,11 @@
"type" : "string"
},
"startDate" : {
- "type" : "string",
- "format" : "date"
+ "format" : "date",
+ "type" : "string"
}
- }
+ },
+ "type" : "object"
}
}
},
@@ -666,9 +661,8 @@
"in" : "path",
"required" : true,
"schema" : {
- "type" : "integer",
"format" : "int32",
- "nullable" : false
+ "type" : "integer"
}
},
{
@@ -729,8 +723,7 @@
"name" : "name",
"in" : "query",
"schema" : {
- "type" : "string",
- "nullable" : false
+ "type" : "string"
}
},
{
@@ -768,9 +761,8 @@
"in" : "path",
"required" : true,
"schema" : {
- "type" : "integer",
"format" : "int32",
- "nullable" : false
+ "type" : "integer"
}
}
],
@@ -796,9 +788,8 @@
"description" : "The hello Id.",
"required" : true,
"schema" : {
- "type" : "integer",
"format" : "int32",
- "nullable" : false
+ "type" : "integer"
}
},
{
@@ -807,8 +798,8 @@
"description" : "The name of the hello",
"required" : true,
"schema" : {
- "type" : "string",
- "format" : "date"
+ "format" : "date",
+ "type" : "string"
}
},
{
@@ -865,8 +856,7 @@
"content" : {
"application/json" : {
"schema" : {
- "$ref" : "#/components/schemas/NullMarkedClassDTO",
- "nullable" : false
+ "$ref" : "#/components/schemas/NullMarkedClassDTO"
}
}
},
@@ -925,41 +915,38 @@
"components" : {
"schemas" : {
"Bar" : {
- "type" : "object",
"properties" : {
"id" : {
- "type" : "integer",
"format" : "int64",
- "nullable" : false
+ "type" : "integer"
},
"name" : {
"type" : "string"
}
- }
+ },
+ "type" : "object"
},
"Baz" : {
- "type" : "object",
"properties" : {
"id" : {
- "type" : "integer",
- "format" : "int64"
+ "format" : "int64",
+ "type" : "integer"
},
"name" : {
"type" : "string"
},
"startDate" : {
- "type" : "string",
- "format" : "date"
+ "format" : "date",
+ "type" : "string"
}
- }
+ },
+ "type" : "object"
},
"HelloDto" : {
- "type" : "object",
"properties" : {
"id" : {
- "type" : "integer",
"format" : "int32",
- "nullable" : false
+ "type" : "integer"
},
"name" : {
"type" : "string"
@@ -968,14 +955,15 @@
"type" : "string"
},
"gid" : {
- "type" : "string",
- "format" : "uuid"
+ "format" : "uuid",
+ "type" : "string"
},
"whenAction" : {
- "type" : "string",
- "format" : "date-time"
+ "format" : "date-time",
+ "type" : "string"
}
- }
+ },
+ "type" : "object"
},
"NullMarkedClassDTO" : {
"required" : [
@@ -988,98 +976,106 @@
"stringArray1",
"stringArray2"
],
- "type" : "object",
"properties" : {
"string1" : {
- "type" : "string",
- "nullable" : false
+ "type" : "string"
},
"string2" : {
- "type" : "string"
+ "type" : [
+ "string",
+ "null"
+ ]
},
"stringArray1" : {
- "type" : "array",
- "nullable" : false,
"items" : {
"type" : "string"
- }
+ },
+ "type" : "array"
},
"stringArray2" : {
- "type" : "array",
- "nullable" : false,
"items" : {
- "type" : "string"
- }
+ "type" : [
+ "string",
+ "null"
+ ]
+ },
+ "type" : "array"
},
"set1" : {
- "type" : "array",
- "nullable" : false,
"items" : {
- "type" : "string",
- "nullable" : false
- }
+ "type" : "string"
+ },
+ "type" : "array"
},
"set2" : {
- "type" : "array",
- "nullable" : false,
"items" : {
- "type" : "string"
- }
+ "type" : [
+ "string",
+ "null"
+ ]
+ },
+ "type" : "array"
},
"set3" : {
- "type" : "array",
"items" : {
- "type" : "string",
- "nullable" : false
- }
+ "type" : "string"
+ },
+ "type" : [
+ "array",
+ "null"
+ ]
},
"map1" : {
- "type" : "object",
"additionalProperties" : {
- "type" : "string",
- "nullable" : false
+ "type" : "string"
},
- "nullable" : false
+ "type" : "object"
},
"map2" : {
- "type" : "object",
"additionalProperties" : {
- "type" : "string",
- "nullable" : false
+ "type" : "string"
},
- "nullable" : false
+ "type" : "object"
},
"map3" : {
- "type" : "object",
"additionalProperties" : {
- "type" : "string"
+ "type" : [
+ "string",
+ "null"
+ ]
},
- "nullable" : false
+ "type" : "object"
},
"map4" : {
- "type" : "object",
"additionalProperties" : {
- "type" : "string",
- "nullable" : false
- }
+ "type" : "string"
+ },
+ "type" : [
+ "object",
+ "null"
+ ]
}
- }
+ },
+ "type" : "object"
},
"NullMarkedRecordDTO" : {
"required" : [
"notNullable"
],
- "type" : "object",
"properties" : {
"notNullable" : {
- "type" : "string",
- "nullable" : false
+ "type" : "string"
},
"nullable" : {
- "type" : "string"
+ "type" : [
+ "string",
+ "null"
+ ]
}
- }
+ },
+ "type" : "object"
}
}
- }
+ },
+ "jsonSchemaDialect" : "https://spec.openapis.org/oas/3.1/dialect/base"
}
\ No newline at end of file
diff --git a/tests/test-javalin/src/test/java/org/example/myapp/NullMarkedControllerTest.java b/tests/test-javalin/src/test/java/org/example/myapp/NullMarkedControllerTest.java
index d67c50de0..b5751c266 100644
--- a/tests/test-javalin/src/test/java/org/example/myapp/NullMarkedControllerTest.java
+++ b/tests/test-javalin/src/test/java/org/example/myapp/NullMarkedControllerTest.java
@@ -7,6 +7,9 @@
import java.io.IOException;
import java.io.InputStream;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
import static org.junit.jupiter.api.Assertions.assertEquals;
@@ -22,20 +25,30 @@ public void testClass() throws IOException {
JsonNode nullMarkedClassDTO = schemas.get("NullMarkedClassDTO");
assertEquals("[\"map1\",\"map2\",\"map3\",\"set1\",\"set2\",\"string1\",\"stringArray1\",\"stringArray2\"]", nullMarkedClassDTO.get("required").toString());
- assertEquals("{\"type\":\"string\",\"nullable\":false}", nullMarkedClassDTO.get("properties").get("string1").toString());
- assertEquals("{\"type\":\"string\"}", nullMarkedClassDTO.get("properties").get("string2").toString());
+ JsonNode properties = nullMarkedClassDTO.get("properties");
+ assertType(properties.get("string1"), "string");
+ assertType(properties.get("string2"), "string", "null");
- assertEquals("{\"type\":\"array\",\"nullable\":false,\"items\":{\"type\":\"string\"}}", nullMarkedClassDTO.get("properties").get("stringArray1").toString());
- assertEquals("{\"type\":\"array\",\"nullable\":false,\"items\":{\"type\":\"string\"}}", nullMarkedClassDTO.get("properties").get("stringArray2").toString());
+ assertType(properties.get("stringArray1"), "array");
+ assertType(properties.get("stringArray1").get("items"), "string");
+ assertType(properties.get("stringArray2"), "array");
+ assertType(properties.get("stringArray2").get("items"), "string", "null");
- assertEquals("{\"type\":\"array\",\"nullable\":false,\"items\":{\"type\":\"string\",\"nullable\":false}}", nullMarkedClassDTO.get("properties").get("set1").toString());
- assertEquals("{\"type\":\"array\",\"nullable\":false,\"items\":{\"type\":\"string\"}}", nullMarkedClassDTO.get("properties").get("set2").toString());
- assertEquals("{\"type\":\"array\",\"items\":{\"type\":\"string\",\"nullable\":false}}", nullMarkedClassDTO.get("properties").get("set3").toString());
+ assertType(properties.get("set1"), "array");
+ assertType(properties.get("set1").get("items"), "string");
+ assertType(properties.get("set2"), "array");
+ assertType(properties.get("set2").get("items"), "string", "null");
+ assertType(properties.get("set3"), "array", "null");
+ assertType(properties.get("set3").get("items"), "string");
- assertEquals("{\"type\":\"object\",\"additionalProperties\":{\"type\":\"string\",\"nullable\":false},\"nullable\":false}", nullMarkedClassDTO.get("properties").get("map1").toString());
- assertEquals("{\"type\":\"object\",\"additionalProperties\":{\"type\":\"string\",\"nullable\":false},\"nullable\":false}", nullMarkedClassDTO.get("properties").get("map2").toString());
- assertEquals("{\"type\":\"object\",\"additionalProperties\":{\"type\":\"string\"},\"nullable\":false}", nullMarkedClassDTO.get("properties").get("map3").toString());
- assertEquals("{\"type\":\"object\",\"additionalProperties\":{\"type\":\"string\",\"nullable\":false}}", nullMarkedClassDTO.get("properties").get("map4").toString());
+ assertType(properties.get("map1"), "object");
+ assertType(properties.get("map1").get("additionalProperties"), "string");
+ assertType(properties.get("map2"), "object");
+ assertType(properties.get("map2").get("additionalProperties"), "string");
+ assertType(properties.get("map3"), "object");
+ assertType(properties.get("map3").get("additionalProperties"), "string", "null");
+ assertType(properties.get("map4"), "object", "null");
+ assertType(properties.get("map4").get("additionalProperties"), "string");
}
}
@@ -49,8 +62,19 @@ public void testRecord() throws IOException {
JsonNode nullMarkedRecordDTO = schemas.get("NullMarkedRecordDTO");
assertEquals("[\"notNullable\"]", nullMarkedRecordDTO.get("required").toString());
- assertEquals("{\"type\":\"string\",\"nullable\":false}", nullMarkedRecordDTO.get("properties").get("notNullable").toString());
- assertEquals("{\"type\":\"string\"}", nullMarkedRecordDTO.get("properties").get("nullable").toString());
+ assertType(nullMarkedRecordDTO.get("properties").get("notNullable"), "string");
+ assertType(nullMarkedRecordDTO.get("properties").get("nullable"), "string", "null");
}
}
+
+ private static void assertType(JsonNode schema, String... expectedTypes) {
+ JsonNode type = schema.get("type");
+ if (expectedTypes.length == 1) {
+ assertEquals(expectedTypes[0], type.asText());
+ return;
+ }
+ List actualTypes = new ArrayList<>();
+ type.forEach(typeNode -> actualTypes.add(typeNode.asText()));
+ assertEquals(Arrays.asList(expectedTypes), actualTypes);
+ }
}
diff --git a/tests/test-jex/src/main/resources/public/openapi.json b/tests/test-jex/src/main/resources/public/openapi.json
index 77131cde2..1e06c4753 100644
--- a/tests/test-jex/src/main/resources/public/openapi.json
+++ b/tests/test-jex/src/main/resources/public/openapi.json
@@ -1,5 +1,5 @@
{
- "openapi" : "3.0.1",
+ "openapi" : "3.1.2",
"info" : {
"title" : "",
"version" : ""
@@ -98,11 +98,10 @@
"content" : {
"application/json" : {
"schema" : {
- "type" : "array",
"items" : {
- "$ref" : "#/components/schemas/Bar",
- "nullable" : false
- }
+ "$ref" : "#/components/schemas/Bar"
+ },
+ "type" : "array"
}
}
}
@@ -133,11 +132,10 @@
"content" : {
"application/json" : {
"schema" : {
- "type" : "array",
"items" : {
- "$ref" : "#/components/schemas/Bar",
- "nullable" : false
- }
+ "$ref" : "#/components/schemas/Bar"
+ },
+ "type" : "array"
}
}
}
@@ -158,9 +156,8 @@
"in" : "path",
"required" : true,
"schema" : {
- "type" : "integer",
"format" : "int64",
- "nullable" : false
+ "type" : "integer"
}
}
],
@@ -191,11 +188,10 @@
"content" : {
"application/json" : {
"schema" : {
- "type" : "array",
"items" : {
- "$ref" : "#/components/schemas/Baz",
- "nullable" : false
- }
+ "$ref" : "#/components/schemas/Baz"
+ },
+ "type" : "array"
}
}
}
@@ -224,8 +220,8 @@
"content" : {
"application/json" : {
"schema" : {
- "type" : "integer",
- "format" : "int64"
+ "format" : "int64",
+ "type" : "integer"
}
}
}
@@ -246,9 +242,8 @@
"in" : "path",
"required" : true,
"schema" : {
- "type" : "integer",
"format" : "int32",
- "nullable" : false
+ "type" : "integer"
}
},
{
@@ -269,8 +264,8 @@
"name" : "p3",
"in" : "query",
"schema" : {
- "type" : "integer",
- "format" : "int32"
+ "format" : "int32",
+ "type" : "integer"
}
},
{
@@ -325,11 +320,10 @@
"content" : {
"application/json" : {
"schema" : {
- "type" : "array",
"items" : {
- "$ref" : "#/components/schemas/Baz",
- "nullable" : false
- }
+ "$ref" : "#/components/schemas/Baz"
+ },
+ "type" : "array"
}
}
}
@@ -350,8 +344,8 @@
"in" : "path",
"required" : true,
"schema" : {
- "type" : "integer",
- "format" : "int64"
+ "format" : "int64",
+ "type" : "integer"
}
}
],
@@ -483,11 +477,10 @@
"content" : {
"application/json" : {
"schema" : {
- "type" : "array",
"items" : {
- "$ref" : "#/components/schemas/WebHelloDto",
- "nullable" : false
- }
+ "$ref" : "#/components/schemas/WebHelloDto"
+ },
+ "type" : "array"
}
}
}
@@ -538,7 +531,8 @@
"url" : {
"type" : "string"
}
- }
+ },
+ "type" : "object"
}
}
},
@@ -601,8 +595,7 @@
"type" : "object",
"properties" : {
"name" : {
- "type" : "string",
- "nullable" : false
+ "type" : "string"
},
"email" : {
"type" : "string"
@@ -611,10 +604,11 @@
"type" : "string"
},
"startDate" : {
- "type" : "string",
- "format" : "date"
+ "format" : "date",
+ "type" : "string"
}
- }
+ },
+ "type" : "object"
}
}
},
@@ -649,7 +643,8 @@
"url" : {
"type" : "string"
}
- }
+ },
+ "type" : "object"
}
}
},
@@ -676,8 +671,7 @@
"type" : "object",
"properties" : {
"name" : {
- "type" : "string",
- "nullable" : false
+ "type" : "string"
},
"email" : {
"type" : "string"
@@ -686,10 +680,11 @@
"type" : "string"
},
"startDate" : {
- "type" : "string",
- "format" : "date"
+ "format" : "date",
+ "type" : "string"
}
- }
+ },
+ "type" : "object"
}
}
},
@@ -768,12 +763,12 @@
"name" : "myEnum",
"in" : "query",
"schema" : {
- "type" : "string",
"enum" : [
"A",
"B",
"C"
- ]
+ ],
+ "type" : "string"
}
}
],
@@ -803,9 +798,11 @@
"name" : "myOptional",
"in" : "query",
"schema" : {
- "type" : "integer",
"format" : "int64",
- "nullable" : true
+ "type" : [
+ "integer",
+ "null"
+ ]
}
}
],
@@ -835,12 +832,14 @@
"name" : "myOptional",
"in" : "query",
"schema" : {
- "type" : "string",
- "nullable" : true,
"enum" : [
"A",
"B",
"C"
+ ],
+ "type" : [
+ "string",
+ "null"
]
}
}
@@ -871,8 +870,10 @@
"name" : "myOptional",
"in" : "query",
"schema" : {
- "type" : "string",
- "nullable" : true
+ "type" : [
+ "string",
+ "null"
+ ]
}
}
],
@@ -903,9 +904,8 @@
"in" : "path",
"required" : true,
"schema" : {
- "type" : "integer",
"format" : "int32",
- "nullable" : false
+ "type" : "integer"
}
},
{
@@ -966,8 +966,7 @@
"name" : "name",
"in" : "query",
"schema" : {
- "type" : "string",
- "nullable" : false
+ "type" : "string"
}
},
{
@@ -981,11 +980,10 @@
"name" : "addresses",
"in" : "query",
"schema" : {
- "type" : "array",
"items" : {
- "type" : "string",
- "nullable" : false
- }
+ "type" : "string"
+ },
+ "type" : "array"
}
},
{
@@ -999,16 +997,15 @@
"name" : "type",
"in" : "query",
"schema" : {
- "type" : "array",
"items" : {
- "type" : "string",
- "nullable" : false,
"enum" : [
"PROXY",
"HIDE_N_SEEK",
"FFA"
- ]
- }
+ ],
+ "type" : "string"
+ },
+ "type" : "array"
}
}
],
@@ -1039,9 +1036,8 @@
"in" : "path",
"required" : true,
"schema" : {
- "type" : "integer",
"format" : "int32",
- "nullable" : false
+ "type" : "integer"
}
}
],
@@ -1066,9 +1062,8 @@
"description" : "The hello Id.",
"required" : true,
"schema" : {
- "type" : "integer",
"format" : "int32",
- "nullable" : false
+ "type" : "integer"
}
},
{
@@ -1077,8 +1072,8 @@
"description" : "The name of the hello",
"required" : true,
"schema" : {
- "type" : "string",
- "format" : "date"
+ "format" : "date",
+ "type" : "string"
}
},
{
@@ -1472,11 +1467,10 @@
"content" : {
"application/json" : {
"schema" : {
- "type" : "array",
"items" : {
- "$ref" : "#/components/schemas/HelloDto",
- "nullable" : false
- }
+ "$ref" : "#/components/schemas/HelloDto"
+ },
+ "type" : "array"
}
}
}
@@ -1517,12 +1511,12 @@
"name" : "type",
"in" : "query",
"schema" : {
- "type" : "string",
"enum" : [
"PROXY",
"HIDE_N_SEEK",
"FFA"
- ]
+ ],
+ "type" : "string"
}
}
],
@@ -1552,16 +1546,15 @@
"name" : "type",
"in" : "query",
"schema" : {
- "type" : "array",
"items" : {
- "type" : "string",
- "nullable" : false,
"enum" : [
"PROXY",
"HIDE_N_SEEK",
"FFA"
- ]
- }
+ ],
+ "type" : "string"
+ },
+ "type" : "array"
}
}
],
@@ -1598,12 +1591,12 @@
"name" : "type",
"in" : "query",
"schema" : {
- "type" : "string",
"enum" : [
"PROXY",
"HIDE_N_SEEK",
"FFA"
- ]
+ ],
+ "type" : "string"
}
}
],
@@ -1632,11 +1625,10 @@
"content" : {
"application/json" : {
"schema" : {
- "type" : "array",
"items" : {
- "type" : "string",
- "nullable" : false
- }
+ "type" : "string"
+ },
+ "type" : "array"
}
}
},
@@ -1668,11 +1660,10 @@
"name" : "strings",
"in" : "query",
"schema" : {
- "type" : "array",
"items" : {
- "type" : "string",
- "nullable" : false
- }
+ "type" : "string"
+ },
+ "type" : "array"
}
}
],
@@ -1815,146 +1806,141 @@
"components" : {
"schemas" : {
"Bar" : {
- "type" : "object",
"properties" : {
"id" : {
- "type" : "integer",
"format" : "int64",
- "nullable" : false
+ "type" : "integer"
},
"name" : {
"type" : "string"
}
- }
+ },
+ "type" : "object"
},
"Baz" : {
- "type" : "object",
"properties" : {
"id" : {
- "type" : "integer",
- "format" : "int64"
+ "format" : "int64",
+ "type" : "integer"
},
"name" : {
"type" : "string"
},
"startDate" : {
- "type" : "string",
- "format" : "date"
+ "format" : "date",
+ "type" : "string"
}
- }
+ },
+ "type" : "object"
},
"HelloDto" : {
"required" : [
"name"
],
- "type" : "object",
"properties" : {
"id" : {
- "type" : "integer",
"format" : "int32",
- "nullable" : false
+ "type" : "integer"
},
"name" : {
- "type" : "string",
- "nullable" : false
+ "type" : "string"
},
"serverType" : {
- "type" : "string",
"enum" : [
"PROXY",
"HIDE_N_SEEK",
"FFA"
- ]
+ ],
+ "type" : "string"
}
- }
+ },
+ "type" : "object"
},
"HelloWorld" : {
- "type" : "object",
"properties" : {
"message" : {
"type" : "string"
},
"people" : {
- "type" : "array",
"items" : {
- "$ref" : "#/components/schemas/Person",
- "nullable" : false
- }
+ "$ref" : "#/components/schemas/Person"
+ },
+ "type" : "array"
}
- }
+ },
+ "type" : "object"
},
"HelloWorldZeroDependency" : {
- "type" : "object",
"properties" : {
"message" : {
"type" : "string"
},
"people" : {
- "type" : "array",
"items" : {
- "$ref" : "#/components/schemas/Person",
- "nullable" : false
- }
+ "$ref" : "#/components/schemas/Person"
+ },
+ "type" : "array"
}
- }
+ },
+ "type" : "object"
},
"Person" : {
- "type" : "object",
"properties" : {
"name" : {
"type" : "string"
},
"birthday" : {
- "type" : "string",
- "format" : "date"
+ "format" : "date",
+ "type" : "string"
}
- }
+ },
+ "type" : "object"
},
"StreamingOutput" : {
"type" : "object"
},
"ViewHome" : {
- "type" : "object",
"properties" : {
"name" : {
"type" : "string"
}
- }
+ },
+ "type" : "object"
},
"ViewPartial" : {
- "type" : "object",
"properties" : {
"name" : {
"type" : "string"
}
- }
+ },
+ "type" : "object"
},
"WebHelloDto" : {
- "type" : "object",
"properties" : {
"id" : {
- "type" : "integer",
"format" : "int32",
- "nullable" : false
+ "type" : "integer"
},
"name" : {
- "type" : "string",
- "description" : "This is a comment"
+ "description" : "This is a comment",
+ "type" : "string"
},
"otherParam" : {
- "type" : "string",
- "description" : "This is a comment"
+ "description" : "This is a comment",
+ "type" : "string"
},
"gid" : {
- "type" : "string",
- "format" : "uuid"
+ "format" : "uuid",
+ "type" : "string"
},
"whenAction" : {
- "type" : "string",
- "format" : "date-time"
+ "format" : "date-time",
+ "type" : "string"
}
- }
+ },
+ "type" : "object"
}
}
- }
+ },
+ "jsonSchemaDialect" : "https://spec.openapis.org/oas/3.1/dialect/base"
}
\ No newline at end of file
diff --git a/tests/test-nima-jsonb/src/test/java/io/avaje/http/generator/NimaProcessorTest.java b/tests/test-nima-jsonb/src/test/java/io/avaje/http/generator/NimaProcessorTest.java
index ad58a6faf..fa03b82e3 100644
--- a/tests/test-nima-jsonb/src/test/java/io/avaje/http/generator/NimaProcessorTest.java
+++ b/tests/test-nima-jsonb/src/test/java/io/avaje/http/generator/NimaProcessorTest.java
@@ -111,6 +111,11 @@ public void testOpenAPIGeneration() throws Exception {
mapper.readTree(new File("src/test/resources/expectedOpenApi.json"));
final var generatedOpenApi = mapper.readTree(new File("openapi.json"));
- assert expectedOpenApiJson.equals(generatedOpenApi);
+ final var json = generatedOpenApi.toString();
+ assertThat(json).contains("\"openapi\":\"3.1.2\"");
+ assertThat(json).contains("\"jsonSchemaDialect\":\"https://spec.openapis.org/oas/3.1/dialect/base\"");
+ assertThat(json).doesNotContain("\"nullable\"");
+ assertThat(json).doesNotContain("\"types\":");
+ assertThat(generatedOpenApi).isEqualTo(expectedOpenApiJson);
}
}
diff --git a/tests/test-nima-jsonb/src/test/resources/expectedOpenApi.json b/tests/test-nima-jsonb/src/test/resources/expectedOpenApi.json
index bae5ce0e2..073678b16 100644
--- a/tests/test-nima-jsonb/src/test/resources/expectedOpenApi.json
+++ b/tests/test-nima-jsonb/src/test/resources/expectedOpenApi.json
@@ -1,9 +1,9 @@
{
- "openapi": "3.0.1",
- "info": {
- "title": "Example service",
- "description": "Example Helidon controllers with Java and Maven",
- "version": ""
+ "openapi" : "3.1.2",
+ "info" : {
+ "title" : "Example service",
+ "description" : "Example Helidon controllers with Java and Maven",
+ "version" : ""
},
"servers": [
{
@@ -147,8 +147,7 @@
"schema": {
"type": "array",
"items": {
- "$ref": "#/components/schemas/Person",
- "nullable": false
+ "$ref": "#/components/schemas/Person"
}
}
}
@@ -231,8 +230,7 @@
"properties": {
"id": {
"type": "integer",
- "format": "int64",
- "nullable": false
+ "format": "int64"
},
"name": {
"type": "string"
@@ -248,5 +246,6 @@
"in": "query"
}
}
- }
-}
\ No newline at end of file
+ },
+ "jsonSchemaDialect" : "https://spec.openapis.org/oas/3.1/dialect/base"
+}
diff --git a/tests/test-sigma/src/main/resources/public/openapi.json b/tests/test-sigma/src/main/resources/public/openapi.json
index 59a801b85..08c5432eb 100644
--- a/tests/test-sigma/src/main/resources/public/openapi.json
+++ b/tests/test-sigma/src/main/resources/public/openapi.json
@@ -1,5 +1,5 @@
{
- "openapi" : "3.0.1",
+ "openapi" : "3.1.2",
"info" : {
"title" : "Example service",
"description" : "Example Javalin controllers with Java and Maven",
@@ -62,11 +62,10 @@
"content" : {
"application/json" : {
"schema" : {
- "type" : "array",
"items" : {
- "$ref" : "#/components/schemas/Bar",
- "nullable" : false
- }
+ "$ref" : "#/components/schemas/Bar"
+ },
+ "type" : "array"
}
}
}
@@ -87,9 +86,8 @@
"in" : "path",
"required" : true,
"schema" : {
- "type" : "integer",
"format" : "int64",
- "nullable" : false
+ "type" : "integer"
}
}
],
@@ -120,11 +118,10 @@
"content" : {
"application/json" : {
"schema" : {
- "type" : "array",
"items" : {
- "$ref" : "#/components/schemas/Baz",
- "nullable" : false
- }
+ "$ref" : "#/components/schemas/Baz"
+ },
+ "type" : "array"
}
}
}
@@ -153,8 +150,8 @@
"content" : {
"application/json" : {
"schema" : {
- "type" : "integer",
- "format" : "int64"
+ "format" : "int64",
+ "type" : "integer"
}
}
}
@@ -175,9 +172,8 @@
"in" : "path",
"required" : true,
"schema" : {
- "type" : "integer",
"format" : "int32",
- "nullable" : false
+ "type" : "integer"
}
},
{
@@ -198,8 +194,8 @@
"name" : "p3",
"in" : "query",
"schema" : {
- "type" : "integer",
- "format" : "int32"
+ "format" : "int32",
+ "type" : "integer"
}
},
{
@@ -254,11 +250,10 @@
"content" : {
"application/json" : {
"schema" : {
- "type" : "array",
"items" : {
- "$ref" : "#/components/schemas/Baz",
- "nullable" : false
- }
+ "$ref" : "#/components/schemas/Baz"
+ },
+ "type" : "array"
}
}
}
@@ -279,8 +274,8 @@
"in" : "path",
"required" : true,
"schema" : {
- "type" : "integer",
- "format" : "int64"
+ "format" : "int64",
+ "type" : "integer"
}
}
],
@@ -363,11 +358,10 @@
"content" : {
"application/json" : {
"schema" : {
- "type" : "array",
"items" : {
- "$ref" : "#/components/schemas/HelloDto",
- "nullable" : false
- }
+ "$ref" : "#/components/schemas/HelloDto"
+ },
+ "type" : "array"
}
}
}
@@ -427,11 +421,10 @@
"content" : {
"application/json" : {
"schema" : {
- "type" : "array",
"items" : {
- "$ref" : "#/components/schemas/HelloDto",
- "nullable" : false
- }
+ "$ref" : "#/components/schemas/HelloDto"
+ },
+ "type" : "array"
}
}
}
@@ -482,7 +475,8 @@
"url" : {
"type" : "string"
}
- }
+ },
+ "type" : "object"
}
}
},
@@ -545,8 +539,7 @@
"type" : "object",
"properties" : {
"name" : {
- "type" : "string",
- "nullable" : false
+ "type" : "string"
},
"email" : {
"type" : "string"
@@ -555,10 +548,11 @@
"type" : "string"
},
"startDate" : {
- "type" : "string",
- "format" : "date"
+ "format" : "date",
+ "type" : "string"
}
- }
+ },
+ "type" : "object"
}
}
},
@@ -593,7 +587,8 @@
"url" : {
"type" : "string"
}
- }
+ },
+ "type" : "object"
}
}
},
@@ -620,8 +615,7 @@
"type" : "object",
"properties" : {
"name" : {
- "type" : "string",
- "nullable" : false
+ "type" : "string"
},
"email" : {
"type" : "string"
@@ -630,10 +624,11 @@
"type" : "string"
},
"startDate" : {
- "type" : "string",
- "format" : "date"
+ "format" : "date",
+ "type" : "string"
}
- }
+ },
+ "type" : "object"
}
}
},
@@ -712,12 +707,12 @@
"name" : "myEnum",
"in" : "query",
"schema" : {
- "type" : "string",
"enum" : [
"A",
"B",
"C"
- ]
+ ],
+ "type" : "string"
}
}
],
@@ -748,9 +743,8 @@
"in" : "path",
"required" : true,
"schema" : {
- "type" : "integer",
"format" : "int32",
- "nullable" : false
+ "type" : "integer"
}
},
{
@@ -811,8 +805,7 @@
"name" : "name",
"in" : "query",
"schema" : {
- "type" : "string",
- "nullable" : false
+ "type" : "string"
}
},
{
@@ -826,11 +819,10 @@
"name" : "addresses",
"in" : "query",
"schema" : {
- "type" : "array",
"items" : {
- "type" : "string",
- "nullable" : false
- }
+ "type" : "string"
+ },
+ "type" : "array"
}
},
{
@@ -844,16 +836,15 @@
"name" : "type",
"in" : "query",
"schema" : {
- "type" : "array",
"items" : {
- "type" : "string",
- "nullable" : false,
"enum" : [
"PROXY",
"HIDE_N_SEEK",
"FFA"
- ]
- }
+ ],
+ "type" : "string"
+ },
+ "type" : "array"
}
}
],
@@ -884,9 +875,8 @@
"in" : "path",
"required" : true,
"schema" : {
- "type" : "integer",
"format" : "int32",
- "nullable" : false
+ "type" : "integer"
}
}
],
@@ -911,9 +901,8 @@
"description" : "The hello Id.",
"required" : true,
"schema" : {
- "type" : "integer",
"format" : "int32",
- "nullable" : false
+ "type" : "integer"
}
},
{
@@ -922,8 +911,8 @@
"description" : "The name of the hello",
"required" : true,
"schema" : {
- "type" : "string",
- "format" : "date"
+ "format" : "date",
+ "type" : "string"
}
},
{
@@ -1114,11 +1103,10 @@
"content" : {
"application/json" : {
"schema" : {
- "type" : "array",
"items" : {
- "$ref" : "#/components/schemas/Person",
- "nullable" : false
- }
+ "$ref" : "#/components/schemas/Person"
+ },
+ "type" : "array"
}
}
},
@@ -1305,14 +1293,15 @@
"type" : "string"
},
"type" : {
- "type" : "string",
"enum" : [
"PROXY",
"HIDE_N_SEEK",
"FFA"
- ]
+ ],
+ "type" : "string"
}
- }
+ },
+ "type" : "object"
}
}
},
@@ -1342,14 +1331,15 @@
"type" : "string"
},
"type" : {
- "type" : "string",
"enum" : [
"PROXY",
"HIDE_N_SEEK",
"FFA"
- ]
+ ],
+ "type" : "string"
}
- }
+ },
+ "type" : "object"
}
}
},
@@ -1382,12 +1372,12 @@
"in" : "path",
"required" : true,
"schema" : {
- "type" : "string",
"enum" : [
"PROXY",
"HIDE_N_SEEK",
"FFA"
- ]
+ ],
+ "type" : "string"
}
}
],
@@ -1417,12 +1407,12 @@
"name" : "type",
"in" : "query",
"schema" : {
- "type" : "string",
"enum" : [
"PROXY",
"HIDE_N_SEEK",
"FFA"
- ]
+ ],
+ "type" : "string"
}
}
],
@@ -1452,16 +1442,15 @@
"name" : "type",
"in" : "query",
"schema" : {
- "type" : "array",
"items" : {
- "type" : "string",
- "nullable" : false,
"enum" : [
"PROXY",
"HIDE_N_SEEK",
"FFA"
- ]
- }
+ ],
+ "type" : "string"
+ },
+ "type" : "array"
}
}
],
@@ -1498,12 +1487,12 @@
"name" : "type",
"in" : "query",
"schema" : {
- "type" : "string",
"enum" : [
"PROXY",
"HIDE_N_SEEK",
"FFA"
- ]
+ ],
+ "type" : "string"
}
}
],
@@ -1543,7 +1532,8 @@
"url" : {
"type" : "string"
}
- }
+ },
+ "type" : "object"
}
}
},
@@ -1585,7 +1575,8 @@
"url" : {
"type" : "string"
}
- }
+ },
+ "type" : "object"
}
}
},
@@ -1619,13 +1610,13 @@
"type" : "object",
"properties" : {
"strings" : {
- "type" : "array",
"items" : {
- "type" : "string",
- "nullable" : false
- }
+ "type" : "string"
+ },
+ "type" : "array"
}
- }
+ },
+ "type" : "object"
}
}
},
@@ -1709,9 +1700,8 @@
"content" : {
"application/json" : {
"schema" : {
- "type" : "integer",
"format" : "int32",
- "nullable" : false
+ "type" : "integer"
}
}
}
@@ -1732,9 +1722,8 @@
"content" : {
"application/json" : {
"schema" : {
- "type" : "integer",
"format" : "int64",
- "nullable" : false
+ "type" : "integer"
}
}
}
@@ -1784,11 +1773,10 @@
"content" : {
"application/json" : {
"schema" : {
- "type" : "array",
"items" : {
- "$ref" : "#/components/schemas/Person",
- "nullable" : false
- }
+ "$ref" : "#/components/schemas/Person"
+ },
+ "type" : "array"
}
}
},
@@ -1862,11 +1850,10 @@
"content" : {
"application/json" : {
"schema" : {
- "type" : "array",
"items" : {
- "$ref" : "#/components/schemas/Person",
- "nullable" : false
- }
+ "$ref" : "#/components/schemas/Person"
+ },
+ "type" : "array"
}
}
}
@@ -1897,11 +1884,10 @@
"content" : {
"application/json" : {
"schema" : {
- "type" : "object",
"additionalProperties" : {
- "$ref" : "#/components/schemas/Person",
- "nullable" : false
- }
+ "$ref" : "#/components/schemas/Person"
+ },
+ "type" : "object"
}
}
}
@@ -1932,11 +1918,10 @@
"content" : {
"application/json" : {
"schema" : {
- "type" : "array",
"items" : {
- "$ref" : "#/components/schemas/Person",
- "nullable" : false
- }
+ "$ref" : "#/components/schemas/Person"
+ },
+ "type" : "array"
}
}
}
@@ -2035,48 +2020,45 @@
"components" : {
"schemas" : {
"Bar" : {
- "type" : "object",
"properties" : {
"id" : {
- "type" : "integer",
"format" : "int64",
- "nullable" : false
+ "type" : "integer"
},
"name" : {
"type" : "string"
}
- }
+ },
+ "type" : "object"
},
"Baz" : {
- "type" : "object",
"properties" : {
"id" : {
- "type" : "integer",
- "format" : "int64"
+ "format" : "int64",
+ "type" : "integer"
},
"name" : {
"type" : "string"
},
"startDate" : {
- "type" : "string",
- "format" : "date"
+ "format" : "date",
+ "type" : "string"
}
- }
+ },
+ "type" : "object"
},
"Data_Bar" : {
- "type" : "object",
"properties" : {
"data" : {
- "type" : "array",
"items" : {
- "$ref" : "#/components/schemas/Bar",
- "nullable" : false
- }
+ "$ref" : "#/components/schemas/Bar"
+ },
+ "type" : "array"
}
- }
+ },
+ "type" : "object"
},
"ErrorResponse" : {
- "type" : "object",
"properties" : {
"id" : {
"type" : "string"
@@ -2084,75 +2066,73 @@
"text" : {
"type" : "string"
}
- }
+ },
+ "type" : "object"
},
"HelloDto" : {
- "type" : "object",
"properties" : {
"id" : {
- "type" : "integer",
"format" : "int32",
- "nullable" : false
+ "type" : "integer"
},
"name" : {
- "type" : "string",
- "description" : "This is a comment"
+ "description" : "This is a comment",
+ "type" : "string"
},
"otherParam" : {
- "type" : "string",
- "description" : "This is a comment"
+ "description" : "This is a comment",
+ "type" : "string"
},
"gid" : {
- "type" : "string",
- "format" : "uuid"
+ "format" : "uuid",
+ "type" : "string"
},
"whenAction" : {
- "type" : "string",
- "format" : "date-time"
+ "format" : "date-time",
+ "type" : "string"
}
- }
+ },
+ "type" : "object"
},
"HelloWorld" : {
- "type" : "object",
"properties" : {
"message" : {
"type" : "string"
},
"people" : {
- "type" : "array",
"items" : {
- "$ref" : "#/components/schemas/Person",
- "nullable" : false
- }
+ "$ref" : "#/components/schemas/Person"
+ },
+ "type" : "array"
}
- }
+ },
+ "type" : "object"
},
"HelloWorldZeroDependency" : {
- "type" : "object",
"properties" : {
"message" : {
"type" : "string"
},
"people" : {
- "type" : "array",
"items" : {
- "$ref" : "#/components/schemas/Person",
- "nullable" : false
- }
+ "$ref" : "#/components/schemas/Person"
+ },
+ "type" : "array"
}
- }
+ },
+ "type" : "object"
},
"Person" : {
- "type" : "object",
"properties" : {
"name" : {
"type" : "string"
},
"birthday" : {
- "type" : "string",
- "format" : "date"
+ "format" : "date",
+ "type" : "string"
}
- }
+ },
+ "type" : "object"
}
},
"securitySchemes" : {
@@ -2163,5 +2143,6 @@
"in" : "query"
}
}
- }
+ },
+ "jsonSchemaDialect" : "https://spec.openapis.org/oas/3.1/dialect/base"
}
\ No newline at end of file
diff --git a/tests/test-vertx-jsonb/src/test/resources/expectedOpenApi.json b/tests/test-vertx-jsonb/src/test/resources/expectedOpenApi.json
index 66c840d9a..01795e036 100644
--- a/tests/test-vertx-jsonb/src/test/resources/expectedOpenApi.json
+++ b/tests/test-vertx-jsonb/src/test/resources/expectedOpenApi.json
@@ -1,5 +1,5 @@
{
- "openapi" : "3.0.1",
+ "openapi" : "3.1.2",
"info" : {
"title" : "",
"version" : ""
@@ -45,11 +45,10 @@
"in" : "path",
"required" : true,
"schema" : {
- "type" : "integer",
- "format" : "int64",
- "nullable" : false
- }
- },
+ "type" : "integer",
+ "format" : "int64",
+ }
+ },
{
"name" : "q",
"in" : "query",
@@ -185,5 +184,6 @@
}
}
}
- }
-}
\ No newline at end of file
+ },
+ "jsonSchemaDialect" : "https://spec.openapis.org/oas/3.1/dialect/base"
+}
diff --git a/tests/test-vertx/src/test/resources/expectedOpenApi.json b/tests/test-vertx/src/test/resources/expectedOpenApi.json
index cf4a304aa..37675c056 100644
--- a/tests/test-vertx/src/test/resources/expectedOpenApi.json
+++ b/tests/test-vertx/src/test/resources/expectedOpenApi.json
@@ -1,30 +1,28 @@
{
- "openapi" : "3.0.1",
- "info" : {
- "title" : "",
- "version" : ""
+ "openapi": "3.1.2",
+ "info": {
+ "title": "",
+ "version": ""
},
- "servers" : [
+ "servers": [
{
- "url" : "localhost:8080",
- "description" : "local testing"
+ "url": "localhost:8080",
+ "description": "local testing"
}
],
- "paths" : {
- "/a" : {
- "get" : {
- "tags" : [
-
- ],
- "summary" : "",
- "description" : "",
- "responses" : {
- "200" : {
- "description" : "",
- "content" : {
- "application/json" : {
- "schema" : {
- "$ref" : "#/components/schemas/StandardRecordWithComments"
+ "paths": {
+ "/a": {
+ "get": {
+ "tags": [],
+ "summary": "",
+ "description": "",
+ "responses": {
+ "200": {
+ "description": "",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/StandardRecordWithComments"
}
}
}
@@ -32,22 +30,20 @@
}
}
},
- "/b" : {
- "get" : {
- "tags" : [
-
- ],
- "summary" : "",
- "description" : "",
- "responses" : {
- "200" : {
- "description" : "",
- "content" : {
- "application/json" : {
- "schema" : {
- "description" : "I'm overriding the description",
- "$ref" : "#/components/schemas/RecordWithSchemaDescriptions",
- "example" : "{\"item\": \"Hi example\"}"
+ "/b": {
+ "get": {
+ "tags": [],
+ "summary": "",
+ "description": "",
+ "responses": {
+ "200": {
+ "description": "",
+ "content": {
+ "application/json": {
+ "schema": {
+ "description": "I'm overriding the description",
+ "$ref": "#/components/schemas/RecordWithSchemaDescriptions",
+ "example": "{\"item\": \"Hi example\"}"
}
}
}
@@ -55,44 +51,40 @@
}
}
},
- "/c" : {
- "post" : {
- "tags" : [
-
- ],
- "summary" : "",
- "description" : "",
- "requestBody" : {
- "content" : {
- "application/text" : {
- "schema" : {
- "type" : "string"
+ "/c": {
+ "post": {
+ "tags": [],
+ "summary": "",
+ "description": "",
+ "requestBody": {
+ "content": {
+ "application/text": {
+ "schema": {
+ "type": "string"
}
}
},
- "required" : true
+ "required": true
},
- "responses" : {
- "201" : {
- "description" : "No content"
+ "responses": {
+ "201": {
+ "description": "No content"
}
}
}
},
- "/hello" : {
- "get" : {
- "tags" : [
-
- ],
- "summary" : "",
- "description" : "",
- "responses" : {
- "200" : {
- "description" : "",
- "content" : {
- "text/plain" : {
- "schema" : {
- "type" : "string"
+ "/hello": {
+ "get": {
+ "tags": [],
+ "summary": "",
+ "description": "",
+ "responses": {
+ "200": {
+ "description": "",
+ "content": {
+ "text/plain": {
+ "schema": {
+ "type": "string"
}
}
}
@@ -100,46 +92,43 @@
}
}
},
- "/hello/with-params/{id}" : {
- "get" : {
- "tags" : [
-
- ],
- "summary" : "",
- "description" : "",
- "parameters" : [
+ "/hello/with-params/{id}": {
+ "get": {
+ "tags": [],
+ "summary": "",
+ "description": "",
+ "parameters": [
{
- "name" : "id",
- "in" : "path",
- "required" : true,
- "schema" : {
- "type" : "integer",
- "format" : "int64",
- "nullable" : false
+ "name": "id",
+ "in": "path",
+ "required": true,
+ "schema": {
+ "format": "int64",
+ "type": "integer"
}
},
{
- "name" : "q",
- "in" : "query",
- "schema" : {
- "type" : "string"
+ "name": "q",
+ "in": "query",
+ "schema": {
+ "type": "string"
}
},
{
- "name" : "X-Trace",
- "in" : "header",
- "schema" : {
- "type" : "string"
+ "name": "X-Trace",
+ "in": "header",
+ "schema": {
+ "type": "string"
}
}
],
- "responses" : {
- "200" : {
- "description" : "",
- "content" : {
- "text/plain" : {
- "schema" : {
- "type" : "string"
+ "responses": {
+ "200": {
+ "description": "",
+ "content": {
+ "text/plain": {
+ "schema": {
+ "type": "string"
}
}
}
@@ -147,20 +136,18 @@
}
}
},
- "/roles-test/blocking" : {
- "get" : {
- "tags" : [
-
- ],
- "summary" : "",
- "description" : "",
- "responses" : {
- "200" : {
- "description" : "",
- "content" : {
- "application/json" : {
- "schema" : {
- "type" : "string"
+ "/roles-test/blocking": {
+ "get": {
+ "tags": [],
+ "summary": "",
+ "description": "",
+ "responses": {
+ "200": {
+ "description": "",
+ "content": {
+ "application/json": {
+ "schema": {
+ "type": "string"
}
}
}
@@ -168,20 +155,18 @@
}
}
},
- "/roles-test/explicit" : {
- "get" : {
- "tags" : [
-
- ],
- "summary" : "",
- "description" : "",
- "responses" : {
- "200" : {
- "description" : "",
- "content" : {
- "application/json" : {
- "schema" : {
- "type" : "string"
+ "/roles-test/explicit": {
+ "get": {
+ "tags": [],
+ "summary": "",
+ "description": "",
+ "responses": {
+ "200": {
+ "description": "",
+ "content": {
+ "application/json": {
+ "schema": {
+ "type": "string"
}
}
}
@@ -189,20 +174,18 @@
}
}
},
- "/roles-test/inherited" : {
- "get" : {
- "tags" : [
-
- ],
- "summary" : "",
- "description" : "",
- "responses" : {
- "200" : {
- "description" : "",
- "content" : {
- "application/json" : {
- "schema" : {
- "type" : "string"
+ "/roles-test/inherited": {
+ "get": {
+ "tags": [],
+ "summary": "",
+ "description": "",
+ "responses": {
+ "200": {
+ "description": "",
+ "content": {
+ "application/json": {
+ "schema": {
+ "type": "string"
}
}
}
@@ -210,30 +193,28 @@
}
}
},
- "/roles-test/payload" : {
- "post" : {
- "tags" : [
-
- ],
- "summary" : "",
- "description" : "",
- "requestBody" : {
- "content" : {
- "application/json" : {
- "schema" : {
- "$ref" : "#/components/schemas/Payload"
+ "/roles-test/payload": {
+ "post": {
+ "tags": [],
+ "summary": "",
+ "description": "",
+ "requestBody": {
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/Payload"
}
}
},
- "required" : true
+ "required": true
},
- "responses" : {
- "201" : {
- "description" : "",
- "content" : {
- "application/json" : {
- "schema" : {
- "$ref" : "#/components/schemas/Payload"
+ "responses": {
+ "201": {
+ "description": "",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/Payload"
}
}
}
@@ -242,41 +223,41 @@
}
}
},
- "components" : {
- "schemas" : {
- "Payload" : {
- "type" : "object",
- "properties" : {
- "name" : {
- "type" : "string"
+ "components": {
+ "schemas": {
+ "Payload": {
+ "properties": {
+ "name": {
+ "type": "string"
}
- }
+ },
+ "type": "object"
},
- "RecordWithSchemaDescriptions" : {
- "type" : "object",
- "properties" : {
- "item" : {
- "_default" : "This is a default value",
- "description" : "Overridden",
+ "RecordWithSchemaDescriptions": {
+ "properties": {
+ "item": {
+ "_default": "This is a default value",
+ "description": "Overridden",
"type": "string"
}
- }
+ },
+ "type": "object"
},
- "StandardRecordWithComments" : {
- "type" : "object",
- "properties" : {
- "blah" : {
- "type" : "string",
- "description" : "The first param"
+ "StandardRecordWithComments": {
+ "properties": {
+ "blah": {
+ "description": "The first param",
+ "type": "string"
},
- "anotherBlah" : {
- "type" : "integer",
- "description" : "The second param",
- "format" : "int32",
- "nullable" : false
+ "anotherBlah": {
+ "description": "The second param",
+ "format": "int32",
+ "type": "integer"
}
- }
+ },
+ "type": "object"
}
}
- }
+ },
+ "jsonSchemaDialect": "https://spec.openapis.org/oas/3.1/dialect/base"
}