Autodocs does not properly support nested generics
It appears that the autodocs auto-response-fields generator does not properly unwrap nested generic properties, even though the supporting jackson library can. Specifically, the resolver does not unwrap the concrete implementation type, instead preferring the generic base type.
Whats more is that the extracted type changes whether or not you return a reactor type (mono/flux) or a straight response type. If you return a non-reactive type, the PagedRestResponse object is captured in the .adoc. If you instead return a reactive type, the PagedRestResponse is swallowed (leading to an improper rest doc).
The PagedRestResponse object
public class PagedRestResponse<T> {
@JsonProperty("results")
private Collection<T> results;
@JsonProperty("page")
private int page;
@JsonProperty("size")
private int size;
@JsonProperty("pageCount")
private int totalPages;
@JsonProperty("last")
private boolean last;
public PagedRestResponse(Page<T> results) {
this.results = results.toList();
this.page = results.getNumber();
this.size = results.getSize();
this.totalPages = results.getTotalPages();
this.last = results.isLast();
}
}
An excerpt from the default spring-restdocs. As you can see, it has resolved the generic response type just fine.
{
"results" : [ {
"id" : 2,
"active" : true,
"description" : "tell me more",
"modifiedDate" : {
"nano" : 820072000,
"epochSecond" : 1593046141
},
"tags" : [ "Garage", "Waterfront" ],
"price" : 14.0,
"currency" : "CAD",
"rooms" : [ {
"length" : 7.0,
"width" : 7.0,
"type" : "BEDROOM",
"roomName" : "Master Bedroom",
"roomDescription" : "A cool place to be"
} ],
The autogenerated auto-response-fields adoc. As you can see, it has completely omitted the PagedRestResponse response wrapper
|===
|Path|Type|Optional|Description
|active
|Boolean
|true
|@return the active.
|listingDate
|Object
|true
|@return the listingDate.
|listingDate.nano
|Integer
|true
|
|listingDate.epochSecond
|Integer
|true
|
|activeDate
|Object
|true
|@return the activeDate.
|activeDate.nano
|Integer
|true
|
|activeDate.epochSecond
|Integer
|true
|
|description
|String
|true
|@return the description.
|lastModified
|Object
|true
|@return the lastModified.
|lastModified.nano
|Integer
|true
|
|lastModified.epochSecond
|Integer
|true
|
|tags
|Array[String]
|true
|@return the tags.
|===
The POJO being resolved by this library
public class ListingDTO {
@JsonProperty("active")
public boolean active;
@JsonProperty("listingDate")
public Instant listingDate;
@JsonProperty("activeDate")
public Instant activeDate;
@JsonProperty("description")
public String description;
@JsonProperty("lastModified")
public Instant lastModified;
@JsonProperty("tags")
public Collection<String> tags = new HashSet<>();
The actual POJO being returned as part of the method (omitted most of it, as its a huge POJO)
@JsonInclude(value = Include.NON_NULL)
@JsonPropertyOrder(value = "id")
public class PropertyDTO extends PricedListingDTO {
@JsonProperty(value = "id", access = Access.READ_ONLY)
public Long id;
@JsonProperty("rooms")
public Collection<RoomDTO> rooms = new HashSet<>();
@JsonProperty("address")
public AddressDTO address;
@JsonProperty("brokerID")
public Long brokerID;
The controller method we are calling
public class AbstractListingManagementController<E extends Listing, D extends ListingDTO, S extends GenericSearchRequest> {
@Autowired
private GenericManagementService<E, D, S> managementService;
@RequestMapping(value = "/listings", method = RequestMethod.GET)
public @ResponseBody Mono<PagedRestResponse<D>> getListings(@RequestParam(name = "start", defaultValue = "0") int start,
@RequestParam(name = "size", defaultValue = "15") int size) {
return Mono.just(new PagedRestResponse<>(managementService.getListingsAsDTO(PageRequest.of(start, size))));
}
The unit test
@Test
@WithMockUser
public void getListings() {
//@formatter:off
this.webTestClient.get().uri("/api/v1/property/listings")
.exchange()
.expectStatus().isOk()
.expectBody().consumeWith(commonDocumentation());
//@formatter:on
}
Am I correct that you are expecting the following result:
|===
|Path|Type|Optional|Description
|results.active
|Boolean
|true
|@return the active.
and currently we are omitting results part incorrectly?