diff --git a/modules/swagger-core/src/main/java/io/swagger/v3/core/jackson/ModelResolver.java b/modules/swagger-core/src/main/java/io/swagger/v3/core/jackson/ModelResolver.java index b281d34855..28b52fb4f4 100644 --- a/modules/swagger-core/src/main/java/io/swagger/v3/core/jackson/ModelResolver.java +++ b/modules/swagger-core/src/main/java/io/swagger/v3/core/jackson/ModelResolver.java @@ -1,5 +1,6 @@ package io.swagger.v3.core.jackson; +import com.fasterxml.jackson.annotation.JsonFormat; import com.fasterxml.jackson.annotation.JsonIdentityInfo; import com.fasterxml.jackson.annotation.JsonIdentityReference; import com.fasterxml.jackson.annotation.JsonIgnore; @@ -336,8 +337,10 @@ public Schema resolve(AnnotatedType annotatedType, ModelConverterContext context if (model == null && type.isEnumType()) { @SuppressWarnings("unchecked") Class> rawEnumClass = (Class>) type.getRawClass(); - model = _createSchemaForEnum(rawEnumClass); - isPrimitive = true; + if (!isEnumSerializedAsObject(rawEnumClass)) { + model = _createSchemaForEnum(rawEnumClass); + isPrimitive = true; + } } if (model == null) { if (resolvedSchemaAnnotation != null && StringUtils.isEmpty(resolvedSchemaAnnotation.type())) { @@ -1334,6 +1337,9 @@ protected boolean _isOptionalType(JavaType propType) { protected void _addEnumProps(Class propClass, Schema property) { if (propClass.isEnum()) { Class> rawEnumClass = (Class>) propClass; + if (isEnumSerializedAsObject(rawEnumClass)) { + return; + } Schema enumSchema = _createSchemaForEnum(rawEnumClass); if (enumSchema != null) { property.setEnum(enumSchema.getEnum()); @@ -3691,4 +3697,21 @@ private boolean isExplicitObjectType() { ? Boolean.TRUE.equals(PrimitiveType.explicitObjectType) : !Boolean.FALSE.equals(PrimitiveType.explicitObjectType); } + + /** + * Determines whether the given enum is serialized as a JSON object rather than as a scalar + * enum value. + *

+ * This is the case when the enum is annotated with + * {@code @JsonFormat(shape = JsonFormat.Shape.OBJECT)}, which instructs Jackson to serialize + * each enum constant as a POJO using its bean properties (getters) instead of a single scalar + * value. Such an enum is resolved as a regular object/bean schema instead of an enum schema. + * + * @param enumClass the enum class to inspect + * @return {@code true} if the enum is configured to be serialized as a JSON object, {@code false} otherwise + */ + private boolean isEnumSerializedAsObject(Class> enumClass) { + JsonFormat jsonFormat = enumClass.getAnnotation(JsonFormat.class); + return jsonFormat != null && jsonFormat.shape() == JsonFormat.Shape.OBJECT; + } } diff --git a/modules/swagger-core/src/test/java/io/swagger/v3/core/resolving/EnumTest.java b/modules/swagger-core/src/test/java/io/swagger/v3/core/resolving/EnumTest.java index 6f2f552353..4bf1659667 100644 --- a/modules/swagger-core/src/test/java/io/swagger/v3/core/resolving/EnumTest.java +++ b/modules/swagger-core/src/test/java/io/swagger/v3/core/resolving/EnumTest.java @@ -1,5 +1,6 @@ package io.swagger.v3.core.resolving; +import com.fasterxml.jackson.annotation.JsonFormat; import com.google.common.base.Functions; import com.google.common.collect.Collections2; import io.swagger.v3.core.converter.AnnotatedType; @@ -51,6 +52,101 @@ public void testEnumGenerics() { assertPropertyExists(model, "type"); } + @Test + public void testEnumWithJsonFormatObjectShape() { + final ModelResolver modelResolver = new ModelResolver(mapper()); + final ModelConverterContextImpl context = new ModelConverterContextImpl(modelResolver); + + final Schema model = context.resolve(new AnnotatedType().type(ObjectShapeEnum.class)); + assertNotNull(model); + // Enum annotated with @JsonFormat(shape = OBJECT) is resolved as an object/bean, not as an enum + assertNull(model.getEnum()); + assertNotNull(model.getProperties()); + assertTrue(model.getProperties().containsKey("code")); + assertTrue(model.getProperties().containsKey("label")); + } + + @Test + public void testEnumWithJsonFormatObjectShapePropertyTypes() { + final ModelResolver modelResolver = new ModelResolver(mapper()); + final ModelConverterContextImpl context = new ModelConverterContextImpl(modelResolver); + + final Schema model = context.resolve(new AnnotatedType().type(ObjectShapeEnum.class)); + assertNotNull(model); + assertNull(model.getEnum()); + assertNull(model.get$ref()); + + final Schema codeSchema = (Schema) model.getProperties().get("code"); + final Schema labelSchema = (Schema) model.getProperties().get("label"); + assertNotNull(codeSchema); + assertNotNull(labelSchema); + assertEquals(codeSchema.getType(), "integer"); + assertEquals(labelSchema.getType(), "string"); + // property schemas must not carry enum values + assertNull(codeSchema.getEnum()); + assertNull(labelSchema.getEnum()); + } + + @Test + public void testEnumWithJsonFormatObjectShapeOpenApi31() { + final ModelResolver modelResolver = new ModelResolver(mapper()).openapi31(true); + final ModelConverterContextImpl context = new ModelConverterContextImpl(modelResolver); + + final Schema model = context.resolve(new AnnotatedType().type(ObjectShapeEnum.class)); + assertNotNull(model); + assertNull(model.getEnum()); + assertNotNull(model.getProperties()); + assertTrue(model.getProperties().containsKey("code")); + assertTrue(model.getProperties().containsKey("label")); + } + + @Test + public void testEnumWithJsonFormatObjectShapeAsField() { + final ModelResolver modelResolver = new ModelResolver(mapper()); + final ModelConverterContextImpl context = new ModelConverterContextImpl(modelResolver); + + final Schema model = context.resolve(new AnnotatedType().type(ClassWithObjectShapeEnum.class)); + assertBasicModelStructure(model, "ClassWithObjectShapeEnum"); + assertPropertyExists(model, "status"); + + final Schema statusProp = (Schema) model.getProperties().get("status"); + // resolved as a ref to the object-shape enum component, like a normal bean + assertNotNull(statusProp.get$ref()); + assertNull(statusProp.getEnum()); + + // the referenced component must be an object with properties, not an enum + Map components = context.getDefinedModels(); + Schema component = components.get("ObjectShapeEnum"); + assertNotNull(component); + assertNull(component.getEnum()); + assertNotNull(component.getProperties()); + assertTrue(component.getProperties().containsKey("code")); + assertTrue(component.getProperties().containsKey("label")); + } + + @Test + public void testArrayOfEnumWithJsonFormatObjectShape() { + final ModelResolver modelResolver = new ModelResolver(mapper()); + final ModelConverterContextImpl context = new ModelConverterContextImpl(modelResolver); + + final Schema model = context.resolve(new AnnotatedType().type(ClassWithObjectShapeEnumArray.class)); + assertBasicModelStructure(model, "ClassWithObjectShapeEnumArray"); + assertPropertyExists(model, "statuses"); + + final Schema arrayProp = (Schema) model.getProperties().get("statuses"); + assertNotNull(arrayProp.getItems()); + // array items reference the object-shape enum component, and are not an inline enum + assertNotNull(arrayProp.getItems().get$ref()); + assertNull(arrayProp.getItems().getEnum()); + + Map components = context.getDefinedModels(); + Schema component = components.get("ObjectShapeEnum"); + assertNotNull(component); + assertNull(component.getEnum()); + assertTrue(component.getProperties().containsKey("code")); + assertTrue(component.getProperties().containsKey("label")); + } + @Test public void testEnumPropertyWithSchemaAnnotation() { final ModelResolver modelResolver = new ModelResolver(mapper()).openapi31(true); @@ -309,6 +405,36 @@ public enum Currency { USA, CANADA } + @JsonFormat(shape = JsonFormat.Shape.OBJECT) + public enum ObjectShapeEnum { + FIRST(1, "one"), + SECOND(2, "two"); + + private final int code; + private final String label; + + ObjectShapeEnum(int code, String label) { + this.code = code; + this.label = label; + } + + public int getCode() { + return code; + } + + public String getLabel() { + return label; + } + } + + public static class ClassWithObjectShapeEnum { + public ObjectShapeEnum status; + } + + public static class ClassWithObjectShapeEnumArray { + public ObjectShapeEnum[] statuses; + } + public static class Contract { private Enum type;