diff --git a/spring-auto-restdocs-core/src/main/java/capital/scalable/restdocs/payload/AbstractJacksonFieldSnippet.java b/spring-auto-restdocs-core/src/main/java/capital/scalable/restdocs/payload/AbstractJacksonFieldSnippet.java index 93bdd50b..96114530 100644 --- a/spring-auto-restdocs-core/src/main/java/capital/scalable/restdocs/payload/AbstractJacksonFieldSnippet.java +++ b/spring-auto-restdocs-core/src/main/java/capital/scalable/restdocs/payload/AbstractJacksonFieldSnippet.java @@ -28,6 +28,7 @@ import java.lang.reflect.ParameterizedType; import java.lang.reflect.Type; +import java.lang.reflect.TypeVariable; import java.util.Collection; import java.util.Map; import java.util.stream.Stream; @@ -40,6 +41,8 @@ import capital.scalable.restdocs.snippet.StandardTableSnippet; import com.fasterxml.jackson.databind.JsonMappingException; import com.fasterxml.jackson.databind.ObjectMapper; +import org.apache.commons.lang3.StringUtils; +import org.springframework.core.GenericTypeResolver; import org.springframework.core.MethodParameter; import org.springframework.restdocs.operation.Operation; import org.springframework.web.method.HandlerMethod; @@ -88,6 +91,20 @@ protected FieldDescriptors createFieldDescriptors(Operation operation, protected Type firstGenericType(MethodParameter param) { Type type = param.getGenericParameterType(); if (type instanceof ParameterizedType) { + ParameterizedType parameterizedType = (ParameterizedType) type; + Type actualArgument = parameterizedType.getActualTypeArguments()[0]; + if(actualArgument instanceof Class) { + return actualArgument; + } else if(actualArgument instanceof TypeVariable) { + TypeVariable typeVariable = (TypeVariable)actualArgument; + String variableName = typeVariable.getName(); + Map typeMap = GenericTypeResolver.getTypeVariableMap(param.getContainingClass()); + for(TypeVariable tv : typeMap.keySet()) { + if(StringUtils.equals(tv.getName(), variableName)) { + return typeMap.get(tv); + } + } + } return ((ParameterizedType) type).getActualTypeArguments()[0]; } else { return Object.class; diff --git a/spring-auto-restdocs-core/src/test/java/capital/scalable/restdocs/payload/JacksonResponseFieldSnippetTest.java b/spring-auto-restdocs-core/src/test/java/capital/scalable/restdocs/payload/JacksonResponseFieldSnippetTest.java index 39eea0c6..d484a762 100644 --- a/spring-auto-restdocs-core/src/test/java/capital/scalable/restdocs/payload/JacksonResponseFieldSnippetTest.java +++ b/spring-auto-restdocs-core/src/test/java/capital/scalable/restdocs/payload/JacksonResponseFieldSnippetTest.java @@ -432,6 +432,27 @@ public void comment() throws Exception { .row("field4", "String", "true", "Method 4.")); } + @Test + public void genericSuperMethod() throws Exception{ + HandlerMethod handlerMethod = createHandlerMethod("getItemsGeneric"); + mockFieldComment(Item.class, "field1", "A string"); + mockFieldComment(Item.class, "field2", "A decimal"); + + new JacksonResponseFieldSnippet().document(operationBuilder + .attribute(HandlerMethod.class.getName(), handlerMethod) + .attribute(ObjectMapper.class.getName(), mapper) + .attribute(JavadocReader.class.getName(), javadocReader) + .attribute(ConstraintReader.class.getName(), constraintReader) + .build()); + + assertThat(this.generatedSnippets.snippet(AUTO_RESPONSE_FIELDS)).is( + tableWithHeader("Path", "Type", "Optional", "Description") + .row("[].field1", "String", "true", "A string.") + .row("[].field2", "Decimal", "true", "A decimal.")); + + } + + private void mockConstraintMessage(Class type, String fieldName, String comment) { when(constraintReader.getConstraintMessages(type, fieldName)) .thenReturn(singletonList(comment)); @@ -477,8 +498,28 @@ private HandlerMethod createHandlerMethod(String responseEntityItem) return new HandlerMethod(new TestResource(), responseEntityItem); } + public interface IGenericTestResource { + + List getItemsGeneric(); + } + + public static abstract class GenericTestResource implements IGenericTestResource{ + + abstract E createGeneric(); + + @Override + public List getItemsGeneric() { + return Collections.singletonList(createGeneric()); + } + } + // actual method responses do not matter, they are here just for the illustration - private static class TestResource { + private static class TestResource extends GenericTestResource{ + + @Override + Item createGeneric() { + return new Item("test"); + } public Item getItem() { return new Item("test");