Skip to content

Commit 383a6cf

Browse files
T3rm1ewaostrowska
andauthored
Search for JsonValue in all classes and interfaces (#4770)
* fix: Include extended classes and implemented interfaces for JsonValue method search * refactor and add tests --------- Co-authored-by: Ewa Ostrowska <ewa.ostrowska@smartbear.com>
1 parent b9b9e96 commit 383a6cf

9 files changed

Lines changed: 165 additions & 4 deletions

File tree

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

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1344,10 +1344,8 @@ protected Schema _createSchemaForEnum(Class<Enum<?>> enumClass) {
13441344
boolean useIndex = _mapper.isEnabled(SerializationFeature.WRITE_ENUMS_USING_INDEX);
13451345
boolean useToString = _mapper.isEnabled(SerializationFeature.WRITE_ENUMS_USING_TO_STRING);
13461346

1347-
Optional<Method> jsonValueMethod = Arrays.stream(enumClass.getDeclaredMethods())
1348-
.filter(m -> m.isAnnotationPresent(JsonValue.class))
1349-
.filter(m -> m.getAnnotation(JsonValue.class).value())
1350-
.findFirst();
1347+
Optional<Method> jsonValueMethod = ReflectionUtils.getAnnotatedMethods(enumClass, JsonValue.class).stream()
1348+
.findFirst();
13511349

13521350
Optional<Field> jsonValueField = Arrays.stream(enumClass.getDeclaredFields())
13531351
.filter(f -> f.isAnnotationPresent(JsonValue.class))

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

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -486,6 +486,29 @@ public static Optional<Object> safeGet(Field field, Object obj) {
486486
} catch (IllegalAccessException e) {
487487
return Optional.empty();
488488
}
489+
}
489490

491+
public static List<Method> getAnnotatedMethods(Class<?> type, Class<? extends Annotation> annotation) {
492+
List<Method> methods = new ArrayList<>();
493+
for (Class<?> clazz = type; clazz != Object.class; clazz = clazz.getSuperclass()) {
494+
collectAnnotatedDeclaredMethods(clazz, annotation, methods);
495+
}
496+
collectAnnotatedMethodsFromInterfaces(type, annotation, methods);
497+
return methods;
498+
}
499+
500+
private static void collectAnnotatedMethodsFromInterfaces(Class<?> type, Class<? extends Annotation> annotation, List<Method> methods) {
501+
for (Class<?> iface : type.getInterfaces()) {
502+
collectAnnotatedDeclaredMethods(iface, annotation, methods);
503+
collectAnnotatedMethodsFromInterfaces(iface, annotation, methods);
504+
}
505+
}
506+
507+
private static void collectAnnotatedDeclaredMethods(Class<?> cls, Class<? extends Annotation> annotation, List<Method> methods) {
508+
for (Method method : cls.getDeclaredMethods()) {
509+
if (method.isAnnotationPresent(annotation)) {
510+
methods.add(method);
511+
}
512+
}
490513
}
491514
}

modules/swagger-core/src/test/java/io/swagger/v3/core/converting/EnumPropertyTest.java

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@
77
import io.swagger.v3.core.jackson.ModelResolver;
88
import io.swagger.v3.core.jackson.TypeNameResolver;
99
import io.swagger.v3.core.matchers.SerializationMatchers;
10+
import io.swagger.v3.core.oas.models.JacksonValueDefaultMethodEnum;
11+
import io.swagger.v3.core.oas.models.JacksonValuePrivateEnum;
1012
import io.swagger.v3.core.oas.models.Model1979;
1113
import io.swagger.v3.core.oas.models.ModelWithEnumField;
1214
import io.swagger.v3.core.oas.models.ModelWithEnumProperty;
@@ -20,9 +22,11 @@
2022
import org.testng.annotations.Test;
2123

2224
import java.util.Arrays;
25+
import java.util.Collections;
2326
import java.util.Map;
2427

2528
import static org.testng.Assert.assertEquals;
29+
import static org.testng.Assert.assertNotNull;
2630
import static org.testng.Assert.assertTrue;
2731

2832
public class EnumPropertyTest {
@@ -235,5 +239,31 @@ public void testExtractJacksonEnumFields() {
235239
assertTrue(sixthEnumProperty instanceof StringSchema);
236240
final StringSchema sixthStringProperty = (StringSchema) sixthEnumProperty;
237241
assertEquals(sixthStringProperty.getEnum(), Arrays.asList("one", "two", "three"));
242+
243+
final Schema seventhEnumProperty = (Schema) model.getProperties().get("seventhEnumValue");
244+
assertTrue(seventhEnumProperty instanceof IntegerSchema);
245+
final IntegerSchema seventhEnumStringProperty = (IntegerSchema) seventhEnumProperty;
246+
assertEquals(seventhEnumStringProperty.getEnum(), Collections.singletonList(10));
247+
248+
final Schema eighthEnumProperty = (Schema) model.getProperties().get("eighthEnumValue");
249+
assertTrue(eighthEnumProperty instanceof StringSchema);
250+
final StringSchema eighthStringProperty = (StringSchema) eighthEnumProperty;
251+
assertEquals(eighthStringProperty.getEnum(), Arrays.asList("alpha", "beta", "gamma"));
252+
}
253+
254+
@Test(description = "it should extract enum values from a private @JsonValue method declared directly on the enum (issue #3998)")
255+
public void testJsonValueOnPrivateMethodIsRecognized() {
256+
final Schema schema = context.resolve(new AnnotatedType(JacksonValuePrivateEnum.class));
257+
assertNotNull(schema);
258+
assertEquals(schema.getType(), "string");
259+
assertEquals(schema.getEnum(), Arrays.asList("one", "two", "three"));
260+
}
261+
262+
@Test(description = "it should extract enum values from a @JsonValue default method declared on an implemented interface")
263+
public void testJsonValueOnInterfaceDefaultMethodIsRecognized() {
264+
final Schema schema = context.resolve(new AnnotatedType(JacksonValueDefaultMethodEnum.class));
265+
assertNotNull(schema);
266+
assertEquals(schema.getType(), "string");
267+
assertEquals(schema.getEnum(), Arrays.asList("alpha", "beta", "gamma"));
238268
}
239269
}
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
package io.swagger.v3.core.oas.models;
2+
3+
import com.fasterxml.jackson.annotation.JsonValue;
4+
5+
public interface InterfaceWithDefaultJsonValue {
6+
7+
@JsonValue
8+
default String toValue() {
9+
return "default";
10+
}
11+
}
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
package io.swagger.v3.core.oas.models;
2+
3+
import com.fasterxml.jackson.annotation.JsonValue;
4+
5+
public interface InterfaceWithJacksonValue {
6+
7+
@JsonValue
8+
int getJsonValue();
9+
}
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
package io.swagger.v3.core.oas.models;
2+
3+
/**
4+
* Enum whose @JsonValue comes from a default method declared on the implemented interface.
5+
* The enum overrides the method (without repeating @JsonValue), so the annotation is only
6+
* present on the interface's default method declaration.
7+
*/
8+
public enum JacksonValueDefaultMethodEnum implements InterfaceWithDefaultJsonValue {
9+
ALPHA, BETA, GAMMA;
10+
11+
@Override
12+
public String toValue() {
13+
return this.name().toLowerCase();
14+
}
15+
}
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
2+
package io.swagger.v3.core.oas.models;
3+
4+
public enum JacksonValueInInterfaceEnum implements InterfaceWithJacksonValue {
5+
6+
TEN(10);
7+
8+
private final int jsonValue;
9+
10+
JacksonValueInInterfaceEnum(int jsonValue) {
11+
this.jsonValue = jsonValue;
12+
}
13+
14+
@Override
15+
public int getJsonValue() {
16+
return jsonValue;
17+
}
18+
}

modules/swagger-core/src/test/java/io/swagger/v3/core/oas/models/ModelWithJacksonEnumField.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,4 +10,6 @@ public class ModelWithJacksonEnumField {
1010
public JacksonValueFieldEnum fourthEnumValue;
1111
public JacksonIntegerValueFieldEnum fifthEnumValue;
1212
public JacksonValuePrivateEnum sixthEnumValue;
13+
public JacksonValueInInterfaceEnum seventhEnumValue;
14+
public JacksonValueDefaultMethodEnum eighthEnumValue;
1315
}

modules/swagger-core/src/test/java/io/swagger/v3/core/util/reflection/ReflectionUtilsTest.java

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
package io.swagger.v3.core.util.reflection;
22

3+
import com.fasterxml.jackson.annotation.JsonValue;
34
import io.swagger.v3.core.util.ReflectionUtils;
45
import io.swagger.v3.core.util.reflection.resources.Child;
56
import io.swagger.v3.core.util.reflection.resources.IParent;
@@ -192,6 +193,60 @@ public void getParameterAnnotationsForOverriddenAnnotationTest() throws NoSuchMe
192193
assertEquals(((AnnotationInterface)parameterAnnotations[0][0]).value(), "level4");
193194
}
194195

196+
@Test
197+
public void getAnnotatedMethods_returnsEmptyWhenNoMethodHasAnnotation() {
198+
List<Method> methods = ReflectionUtils.getAnnotatedMethods(String.class, JsonValue.class);
199+
assertTrue(methods.isEmpty());
200+
}
201+
202+
@Test
203+
public void getAnnotatedMethods_findsAnnotationInSuperclass() {
204+
List<Method> methods = ReflectionUtils.getAnnotatedMethods(SubclassWithoutJsonValue.class, JsonValue.class);
205+
assertEquals(methods.size(), 1);
206+
assertEquals(methods.get(0).getName(), "getJsonValue");
207+
}
208+
209+
@Test
210+
public void getAnnotatedMethods_findsMultipleMatchesAcrossClassHierarchy() {
211+
List<Method> methods = ReflectionUtils.getAnnotatedMethods(SubclassWithOwnJsonValue.class, JsonValue.class);
212+
assertEquals(methods.size(), 2);
213+
List<String> names = methods.stream().map(Method::getName).collect(Collectors.toList());
214+
assertTrue(names.contains("getJsonValue"));
215+
assertTrue(names.contains("getSubJsonValue"));
216+
}
217+
218+
@Test
219+
public void getAnnotatedMethods_findsDefaultMethodAnnotationFromInterface() {
220+
List<Method> methods = ReflectionUtils.getAnnotatedMethods(ImplementorWithoutOverride.class, JsonValue.class);
221+
assertEquals(methods.size(), 1);
222+
assertEquals(methods.get(0).getName(), "toValue");
223+
}
224+
225+
// --- Support classes for getAnnotatedMethods tests ---
226+
227+
private static class SuperclassWithJsonValue {
228+
@JsonValue
229+
public String getJsonValue() { return "super"; }
230+
}
231+
232+
private static class SubclassWithoutJsonValue extends SuperclassWithJsonValue {
233+
public String getOtherMethod() { return "other"; }
234+
}
235+
236+
private static class SubclassWithOwnJsonValue extends SuperclassWithJsonValue {
237+
@JsonValue
238+
public String getSubJsonValue() { return "sub"; }
239+
}
240+
241+
private interface InterfaceWithDefaultJsonValue {
242+
@JsonValue
243+
default String toValue() { return "from-interface"; }
244+
}
245+
246+
private static class ImplementorWithoutOverride implements InterfaceWithDefaultJsonValue {}
247+
248+
// ---
249+
195250
@Tag(name = "inherited tag")
196251
private interface AnnotatedInterface {}
197252

0 commit comments

Comments
 (0)