OpenAPITools / OpenAPITools/openapi-generator
spring generator generates custom PageModel class instead of using standard Page<Model> as return type
Nobody has claimed this yet.
- Dominant language
- Java
- Stars
- 26.8k
- Forks
- 7.7k
- PR merge metrics
- PR metrics pending
Description
Description
- I am using
springdocto generateopenapi 3schema for one of my microservice API.
- tools versions:
implementation 'org.springdoc:springdoc-openapi-ui:1.6.11'
I tried to add implementation 'org.springdoc:springdoc-openapi-data-rest:1.6.11' and it didn't change anything
- Controller method for which schema is generated:
@GetMapping("/country")
@Timed
public ResponseEntity<Page<CountryDto>> searchCountry(
@RequestParam("searchTerm") String searchTerm,
@RequestParam(name = "filterBy", required = false) String filterBy,
@RequestParam(name = "filterValue", required = false) String filterValue) {
return ResponseEntity.ok(searchService.findCountry(searchTerm));
}
- Generate by
springdocschema:
click to expand
{
"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"
}
}
}
}
}
}
- I feed generated schema to
openapi generatorwith"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"
}
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
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<ExtCountryDto> 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<ExtCountryDto> 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<ExtCountryDto> getContent() {
return content;
}
public void setContent(List<ExtCountryDto> 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
Contributor guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Research direction
Reproduce the issue with the provided OpenAPI schema and Gradle GenerateTask configuration using the spring generator and spring-cloud library. Start by tracing how the Spring generator handles the PageCountryDto response and compare the generated ExtPageCountryDto with the expected Page return type. Done means generation produces the standard paged return type rather than an expanded custom page model.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- java, openapi, spring
- Domain
- api, backend
- Issue type
- Bug
- Difficulty
- 3/5
- Estimated time
- 1-2 days
- Activity status
- Quiet
- Clarity
- Mostly clear
- Newbie friendliness
- 48/100