Skip to content

Commit 5e0bb63

Browse files
committed
fix: reattach enumAsRef to Schema and ArraySchema for OAS 3.1 and OAS 3.0 ALL_OF. Fixes #4932
1 parent 0d0d2e3 commit 5e0bb63

File tree

4 files changed

+265
-19
lines changed

4 files changed

+265
-19
lines changed

modules/swagger-core/src/main/java/io/swagger/v3/core/converter/AnnotatedType.java

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ public class AnnotatedType {
2121
private boolean schemaProperty;
2222
private Annotation[] ctxAnnotations;
2323
private boolean resolveAsRef;
24+
private boolean resolveEnumAsRef;
2425
private JsonView jsonViewAnnotation;
2526
private boolean includePropertiesWithoutJSONView = true;
2627
private boolean skipSchemaName;
@@ -88,6 +89,19 @@ public AnnotatedType resolveAsRef(boolean resolveAsRef) {
8889
return this;
8990
}
9091

92+
public boolean isResolveEnumAsRef() {
93+
return resolveEnumAsRef;
94+
}
95+
96+
public void setResolveEnumAsRef(boolean resolveEnumAsRef) {
97+
this.resolveEnumAsRef = resolveEnumAsRef;
98+
}
99+
100+
public AnnotatedType resolveEnumAsRef(boolean resolveEnumAsRef) {
101+
this.resolveEnumAsRef = resolveEnumAsRef;
102+
return this;
103+
}
104+
91105
public boolean isSchemaProperty() {
92106
return schemaProperty;
93107
}

modules/swagger-core/src/main/java/io/swagger/v3/core/jackson/ModelResolver.java

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -411,7 +411,7 @@ public Schema resolve(AnnotatedType annotatedType, ModelConverterContext context
411411
schema.setItems(model);
412412
return schema;
413413
}
414-
if (type.isEnumType() && shouldResolveEnumAsRef(resolvedSchemaAnnotation)) {
414+
if (type.isEnumType() && shouldResolveEnumAsRef(resolvedSchemaAnnotation, annotatedType.isResolveEnumAsRef())) {
415415
// Store off the ref and add the enum as a top-level model
416416
context.defineModel(name, model, annotatedType, null);
417417
// Return the model as a ref only property
@@ -540,6 +540,7 @@ public Schema resolve(AnnotatedType annotatedType, ModelConverterContext context
540540
.propertyName(annotatedType.getPropertyName())
541541
.jsonViewAnnotation(annotatedType.getJsonViewAnnotation())
542542
.components(annotatedType.getComponents())
543+
.resolveEnumAsRef(annotatedType.isResolveEnumAsRef())
543544
.parent(annotatedType.getParent()));
544545

545546
if (items == null) {
@@ -794,7 +795,8 @@ public Schema resolve(AnnotatedType annotatedType, ModelConverterContext context
794795
.skipSchemaName(true)
795796
.schemaProperty(true)
796797
.components(annotatedType.getComponents())
797-
.propertyName(propName);
798+
.propertyName(propName)
799+
.resolveEnumAsRef(AnnotationsUtils.computeEnumAsRef(ctxSchema, ctxArraySchema));
798800
if (
799801
Schema.SchemaResolution.ALL_OF.equals(resolvedSchemaResolution) ||
800802
Schema.SchemaResolution.ALL_OF_REF.equals(resolvedSchemaResolution) ||
@@ -1239,8 +1241,8 @@ private Stream<Annotation> getGenericTypeArgumentAnnotations(java.lang.reflect.A
12391241
.orElseGet(Stream::of);
12401242
}
12411243

1242-
private boolean shouldResolveEnumAsRef(io.swagger.v3.oas.annotations.media.Schema resolvedSchemaAnnotation) {
1243-
return (resolvedSchemaAnnotation != null && resolvedSchemaAnnotation.enumAsRef()) || ModelResolver.enumsAsRef;
1244+
private boolean shouldResolveEnumAsRef(io.swagger.v3.oas.annotations.media.Schema resolvedSchemaAnnotation, boolean isResolveEnumAsRef) {
1245+
return (resolvedSchemaAnnotation != null && resolvedSchemaAnnotation.enumAsRef()) || ModelResolver.enumsAsRef || isResolveEnumAsRef;
12441246
}
12451247

12461248
protected Type findJsonValueType(final BeanDescription beanDesc) {

modules/swagger-core/src/main/java/io/swagger/v3/core/util/AnnotationsUtils.java

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2907,4 +2907,13 @@ public static Schema.SchemaResolution resolveSchemaResolution(Schema.SchemaResol
29072907
}
29082908
return globalSchemaResolution;
29092909
}
2910+
2911+
public static boolean computeEnumAsRef(io.swagger.v3.oas.annotations.media.Schema ctxSchema, io.swagger.v3.oas.annotations.media.ArraySchema ctxArraySchema) {
2912+
if (ctxSchema != null && ctxSchema.enumAsRef()) {
2913+
return ctxSchema.enumAsRef();
2914+
} else if(ctxArraySchema != null && ctxArraySchema.schema() != null && ctxArraySchema.schema().enumAsRef()) {
2915+
return ctxArraySchema.schema().enumAsRef();
2916+
}
2917+
return false;
2918+
}
29102919
}

modules/swagger-core/src/test/java/io/swagger/v3/core/resolving/EnumTest.java

Lines changed: 236 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -9,13 +9,11 @@
99
import io.swagger.v3.oas.models.media.StringSchema;
1010
import org.testng.annotations.Test;
1111

12-
import java.util.ArrayList;
13-
import java.util.Arrays;
14-
import java.util.Collection;
12+
import java.util.*;
13+
import java.util.stream.Collectors;
1514

16-
import static org.testng.Assert.assertEquals;
17-
import static org.testng.Assert.assertNotNull;
18-
import static org.testng.Assert.assertTrue;
15+
import static org.testng.Assert.*;
16+
import static org.testng.AssertJUnit.assertFalse;
1917

2018
public class EnumTest extends SwaggerTestBase {
2119

@@ -33,7 +31,6 @@ public void testEnum() {
3331
new ArrayList<String>(Collections2.transform(Arrays.asList(Currency.values()), Functions.toStringFunction()));
3432
assertEquals(strModel.getEnum(), modelValues);
3533

36-
3734
final Schema property = context.resolve(new AnnotatedType().type(Currency.class).schemaProperty(true));
3835
assertNotNull(property);
3936
assertTrue(property instanceof StringSchema);
@@ -50,10 +47,8 @@ public void testEnumGenerics() {
5047
final ModelConverterContextImpl context = new ModelConverterContextImpl(modelResolver);
5148

5249
final Schema model = context.resolve((new AnnotatedType().type(Contract.class)));
53-
assertNotNull(model);
54-
assertEquals(model.getName(), "Contract");
55-
assertTrue(model.getProperties().containsKey("type"));
56-
assertNotNull(model.getProperties().get("type"));
50+
assertBasicModelStructure(model, "Contract");
51+
assertPropertyExists(model, "type");
5752
}
5853

5954
@Test
@@ -62,28 +57,254 @@ public void testEnumPropertyWithSchemaAnnotation() {
6257
final ModelConverterContextImpl context = new ModelConverterContextImpl(modelResolver);
6358

6459
final Schema model = context.resolve(new AnnotatedType().type(ClassWithEnumAsRefProperty.class));
65-
assertNotNull(model);
66-
assertEquals(model.getName(), "ClassWithEnumAsRefProperty");
67-
assertTrue(model.getProperties().containsKey("enumWithSchemaProperty"));
60+
assertBasicModelStructure(model, "ClassWithEnumAsRefProperty");
61+
assertPropertyExists(model, "enumWithSchemaProperty");
62+
6863
final Schema enumPropertySchema = (Schema) model.getProperties().get("enumWithSchemaProperty");
64+
assertEnumAsRefProperty(enumPropertySchema, "#/components/schemas/EnumWithSchemaProperty");
65+
assertEquals(enumPropertySchema.getDescription(), "Property description");
66+
}
67+
68+
@Test
69+
public void testEnumPropertyWithGlobalSwitchOnlyOpenApi31() {
70+
ModelResolver.enumsAsRef = true;
71+
final ModelResolver modelResolver = new ModelResolver(mapper()).openapi31(true);
72+
final ModelConverterContextImpl context = new ModelConverterContextImpl(modelResolver);
73+
74+
final Schema model = context.resolve(new AnnotatedType().type(ClassWithPlainEnum.class));
75+
assertBasicModelStructure(model, "ClassWithPlainEnum");
76+
assertPropertyExists(model, "plainEnum");
77+
78+
final Schema enumPropertySchema = (Schema) model.getProperties().get("plainEnum");
6979
assertNotNull(enumPropertySchema.get$ref());
80+
assertNull(enumPropertySchema.getEnum());
81+
assertEquals(enumPropertySchema.getDescription(), "Plain enum property");
82+
83+
assertEnumComponentExists(context, ClassWithPlainEnum.PlainEnum.values(), null);
84+
85+
// Reset the static field
86+
ModelResolver.enumsAsRef = false;
87+
}
88+
89+
@Test
90+
public void testArrayOfEnumWithSchemaAnnotationOpenApi31() {
91+
final ModelResolver modelResolver = new ModelResolver(mapper()).openapi31(true);
92+
final ModelConverterContextImpl context = new ModelConverterContextImpl(modelResolver);
93+
94+
final Schema model = context.resolve(new AnnotatedType().type(ClassWithEnumArray.class));
95+
assertBasicModelStructure(model, "ClassWithEnumArray");
96+
assertPropertyExists(model, "enumArray");
97+
98+
final Schema arrayPropertySchema = (Schema) model.getProperties().get("enumArray");
99+
assertArrayWithEnumRef(arrayPropertySchema);
100+
101+
assertEnumComponentExists(context, ClassWithEnumArray.ArrayEnum.values(), "Enum description");
102+
}
103+
104+
@Test
105+
public void testArrayOfEnumWithSchemaAnnotationOpenApi30() {
106+
final ModelResolver modelResolver = new ModelResolver(mapper()).openapi31(false);
107+
final ModelConverterContextImpl context = new ModelConverterContextImpl(modelResolver);
108+
109+
final Schema model = context.resolve(new AnnotatedType().type(ClassWithEnumArray.class));
110+
assertBasicModelStructure(model, "ClassWithEnumArray");
111+
assertPropertyExists(model, "enumArray");
112+
113+
final Schema arrayPropertySchema = (Schema) model.getProperties().get("enumArray");
114+
assertArrayWithEnumRef(arrayPropertySchema);
115+
116+
assertEnumComponentExists(context, ClassWithEnumArray.ArrayEnum.values(), "Enum description");
70117
}
71118

119+
@Test
120+
public void testControlTestNoRefOpenApi31() {
121+
ModelResolver.enumsAsRef = false;
122+
final ModelResolver modelResolver = new ModelResolver(mapper()).openapi31(true);
123+
final ModelConverterContextImpl context = new ModelConverterContextImpl(modelResolver);
124+
125+
final Schema model = context.resolve(new AnnotatedType().type(ClassWithPlainEnum.class));
126+
assertBasicModelStructure(model, "ClassWithPlainEnum");
127+
assertPropertyExists(model, "plainEnum");
128+
129+
final Schema enumPropertySchema = (Schema) model.getProperties().get("plainEnum");
130+
assertInlineEnumProperty(enumPropertySchema);
131+
132+
// Apply broad assertions - verify no components are created for inline enums
133+
Map<String, Schema> components = context.getDefinedModels();
134+
if (components != null && !components.isEmpty()) {
135+
Set<String> expected = Arrays.stream(ClassWithPlainEnum.PlainEnum.values())
136+
.map(Enum::name)
137+
.collect(Collectors.toSet());
138+
Schema enumComponent = findEnumComponent(components, expected);
139+
assertNull(enumComponent, "No enum component should exist for inline enums");
140+
}
141+
}
142+
143+
@Test
144+
public void testControlTestNoRefOpenApi30() {
145+
ModelResolver.enumsAsRef = false;
146+
final ModelResolver modelResolver = new ModelResolver(mapper()).openapi31(false);
147+
final ModelConverterContextImpl context = new ModelConverterContextImpl(modelResolver);
148+
149+
final Schema model = context.resolve(new AnnotatedType().type(ClassWithPlainEnum.class));
150+
assertBasicModelStructure(model, "ClassWithPlainEnum");
151+
assertPropertyExists(model, "plainEnum");
152+
153+
final Schema enumPropertySchema = (Schema) model.getProperties().get("plainEnum");
154+
assertInlineEnumProperty(enumPropertySchema);
155+
156+
// Apply broad assertions - verify no components are created for inline enums
157+
Map<String, Schema> components = context.getDefinedModels();
158+
if (components != null && !components.isEmpty()) {
159+
Set<String> expected = Arrays.stream(ClassWithPlainEnum.PlainEnum.values())
160+
.map(Enum::name)
161+
.collect(Collectors.toSet());
162+
Schema enumComponent = findEnumComponent(components, expected);
163+
assertNull(enumComponent, "No enum component should exist for inline enums");
164+
}
165+
}
166+
167+
@Test
168+
public void testEnumWithAllOfSchemaResolutionOpenApi30() {
169+
final ModelResolver modelResolver = new ModelResolver(mapper())
170+
.openapi31(false)
171+
.schemaResolution(Schema.SchemaResolution.ALL_OF);
172+
final ModelConverterContextImpl context = new ModelConverterContextImpl(modelResolver);
173+
174+
final Schema model = context.resolve(new AnnotatedType().type(ClassWithEnumAsRefProperty.class));
175+
assertBasicModelStructure(model, "ClassWithEnumAsRefProperty");
176+
assertPropertyExists(model, "enumWithSchemaProperty");
177+
178+
final Schema enumPropertySchema = (Schema) model.getProperties().get("enumWithSchemaProperty");
179+
180+
boolean hasEnumRef = false;
181+
for (Object allOfItem : enumPropertySchema.getAllOf()) {
182+
if (allOfItem instanceof Schema) {
183+
Schema allOfSchema = (Schema) allOfItem;
184+
if ("#/components/schemas/EnumWithSchemaProperty".equals(allOfSchema.get$ref())) {
185+
hasEnumRef = true;
186+
break;
187+
}
188+
}
189+
}
190+
assertTrue(hasEnumRef, "AllOf should contain reference to enum component");
191+
assertEnumComponentExists(context, ClassWithEnumAsRefProperty.EnumWithSchemaProperty.values(), "Enum description");
192+
}
193+
194+
private void assertBasicModelStructure(Schema model, String expectedName) {
195+
assertNotNull(model);
196+
assertEquals(model.getName(), expectedName);
197+
}
198+
199+
private void assertPropertyExists(Schema model, String propertyName) {
200+
assertTrue(model.getProperties().containsKey(propertyName));
201+
assertNotNull(model.getProperties().get(propertyName));
202+
}
203+
204+
private void assertEnumComponentExists(ModelConverterContextImpl context, Enum<?>[] enumValues, String expectedDescription) {
205+
Map<String, Schema> components = context.getDefinedModels();
206+
assertNotNull(components);
207+
assertFalse(components.isEmpty());
208+
209+
Set<String> expected = Arrays.stream(enumValues)
210+
.map(Enum::name)
211+
.collect(Collectors.toCollection(LinkedHashSet::new));
212+
213+
Schema enumComponent = findEnumComponent(components, expected);
214+
assertNotNull(enumComponent);
215+
assertEquals(enumComponent.getDescription(), expectedDescription);
216+
}
217+
218+
private void assertEnumComponentExistsWithDefault(ModelConverterContextImpl context, Enum<?>[] enumValues, String expectedDescription, String expectedDefault) {
219+
assertEnumComponentExists(context, enumValues, expectedDescription);
220+
221+
Map<String, Schema> components = context.getDefinedModels();
222+
Set<String> expected = Arrays.stream(enumValues)
223+
.map(Enum::name)
224+
.collect(Collectors.toCollection(LinkedHashSet::new));
225+
226+
Schema enumComponent = findEnumComponent(components, expected);
227+
assertEquals(enumComponent.getDefault(), expectedDefault);
228+
}
229+
230+
private Schema findEnumComponent(Map<String, Schema> components, Set<String> expectedValues) {
231+
return components.values().stream()
232+
.filter(Objects::nonNull)
233+
.filter(s -> s.getEnum() != null)
234+
.filter(s -> {
235+
List<?> ev = s.getEnum();
236+
Set<String> vals = ev.stream().map(Object::toString).collect(Collectors.toSet());
237+
return vals.containsAll(expectedValues) && expectedValues.containsAll(vals);
238+
})
239+
.findFirst()
240+
.orElse(null);
241+
}
242+
243+
private void assertEnumAsRefProperty(Schema propertySchema, String expectedRef) {
244+
assertEquals(propertySchema.get$ref(), expectedRef);
245+
assertNull(propertySchema.getEnum());
246+
}
247+
248+
private void assertInlineEnumProperty(Schema propertySchema) {
249+
assertNotNull(propertySchema.getEnum());
250+
assertNull(propertySchema.get$ref());
251+
}
252+
253+
private void assertArrayWithEnumRef(Schema arrayPropertySchema) {
254+
assertNotNull(arrayPropertySchema.getItems());
255+
assertNotNull(arrayPropertySchema.getItems().get$ref());
256+
}
257+
258+
72259
public static class ClassWithEnumAsRefProperty {
73260

74-
@io.swagger.v3.oas.annotations.media.Schema(enumAsRef = true)
261+
@io.swagger.v3.oas.annotations.media.Schema(enumAsRef = true, description = "Property description", maximum = "1923234")
75262
public final EnumWithSchemaProperty enumWithSchemaProperty;
76263

77264
public ClassWithEnumAsRefProperty(EnumWithSchemaProperty enumWithSchemaProperty) {
78265
this.enumWithSchemaProperty = enumWithSchemaProperty;
79266
}
80267

268+
@io.swagger.v3.oas.annotations.media.Schema(description = "Enum description")
81269
public enum EnumWithSchemaProperty {
82270
VALUE1,
83271
VALUE2
84272
}
85273
}
86274

275+
public static class ClassWithEnumArray {
276+
277+
@io.swagger.v3.oas.annotations.media.ArraySchema(schema = @io.swagger.v3.oas.annotations.media.Schema(enumAsRef = true, description = "Property Description"))
278+
public final ArrayEnum[] enumArray;
279+
280+
public ClassWithEnumArray(ArrayEnum[] enumArray) {
281+
this.enumArray = enumArray;
282+
}
283+
284+
@io.swagger.v3.oas.annotations.media.Schema(description = "Enum description")
285+
public enum ArrayEnum {
286+
FIRST,
287+
SECOND,
288+
THIRD
289+
}
290+
}
291+
292+
public static class ClassWithPlainEnum {
293+
294+
@io.swagger.v3.oas.annotations.media.Schema(description = "Plain enum property")
295+
public final PlainEnum plainEnum;
296+
297+
public ClassWithPlainEnum(PlainEnum plainEnum) {
298+
this.plainEnum = plainEnum;
299+
}
300+
301+
public enum PlainEnum {
302+
ONE,
303+
TWO,
304+
THREE
305+
}
306+
}
307+
87308
public enum Currency {
88309
USA, CANADA
89310
}

0 commit comments

Comments
 (0)