spring generator generates custom PageModel class instead of using standard Page<Model> as return type
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
There is a similar issue whe JsonNullable<String> is defined, the generator creates JsonNullableString instead of expected one.
Did you find a solution? @OleksiL