Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -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;
Expand Down Expand Up @@ -336,8 +337,10 @@ public Schema resolve(AnnotatedType annotatedType, ModelConverterContext context
if (model == null && type.isEnumType()) {
@SuppressWarnings("unchecked")
Class<Enum<?>> rawEnumClass = (Class<Enum<?>>) 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())) {
Expand Down Expand Up @@ -1334,6 +1337,9 @@ protected boolean _isOptionalType(JavaType propType) {
protected void _addEnumProps(Class<?> propClass, Schema property) {
if (propClass.isEnum()) {
Class<Enum<?>> rawEnumClass = (Class<Enum<?>>) propClass;
if (isEnumSerializedAsObject(rawEnumClass)) {
return;
}
Schema enumSchema = _createSchemaForEnum(rawEnumClass);
if (enumSchema != null) {
property.setEnum(enumSchema.getEnum());
Expand Down Expand Up @@ -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.
* <p>
* 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<Enum<?>> enumClass) {
JsonFormat jsonFormat = enumClass.getAnnotation(JsonFormat.class);
return jsonFormat != null && jsonFormat.shape() == JsonFormat.Shape.OBJECT;
}
}
Original file line number Diff line number Diff line change
@@ -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;
Expand Down Expand Up @@ -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<String, Schema> 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<String, Schema> 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);
Expand Down Expand Up @@ -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;
Expand Down