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 4c07677cb9..c85900bb22 100644 --- a/openapi/src/main/java/io/micronaut/openapi/visitor/ConvertUtils.java +++ b/openapi/src/main/java/io/micronaut/openapi/visitor/ConvertUtils.java @@ -85,6 +85,7 @@ import static io.micronaut.openapi.OpenApiUtils.CONVERT_JSON_MAPPER; import static io.micronaut.openapi.OpenApiUtils.JSON_MAPPER; import static io.micronaut.openapi.visitor.ContextUtils.warn; +import static io.micronaut.openapi.visitor.ElementUtils.isEnum; import static io.micronaut.openapi.visitor.OpenApiModelProp.PROP_BEARER_FORMAT; import static io.micronaut.openapi.visitor.OpenApiModelProp.PROP_CONTENT; import static io.micronaut.openapi.visitor.OpenApiModelProp.PROP_FLOWS; @@ -596,7 +597,7 @@ public static SecurityRequirement mapToSecurityRequirement(AnnotationValue schema, String defaultValue, @Nullable Element element, @Nullable String schemaType, @Nullable String schemaFormat, boolean isMicronautFormat, VisitorContext context) { try { Pair typeAndFormat; - if (element instanceof EnumElement enumEl) { + if (element instanceof EnumElement enumEl && isEnum(enumEl)) { typeAndFormat = ConvertUtils.checkEnumJsonValueType(context, enumEl, schemaType, schemaFormat); } else { typeAndFormat = Pair.of(schemaType, schemaFormat); @@ -649,7 +650,7 @@ public static Pair checkEnumJsonValueType(VisitorContext context Pair result = null; if (firstMethod != null) { ClassElement returnType = firstMethod.getReturnType(); - if (returnType.isEnum()) { + if (isEnum(returnType)) { return checkEnumJsonValueType(context, (EnumElement) returnType, null, null); } result = ConvertUtils.getTypeAndFormatByClass(returnType.getName(), returnType.isArray(), returnType); @@ -661,7 +662,7 @@ public static Pair checkEnumJsonValueType(VisitorContext context if (CollectionUtils.isNotEmpty(fields)) { var firstField = fields.get(0); ClassElement fieldType = firstField.getType(); - if (fieldType.isEnum()) { + if (isEnum(fieldType)) { return checkEnumJsonValueType(context, (EnumElement) fieldType, null, null); } result = ConvertUtils.getTypeAndFormatByClass(fieldType.getName(), fieldType.isArray(), fieldType); diff --git a/openapi/src/main/java/io/micronaut/openapi/visitor/ElementUtils.java b/openapi/src/main/java/io/micronaut/openapi/visitor/ElementUtils.java index 63fe2c3970..0ef8100482 100644 --- a/openapi/src/main/java/io/micronaut/openapi/visitor/ElementUtils.java +++ b/openapi/src/main/java/io/micronaut/openapi/visitor/ElementUtils.java @@ -16,6 +16,7 @@ package io.micronaut.openapi.visitor; import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonFormat; import com.fasterxml.jackson.annotation.JsonIgnore; import com.fasterxml.jackson.annotation.JsonProperty; import com.fasterxml.jackson.annotation.JsonView; @@ -470,4 +471,17 @@ public static boolean isTypeWithGenericNullable(ClassElement type) { || type.isAssignable(AtomicReference.class) ; } + + public static boolean isEnum(ClassElement classElement) { + + var isEnum = classElement.isEnum(); + + var jsonFormatAnn = classElement.getAnnotation(JsonFormat.class); + if (jsonFormatAnn == null) { + return isEnum; + } + + var jsonShape = jsonFormatAnn.get("shape", JsonFormat.Shape.class).orElse(JsonFormat.Shape.ANY); + return jsonShape != JsonFormat.Shape.OBJECT && isEnum; + } } diff --git a/openapi/src/main/java/io/micronaut/openapi/visitor/SchemaDefinitionUtils.java b/openapi/src/main/java/io/micronaut/openapi/visitor/SchemaDefinitionUtils.java index c4881a0f6a..f9f17281aa 100644 --- a/openapi/src/main/java/io/micronaut/openapi/visitor/SchemaDefinitionUtils.java +++ b/openapi/src/main/java/io/micronaut/openapi/visitor/SchemaDefinitionUtils.java @@ -137,6 +137,7 @@ import static io.micronaut.openapi.visitor.ElementUtils.getAnnotation; import static io.micronaut.openapi.visitor.ElementUtils.getAnnotationMetadata; import static io.micronaut.openapi.visitor.ElementUtils.isAnnotationPresent; +import static io.micronaut.openapi.visitor.ElementUtils.isEnum; import static io.micronaut.openapi.visitor.ElementUtils.isFileUpload; import static io.micronaut.openapi.visitor.ElementUtils.isNotNullable; import static io.micronaut.openapi.visitor.ElementUtils.isNullable; @@ -274,7 +275,7 @@ public static Schema readSchema(AnnotationValue typeAndFormat; - if (classEl instanceof EnumElement enumEl) { + if (classEl instanceof EnumElement enumEl && isEnum(enumEl)) { typeAndFormat = ConvertUtils.checkEnumJsonValueType(context, enumEl, null, elFormat); } else { typeAndFormat = ConvertUtils.getTypeAndFormatByClass(classEl.getName(), classEl.isArray(), classEl); @@ -287,7 +288,7 @@ public static Schema readSchema(AnnotationValue getSchemaDefinition(OpenAPI openAPI, JavadocDescription javadoc = Utils.getJavadocParser().parse(type.getDocumentation().orElse(null)); if (schema == null) { - if (type instanceof EnumElement enumEl) { + if (type instanceof EnumElement enumEl && isEnum(enumEl)) { schema = setSpecVersion(new Schema<>()); schema.setName(schemaName); processJacksonDescription(enumEl, schema); @@ -677,7 +678,7 @@ public static Schema resolveSchema(OpenAPI openApi, @Nullable Element definin Schema schema = null; - if (type instanceof EnumElement enumEl) { + if (type instanceof EnumElement enumEl && isEnum(enumEl)) { schema = getSchemaDefinition(openApi, context, enumEl, typeArgs, definingElement, mediaTypes, jsonViewClass); if (isArray != null && isArray) { schema = SchemaUtils.arraySchema(schema); @@ -1408,7 +1409,7 @@ public static Schema bindSchemaAnnotationValue(VisitorContext context, TypedE Pair typeAndFormat; if (classEl.isIterable()) { typeAndFormat = Pair.of(TYPE_ARRAY, null); - } else if (classEl instanceof EnumElement enumEl) { + } else if (classEl instanceof EnumElement enumEl && isEnum(enumEl)) { typeAndFormat = ConvertUtils.checkEnumJsonValueType(context, enumEl, null, null); } else { typeAndFormat = ConvertUtils.getTypeAndFormatByClass(classEl.getName(), classEl.isArray(), classEl); @@ -1515,7 +1516,7 @@ public static void processSchemaProperty(VisitorContext context, TypedElement el String elFormat = schemaAnn.stringValue(PROP_ONE_FORMAT).orElse(null); if (elType == null && elementType != null) { Pair typeAndFormat; - if (elementType instanceof EnumElement enumEl) { + if (elementType instanceof EnumElement enumEl && isEnum(enumEl)) { typeAndFormat = ConvertUtils.checkEnumJsonValueType(context, enumEl, null, elFormat); } else { typeAndFormat = ConvertUtils.getTypeAndFormatByClass(elementType.getName(), elementType.isArray(), elementType); @@ -1636,6 +1637,7 @@ private static Schema processSuperTypes(Schema schema, } else { var superType = classElement.getSuperType().orElse(null); if (superType != null + && !Enum.class.getName().equals(superType.getName()) // check protobuf generated classes && !isProtobufMessageClass(superType)) { superTypes.add(superType); @@ -1896,7 +1898,7 @@ private static void processSchemaAnn(Schema schemaToBind, VisitorContext context if (element instanceof ClassElement classElement) { if (classElement.isIterable()) { typeAndFormat = Pair.of(TYPE_ARRAY, null); - } else if (element instanceof EnumElement enumEl) { + } else if (element instanceof EnumElement enumEl && isEnum(enumEl)) { typeAndFormat = ConvertUtils.checkEnumJsonValueType(context, enumEl, null, null); } else { typeAndFormat = ConvertUtils.getTypeAndFormatByClass(classElement.getName(), classElement.isArray(), classElement); @@ -2401,7 +2403,7 @@ private static Schema processMapSchema(ClassElement type, Map doBindSchemaAnnotationValue(VisitorContext context, Typ if (elType == null && element != null) { ClassElement typeEl = element.getType(); Pair typeAndFormat; - if (typeEl instanceof EnumElement enumEl) { + if (typeEl instanceof EnumElement enumEl && isEnum(enumEl)) { typeAndFormat = ConvertUtils.checkEnumJsonValueType(context, enumEl, null, elFormat); } else { typeAndFormat = ConvertUtils.getTypeAndFormatByClass(typeEl.getName(), typeEl.isArray(), typeEl); @@ -2842,6 +2844,9 @@ private static void handleUnwrapped(VisitorContext context, Element element, Cla private static boolean doesParamExistsMandatoryInConstructor(Element element, @Nullable Element classElement) { if (classElement instanceof ClassElement classEl) { + if (classEl.isEnum()) { + return true; + } return classEl.getPrimaryConstructor().flatMap(methodElement -> Arrays.stream(methodElement.getParameters()) .filter(parameterElement -> parameterElement.getName().equals(element.getName())) .map(parameterElement -> !parameterElement.isNullable()) diff --git a/openapi/src/test/groovy/io/micronaut/openapi/visitor/OpenApiSchemaFieldSpec.groovy b/openapi/src/test/groovy/io/micronaut/openapi/visitor/OpenApiSchemaFieldSpec.groovy index e20b91c0dc..e29d8c6b74 100644 --- a/openapi/src/test/groovy/io/micronaut/openapi/visitor/OpenApiSchemaFieldSpec.groovy +++ b/openapi/src/test/groovy/io/micronaut/openapi/visitor/OpenApiSchemaFieldSpec.groovy @@ -1341,4 +1341,65 @@ class MyBean {} schemas.ResourceSearchVO.properties.type2.default == "VAL2" schemas.ResourceSearchVO.properties.type3.$ref == '#/components/schemas/ResourceEnum' } + + void "test JsonFormat on enum"() { + + when: + buildBeanDefinition('test.MyBean', ''' +package test; + +import com.fasterxml.jackson.annotation.JsonFormat; +import com.fasterxml.jackson.annotation.JsonProperty;import io.micronaut.core.annotation.Nullable; +import io.micronaut.http.annotation.Controller; +import io.micronaut.http.annotation.Get; +import io.micronaut.http.annotation.QueryValue; + +@Controller +class HelloController { + + @Get("/foo") + User getFoo(@QueryValue @Nullable User user) { + return null; + } +} + +@JsonFormat(shape = JsonFormat.Shape.OBJECT) +enum User { + + FOO(1, "Foo"), + BAR(2, "Bar"); + + @JsonProperty("id") + public final int id; + + @JsonProperty("name") + public final String name; + + User(int id, String name) { + this.id = id; + this.name = name; + } +} + +@jakarta.inject.Singleton +class MyBean {} +''') + then: "the state is correct" + Utils.testReference != null + + when: "The OpenAPI is retrieved" + def openApi = Utils.testReference + def schemas = openApi.components.schemas + + then: "the components are valid" + schemas.User + schemas.User.type == 'object' + schemas.User.required + schemas.User.required.size() == 2 + schemas.User.required[0] == 'id' + schemas.User.required[1] == 'name' + schemas.User.properties.id.type == "integer" + schemas.User.properties.id.format == "int32" + schemas.User.properties.name.type == "string" + } }