diff --git a/openapi/src/main/java/io/micronaut/openapi/visitor/AbstractOpenApiVisitor.java b/openapi/src/main/java/io/micronaut/openapi/visitor/AbstractOpenApiVisitor.java index 0c00e8e346..ba26bdcfc5 100644 --- a/openapi/src/main/java/io/micronaut/openapi/visitor/AbstractOpenApiVisitor.java +++ b/openapi/src/main/java/io/micronaut/openapi/visitor/AbstractOpenApiVisitor.java @@ -905,6 +905,8 @@ protected Schema resolveSchema(OpenAPI openAPI, @Nullable Element definingElemen schema = new StringSchema().format("partial-time"); } else if (type.isAssignable(Number.class)) { schema = PrimitiveType.NUMBER.createProperty(); + } else if (type.getName().equals(Object.class.getName())) { + schema = PrimitiveType.OBJECT.createProperty(); } else { schema = getSchemaDefinition(openAPI, context, type, typeArgs, definingElement, mediaTypes); } @@ -1022,7 +1024,7 @@ protected void processSchemaProperty(VisitorContext context, TypedElement elemen String elType = schemaAnnotationValue.stringValue("type").orElse(null); String elFormat = schemaAnnotationValue.stringValue("format").orElse(null); if (elType == null && elementType != null) { - Pair typeAndFormat = ConvertUtils.getTypeAndFormatByClass(elementType.getName()); + Pair typeAndFormat = ConvertUtils.getTypeAndFormatByClass(elementType.getName(), elementType.isArray()); elType = typeAndFormat.getFirst(); if (elFormat == null) { elFormat = typeAndFormat.getSecond(); @@ -1583,7 +1585,7 @@ private void setSchemaDocumentation(Element element, Schema schemaToBind) { protected Schema bindSchemaAnnotationValue(VisitorContext context, Element element, Schema schemaToBind, AnnotationValue schemaAnn) { ClassElement classElement = ((TypedElement) element).getType(); - Pair typeAndFormat = classElement.isIterable() ? Pair.of("array", null) : ConvertUtils.getTypeAndFormatByClass(classElement.getName()); + Pair typeAndFormat = classElement.isIterable() ? Pair.of("array", null) : ConvertUtils.getTypeAndFormatByClass(classElement.getName(), classElement.isArray()); JsonNode schemaJson = toJson(schemaAnn.getValues(), context); return doBindSchemaAnnotationValue(context, element, schemaToBind, schemaJson, @@ -1604,7 +1606,8 @@ private Schema doBindSchemaAnnotationValue(VisitorContext context, Element eleme } if (elType == null && element != null) { - Pair typeAndFormat = ConvertUtils.getTypeAndFormatByClass(((TypedElement) element).getType().getName()); + ClassElement typeEl = ((TypedElement) element).getType(); + Pair typeAndFormat = ConvertUtils.getTypeAndFormatByClass(typeEl.getName(), typeEl.isArray()); elType = typeAndFormat.getFirst(); if (elFormat == null) { elFormat = typeAndFormat.getSecond(); @@ -1814,7 +1817,9 @@ private Schema getSchemaDefinition( schemas.put(schemaName, schema); EnumElement enumEl = (EnumElement) type; - schema.setType(checkEnumJsonValueType(enumEl, schema.getType())); + Pair typeAndFormat = checkEnumJsonValueType(enumEl, schema.getType(), schema.getFormat()); + schema.setType(typeAndFormat.getFirst()); + schema.setFormat(typeAndFormat.getSecond()); if (CollectionUtils.isEmpty(schema.getEnum())) { schema.setEnum(getEnumValues(enumEl, schema.getType(), schema.getFormat(), context)); } @@ -1894,6 +1899,11 @@ private Schema processSuperTypes(Schema schema, List mediaTypes, Map schemas, VisitorContext context) { + + if (type.getName().equals(Object.class.getName())) { + return null; + } + ClassElement classElement = ((TypedElement) type).getType(); List superTypes = new ArrayList<>(); Collection parentInterfaces = classElement.getInterfaces(); @@ -2012,8 +2022,9 @@ protected Schema readSchema(AnnotationValue typeAndFormat = ConvertUtils.getTypeAndFormatByClass(type.getName()); + if (elType == null && type instanceof TypedElement) { + TypedElement typedType = (TypedElement) type; + Pair typeAndFormat = ConvertUtils.getTypeAndFormatByClass(typedType.getName(), typedType.isArray()); elType = typeAndFormat.getFirst(); if (elFormat == null) { elFormat = typeAndFormat.getSecond(); @@ -2076,7 +2087,8 @@ protected Schema readSchema(AnnotationValue typeAndFormat = checkEnumJsonValueType(enumEl, elType != null ? elType : PrimitiveType.STRING.getCommonName(), null); + schema.setType(typeAndFormat.getFirst()); if (CollectionUtils.isEmpty(schema.getEnum())) { schema.setEnum(getEnumValues((EnumElement) type, schema.getType(), elFormat, context)); } @@ -2089,22 +2101,19 @@ protected Schema readSchema(AnnotationValue checkEnumJsonValueType(@NonNull EnumElement type, @Nullable String schemaType, @Nullable String schemaFormat) { if (schemaType != null && !schemaType.equals(PrimitiveType.STRING.getCommonName())) { - return schemaType; + return Pair.of(schemaType, schemaFormat); } - PrimitiveType jsonValueType = null; + Pair result = null; // check JsonValue method for (MethodElement method : type.getEnclosedElements(ElementQuery.ALL_METHODS)) { if (method.isAnnotationPresent(JsonValue.class)) { - jsonValueType = PrimitiveType.fromName(method.getReturnType().getName()); + result = ConvertUtils.getTypeAndFormatByClass(method.getReturnType().getName(), method.getReturnType().isArray()); break; } } - if (jsonValueType != null) { - schemaType = jsonValueType.getCommonName(); - } - return schemaType != null ? schemaType : PrimitiveType.STRING.getCommonName(); + return result != null ? result : Pair.of(PrimitiveType.STRING.getCommonName(), schemaFormat); } private List getEnumValues(EnumElement type, String schemaType, String schemaFormat, VisitorContext context) { diff --git a/openapi/src/main/java/io/micronaut/openapi/visitor/ConvertUtils.java b/openapi/src/main/java/io/micronaut/openapi/visitor/ConvertUtils.java index 6b9991f5d6..136cad5e0c 100644 --- a/openapi/src/main/java/io/micronaut/openapi/visitor/ConvertUtils.java +++ b/openapi/src/main/java/io/micronaut/openapi/visitor/ConvertUtils.java @@ -191,10 +191,11 @@ public static SecurityRequirement mapToSecurityRequirement(AnnotationValue getTypeAndFormatByClass(String className) { + public static Pair getTypeAndFormatByClass(String className, boolean isArray) { if (className == null) { return Pair.of("object", null); } @@ -222,9 +223,13 @@ public static Pair getTypeAndFormatByClass(String className) { } else if (Double.class.getName().equals(className) || double.class.getName().equals(className)) { return Pair.of("number", "double"); + } else if (isArray && (Byte.class.getName().equals(className) + || byte.class.getName().equals(className))) { + return Pair.of("string", "byte"); + // swagger doesn't support type byte } else if (Byte.class.getName().equals(className) || byte.class.getName().equals(className)) { - return Pair.of("string", "byte"); + return Pair.of("integer", "int32"); } else if (BigDecimal.class.getName().equals(className)) { return Pair.of("number", null); } else if (URI.class.getName().equals(className)) { diff --git a/openapi/src/test/groovy/io/micronaut/openapi/visitor/OpenApiEnumSpec.groovy b/openapi/src/test/groovy/io/micronaut/openapi/visitor/OpenApiEnumSpec.groovy index 643a108515..69fb22bd3b 100644 --- a/openapi/src/test/groovy/io/micronaut/openapi/visitor/OpenApiEnumSpec.groovy +++ b/openapi/src/test/groovy/io/micronaut/openapi/visitor/OpenApiEnumSpec.groovy @@ -534,4 +534,145 @@ class MyBean {} schema.enum.get(0) == 'AWS' schema.enum.get(1) == 'AZURE' } + + void "test build OpenAPI enum Schema with byte type values"() { + + when: + buildBeanDefinition('test.MyBean', ''' +package test; + +import java.util.List; + +import io.micronaut.http.annotation.Controller; +import io.micronaut.http.annotation.Post; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.annotation.JsonValue; + +@Controller +class OpenApiController { + + @Post + public void postRaw(DictionaryRequest request) { + } +} + +class DictionaryRequest { + + protected Type1 type1; + protected Type2 type2; + + public Type1 getType1() { + return type1; + } + + public void setType1(Type1 type1) { + this.type1 = type1; + } + + public Type2 getType2() { + return type2; + } + + public void setType2(Type2 type2) { + this.type2 = type2; + } +} + +enum Type1 { + + @JsonProperty("1") + _1(((byte) 1)), + @JsonProperty("2") + _2(((byte) 2)); + + private final byte value; + + Type1(byte value) { + this.value = value; + } + + @JsonValue + public byte value() { + return value; + } + + @JsonCreator + public static Type1 fromValue(byte v) { + for (Type1 c : values()) { + if (c.value == v) { + return c; + } + } + throw new IllegalArgumentException(String.valueOf(v)); + } +} + +enum Type2 { + + @JsonProperty("111") + _1("111"), + @JsonProperty("222") + _2("222"); + + private final byte[] value; + + Type2(String value) { + this.value = value.getBytes(); + } + + @JsonValue + public byte[] value() { + return value; + } + + @JsonCreator + public static Type2 fromValue(byte[] v) { + for (Type2 c : values()) { + if (c.value == v) { + return c; + } + } + throw new IllegalArgumentException(String.valueOf(v)); + } +} + +@jakarta.inject.Singleton +class MyBean {} +''') + then: "the state is correct" + Utils.testReferenceAfterPlaceholders != null + + when: "The OpenAPI is retrieved" + OpenAPI openAPI = Utils.testReferenceAfterPlaceholders + + then: "the state is correct" + openAPI.components + openAPI.components.schemas + openAPI.components.schemas.size() == 3 + + when: + Schema requestSchema = openAPI.components.schemas['DictionaryRequest'] + Schema type1Schema = openAPI.components.schemas['Type1'] + Schema type2Schema = openAPI.components.schemas['Type2'] + + then: "the components are valid" + requestSchema + type1Schema + type1Schema.enum + type1Schema.enum.size() == 2 + type1Schema.type == 'integer' + type1Schema.format == 'int32' + type1Schema.enum.contains(1) + type1Schema.enum.contains(2) + + type2Schema + type2Schema.enum + type2Schema.enum.size() == 2 + type2Schema.type == 'string' + type2Schema.format == 'byte' + type2Schema.enum.contains("111") + type2Schema.enum.contains("222") + } } diff --git a/openapi/src/test/groovy/io/micronaut/openapi/visitor/OpenApiIncludeVisitorSpec.groovy b/openapi/src/test/groovy/io/micronaut/openapi/visitor/OpenApiIncludeVisitorSpec.groovy index cb991d0d8a..21d08bbfb1 100644 --- a/openapi/src/test/groovy/io/micronaut/openapi/visitor/OpenApiIncludeVisitorSpec.groovy +++ b/openapi/src/test/groovy/io/micronaut/openapi/visitor/OpenApiIncludeVisitorSpec.groovy @@ -102,7 +102,7 @@ class MyBean {} loginPathItem.post.requestBody.content['application/x-www-form-urlencoded'].schema['$ref'] == '#/components/schemas/UsernamePasswordCredentials' loginPathItem.post.requestBody.content['application/json'].schema loginPathItem.post.requestBody.content['application/json'].schema['$ref'] == '#/components/schemas/UsernamePasswordCredentials' - loginPathItem.post.responses['200'].content['application/json'].schema['$ref'] == '#/components/schemas/Object' + loginPathItem.post.responses['200'].content['application/json'].schema.type == 'object' openAPI.components.schemas['UsernamePasswordCredentials'] openAPI.components.schemas['UsernamePasswordCredentials'].required.size() == 2 openAPI.components.schemas['UsernamePasswordCredentials'].properties['username'] @@ -202,7 +202,7 @@ class MyBean {} loginPathItem.post.requestBody.content['application/x-www-form-urlencoded'].schema['$ref'] == '#/components/schemas/UsernamePasswordCredentials' loginPathItem.post.requestBody.content['application/json'].schema loginPathItem.post.requestBody.content['application/json'].schema['$ref'] == '#/components/schemas/UsernamePasswordCredentials' - loginPathItem.post.responses['200'].content['application/json'].schema['$ref'] == '#/components/schemas/Object' + loginPathItem.post.responses['200'].content['application/json'].schema.type == 'object' openAPI.components.schemas['UsernamePasswordCredentials'] openAPI.components.schemas['UsernamePasswordCredentials'].required.size() == 2 @@ -315,17 +315,17 @@ class MyBean {} loginPathItem.post.requestBody.content['application/x-www-form-urlencoded'].schema['$ref'] == '#/components/schemas/UsernamePasswordCredentials' loginPathItem.post.requestBody.content['application/json'].schema loginPathItem.post.requestBody.content['application/json'].schema['$ref'] == '#/components/schemas/UsernamePasswordCredentials' - loginPathItem.post.responses['200'].content['application/json'].schema['$ref'] == '#/components/schemas/Object' + loginPathItem.post.responses['200'].content['application/json'].schema.type == 'object' logoutPathItem.post.operationId == 'index' logoutPathItem.post.tags[0] == "Tag 5" logoutPathItem.post.security[0]["req 3"] - logoutPathItem.post.responses['200'].content['application/json'].schema['$ref'] == '#/components/schemas/Object' + logoutPathItem.post.responses['200'].content['application/json'].schema.type == 'object' logoutPathItem.get.operationId == 'indexGet' logoutPathItem.get.tags[0] == "Tag 5" logoutPathItem.get.security[0]["req 3"] - logoutPathItem.get.responses['200'].content['application/json'].schema['$ref'] == '#/components/schemas/Object' + logoutPathItem.get.responses['200'].content['application/json'].schema.type == 'object' openAPI.components.schemas['UsernamePasswordCredentials'] openAPI.components.schemas['UsernamePasswordCredentials'].required.size() == 2 diff --git a/openapi/src/test/groovy/io/micronaut/openapi/visitor/OpenApiPojoControllerSpec.groovy b/openapi/src/test/groovy/io/micronaut/openapi/visitor/OpenApiPojoControllerSpec.groovy index 82b89a7dfe..6d4490691e 100644 --- a/openapi/src/test/groovy/io/micronaut/openapi/visitor/OpenApiPojoControllerSpec.groovy +++ b/openapi/src/test/groovy/io/micronaut/openapi/visitor/OpenApiPojoControllerSpec.groovy @@ -313,7 +313,7 @@ class Tag { private String name; private String description; - public Tag(String name, String description) { + Tag(String name, String description) { this.name = name; this.description = description; } diff --git a/openapi/src/test/groovy/io/micronaut/openapi/visitor/OpenApiPropsFromEnvSpec.groovy b/openapi/src/test/groovy/io/micronaut/openapi/visitor/OpenApiPropsFromEnvSpec.groovy index 08d44bc7b6..f6acfa3067 100644 --- a/openapi/src/test/groovy/io/micronaut/openapi/visitor/OpenApiPropsFromEnvSpec.groovy +++ b/openapi/src/test/groovy/io/micronaut/openapi/visitor/OpenApiPropsFromEnvSpec.groovy @@ -200,17 +200,17 @@ class MyBean {} loginPathItem.post.requestBody.content['application/x-www-form-urlencoded'].schema['$ref'] == '#/components/schemas/UsernamePasswordCredentials' loginPathItem.post.requestBody.content['application/json'].schema loginPathItem.post.requestBody.content['application/json'].schema['$ref'] == '#/components/schemas/UsernamePasswordCredentials' - loginPathItem.post.responses['200'].content['application/json'].schema['$ref'] == '#/components/schemas/Object' + loginPathItem.post.responses['200'].content['application/json'].schema.type == 'object' logoutPathItem.post.operationId == 'index' logoutPathItem.post.tags[0] == "Tag 5" logoutPathItem.post.security[0]["req 3"] - logoutPathItem.post.responses['200'].content['application/json'].schema['$ref'] == '#/components/schemas/Object' + loginPathItem.post.responses['200'].content['application/json'].schema.type == 'object' logoutPathItem.get.operationId == 'indexGet' logoutPathItem.get.tags[0] == "Tag 5" logoutPathItem.get.security[0]["req 3"] - logoutPathItem.get.responses['200'].content['application/json'].schema['$ref'] == '#/components/schemas/Object' + loginPathItem.post.responses['200'].content['application/json'].schema.type == 'object' openAPI.components.schemas['UsernamePasswordCredentials'] openAPI.components.schemas['UsernamePasswordCredentials'].required.size() == 2