ScaCap / spring-auto-restdocs

Spring Auto REST Docs is an extension to Spring REST Docs
https://scacap.github.io/spring-auto-restdocs/
Apache License 2.0
310 stars 86 forks source link

Autodocs does not properly support nested generics #404

Closed heanssgen-troy closed 9 months ago

heanssgen-troy commented 4 years ago

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
    }
fbenz commented 4 years ago

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?