OpenAPITools / openapi-generator

OpenAPI Generator allows generation of API client libraries (SDK generation), server stubs, documentation and configuration automatically given an OpenAPI Spec (v2, v3)
https://openapi-generator.tech
Apache License 2.0
22.01k stars 6.6k forks source link

spring generator generates custom PageModel class instead of using standard Page<Model> as return type #15121

Open OleksiL opened 1 year ago

OleksiL commented 1 year ago
Description
  1. I am using springdoc to generate openapi 3 schema for one of my microservice API.
{
  "openapi": "3.0.1",
  "info": {
    "title": "OpenAPI definition",
    "version": "v0"
  },
  "servers": [
    {
      "url": "http://localhost:8083",
      "description": "Generated server url"
    }
  ],
  "paths": {
    "/api/search/country": {
      "get": {
        "tags": [
          "mdm-search-controller"
        ],
        "operationId": "searchCountry",
        "parameters": [
          {
            "name": "searchTerm",
            "in": "query",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "filterBy",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "filterValue",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "*/*": {
                "schema": {
                  "$ref": "#/components/schemas/PageCountryDto"
                }
              }
            }
          }
        }
      }
    }
  },
  "components": {
    "schemas": {
      "CountryDto": {
        "required": [
          "isoShortNameUpperCaseEn"
        ],
        "type": "object",
        "properties": {
          "isoShortNameUpperCaseEn": {
            "maxLength": 70,
            "minLength": 0,
            "type": "string"
          }
        }
      },
      "PageCountryDto": {
        "type": "object",
        "properties": {
          "totalPages": {
            "type": "integer",
            "format": "int32"
          },
          "totalElements": {
            "type": "integer",
            "format": "int64"
          },
          "size": {
            "type": "integer",
            "format": "int32"
          },
          "content": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/CountryDto"
            }
          },
          "number": {
            "type": "integer",
            "format": "int32"
          },
          "sort": {
            "$ref": "#/components/schemas/SortObject"
          },
          "first": {
            "type": "boolean"
          },
          "last": {
            "type": "boolean"
          },
          "pageable": {
            "$ref": "#/components/schemas/PageableObject"
          },
          "numberOfElements": {
            "type": "integer",
            "format": "int32"
          },
          "empty": {
            "type": "boolean"
          }
        }
      },
      "PageableObject": {
        "type": "object",
        "properties": {
          "offset": {
            "type": "integer",
            "format": "int64"
          },
          "sort": {
            "$ref": "#/components/schemas/SortObject"
          },
          "pageNumber": {
            "type": "integer",
            "format": "int32"
          },
          "pageSize": {
            "type": "integer",
            "format": "int32"
          },
          "paged": {
            "type": "boolean"
          },
          "unpaged": {
            "type": "boolean"
          }
        }
      },
      "SortObject": {
        "type": "object",
        "properties": {
          "empty": {
            "type": "boolean"
          },
          "sorted": {
            "type": "boolean"
          },
          "unsorted": {
            "type": "boolean"
          }
        }
      }
    }
  }
}

  1. I feed generated schema to openapi generator with "spring" generator to generate models and API interface in another module:
    • tools versions:
      
      import org.openapitools.generator.gradle.plugin.tasks.GenerateTask

plugins { id "org.openapi.generator" version "5.3.0" id "java-library" }


```groovy
def tasks = new File("$projectDir/src/main/resources/swagger/")
    .list { _, name -> name.endsWith("json") }
    .collect {
        def file = it
        def name = file.replaceAll("(\\.json)|(\\s)|(tic-)", "")
        return tasks.create(name: "openApiGenerate-$name", type: GenerateTask) {
            generatorName = "spring"
            library = "spring-cloud"
            inputSpec = "$projectDir/src/main/resources/swagger/$file".toString()
            outputDir = "$buildDir/generated".toString()
            apiPackage = "com.kn.tic.rest.client.${name}.api"
            modelPackage = "com.kn.tic.rest.client.${name}.model"
            invokerPackage = "com.kn.tic.rest.client"
            modelNamePrefix = "Ext"
            configOptions = [
                dateLibrary: "java8"
            ]
        }
    }

My problem is that while the return type of a controller method in the first microservice is Page<ModelDto>, what I get generated by "spring" openapi generator in the second module for that controller method return type is some custom class PageModelDto which has all the fields of Pageable interface and a list of ModelDto in its content property:

click to expand ```java package com.kn.tic.rest.client.mdm_api.model; import java.util.Objects; import com.fasterxml.jackson.annotation.JsonProperty; import com.fasterxml.jackson.annotation.JsonCreator; import com.kn.tic.rest.client.mdm_api.model.ExtCountryDto; import com.kn.tic.rest.client.mdm_api.model.ExtPageableObject; import com.kn.tic.rest.client.mdm_api.model.ExtSortObject; import io.swagger.annotations.ApiModel; import io.swagger.annotations.ApiModelProperty; import java.util.ArrayList; import java.util.List; import org.openapitools.jackson.nullable.JsonNullable; import javax.validation.Valid; import javax.validation.constraints.*; /** * ExtPageCountryDto */ @javax.annotation.Generated(value = "org.openapitools.codegen.languages.SpringCodegen", date = "2023-04-04T10:07:29.038718+03:00[Europe/Tallinn]") public class ExtPageCountryDto { @JsonProperty("totalPages") private Integer totalPages; @JsonProperty("totalElements") private Long totalElements; @JsonProperty("size") private Integer size; @JsonProperty("content") @Valid private List content = null; @JsonProperty("number") private Integer number; @JsonProperty("sort") private ExtSortObject sort; @JsonProperty("first") private Boolean first; @JsonProperty("last") private Boolean last; @JsonProperty("pageable") private ExtPageableObject pageable; @JsonProperty("numberOfElements") private Integer numberOfElements; @JsonProperty("empty") private Boolean empty; public ExtPageCountryDto totalPages(Integer totalPages) { this.totalPages = totalPages; return this; } /** * Get totalPages * @return totalPages */ @ApiModelProperty(value = "") public Integer getTotalPages() { return totalPages; } public void setTotalPages(Integer totalPages) { this.totalPages = totalPages; } public ExtPageCountryDto totalElements(Long totalElements) { this.totalElements = totalElements; return this; } /** * Get totalElements * @return totalElements */ @ApiModelProperty(value = "") public Long getTotalElements() { return totalElements; } public void setTotalElements(Long totalElements) { this.totalElements = totalElements; } public ExtPageCountryDto size(Integer size) { this.size = size; return this; } /** * Get size * @return size */ @ApiModelProperty(value = "") public Integer getSize() { return size; } public void setSize(Integer size) { this.size = size; } public ExtPageCountryDto content(List content) { this.content = content; return this; } public ExtPageCountryDto addContentItem(ExtCountryDto contentItem) { if (this.content == null) { this.content = new ArrayList<>(); } this.content.add(contentItem); return this; } /** * Get content * @return content */ @ApiModelProperty(value = "") @Valid public List getContent() { return content; } public void setContent(List content) { this.content = content; } public ExtPageCountryDto number(Integer number) { this.number = number; return this; } /** * Get number * @return number */ @ApiModelProperty(value = "") public Integer getNumber() { return number; } public void setNumber(Integer number) { this.number = number; } public ExtPageCountryDto sort(ExtSortObject sort) { this.sort = sort; return this; } /** * Get sort * @return sort */ @ApiModelProperty(value = "") @Valid public ExtSortObject getSort() { return sort; } public void setSort(ExtSortObject sort) { this.sort = sort; } public ExtPageCountryDto first(Boolean first) { this.first = first; return this; } /** * Get first * @return first */ @ApiModelProperty(value = "") public Boolean getFirst() { return first; } public void setFirst(Boolean first) { this.first = first; } public ExtPageCountryDto last(Boolean last) { this.last = last; return this; } /** * Get last * @return last */ @ApiModelProperty(value = "") public Boolean getLast() { return last; } public void setLast(Boolean last) { this.last = last; } public ExtPageCountryDto pageable(ExtPageableObject pageable) { this.pageable = pageable; return this; } /** * Get pageable * @return pageable */ @ApiModelProperty(value = "") @Valid public ExtPageableObject getPageable() { return pageable; } public void setPageable(ExtPageableObject pageable) { this.pageable = pageable; } public ExtPageCountryDto numberOfElements(Integer numberOfElements) { this.numberOfElements = numberOfElements; return this; } /** * Get numberOfElements * @return numberOfElements */ @ApiModelProperty(value = "") public Integer getNumberOfElements() { return numberOfElements; } public void setNumberOfElements(Integer numberOfElements) { this.numberOfElements = numberOfElements; } public ExtPageCountryDto empty(Boolean empty) { this.empty = empty; return this; } /** * Get empty * @return empty */ @ApiModelProperty(value = "") public Boolean getEmpty() { return empty; } public void setEmpty(Boolean empty) { this.empty = empty; } @Override public boolean equals(Object o) { if (this == o) { return true; } if (o == null || getClass() != o.getClass()) { return false; } ExtPageCountryDto pageCountryDto = (ExtPageCountryDto) o; return Objects.equals(this.totalPages, pageCountryDto.totalPages) && Objects.equals(this.totalElements, pageCountryDto.totalElements) && Objects.equals(this.size, pageCountryDto.size) && Objects.equals(this.content, pageCountryDto.content) && Objects.equals(this.number, pageCountryDto.number) && Objects.equals(this.sort, pageCountryDto.sort) && Objects.equals(this.first, pageCountryDto.first) && Objects.equals(this.last, pageCountryDto.last) && Objects.equals(this.pageable, pageCountryDto.pageable) && Objects.equals(this.numberOfElements, pageCountryDto.numberOfElements) && Objects.equals(this.empty, pageCountryDto.empty); } @Override public int hashCode() { return Objects.hash(totalPages, totalElements, size, content, number, sort, first, last, pageable, numberOfElements, empty); } @Override public String toString() { StringBuilder sb = new StringBuilder(); sb.append("class ExtPageCountryDto {\n"); sb.append(" totalPages: ").append(toIndentedString(totalPages)).append("\n"); sb.append(" totalElements: ").append(toIndentedString(totalElements)).append("\n"); sb.append(" size: ").append(toIndentedString(size)).append("\n"); sb.append(" content: ").append(toIndentedString(content)).append("\n"); sb.append(" number: ").append(toIndentedString(number)).append("\n"); sb.append(" sort: ").append(toIndentedString(sort)).append("\n"); sb.append(" first: ").append(toIndentedString(first)).append("\n"); sb.append(" last: ").append(toIndentedString(last)).append("\n"); sb.append(" pageable: ").append(toIndentedString(pageable)).append("\n"); sb.append(" numberOfElements: ").append(toIndentedString(numberOfElements)).append("\n"); sb.append(" empty: ").append(toIndentedString(empty)).append("\n"); sb.append("}"); return sb.toString(); } /** * Convert the given object to string with each line indented by 4 spaces * (except the first line). */ private String toIndentedString(Object o) { if (o == null) { return "null"; } return o.toString().replace("\n", "\n "); } } ```

Honestly, I even don't understand where the problem is: is it springdoc which generates a wrong schema for Page return type? Or the problem is with spring openapi generator which generates wrong classes based on the schema? Few hours of googling didn't help. Would appreciate some help.

openapi-generator version
robertop87 commented 9 months ago

There is a similar issue whe JsonNullable is defined, the generator creates JsonNullableString instead of expected one.

priprocess commented 6 months ago

Did you find a solution? @OleksiL