OpenAPITools / OpenAPITools/openapi-generator
[BUG][Java Spring] Generating wrong type for response when list and single responses under same tag
Nobody has claimed this yet.
- Dominant language
- Java
- Stars
- 26.8k
- Forks
- 7.7k
- PR merge metrics
- PR metrics pending
Description
- Have you provided a full/minimal spec to reproduce the issue?
- Have you validated the input using an OpenAPI validator (example)?
- Have you tested with the latest master to confirm the issue still exists?
- Have you searched for related issues/PRs?
- What's the actual output vs expected output?
Description
As a learning lab, I have this repository (and until the merge happens, the code can be found in the branch feature/wip_country-api). In it I am exhaustively using the $ref: parameter in every possible place.
Why am I doing this? Simply because the specification says it is possible.
Apparently there must be some cache that considers the tag and replicates the response type regardless of the endpoints contained in it.
openapi-generator version
org.openapitools:openapi-generator-maven-plugin:6.0.0 and with the latest master by building the JAR locally to see if the issue has already been addressed.
OpenAPI declaration file content or url
NOTE!: The comments # Open-API generation issue. are related to another type of path resolution bug.
# specification.yaml:
openapi: '3.0.3'
info:
title: &info-title moname-addressing-api
description: Project to money me with no name [addressing-api].
termsOfService: https://moname.kinlhp.com/api/terms
contact:
name: Kin.LHP® Software, Inc.
url: https://kinlhp.com
email: lhp.kin@gmail.com
license:
name: &info-license-name MIT License
url: https://github.com/kinlhpsoftwareinc/moname/blob/develop/LICENSE
version: 1.0.0.BUILD-SNAPSHOT
servers:
- url: http://{host}:{port}/{basePath}
description: The local development API server.
variables:
host:
# NOTE! No enum here means it is an open value.
enum:
- localhost
default: localhost
description:
port:
# NOTE! No enum here means it is an open value.
enum:
- 8080
default: 8080
description:
basePath:
# NOTE! No enum here means it is an open value.
enum:
- api
default: api
description:
- url: https://{host}:{port}
description: The production API server.
variables:
host:
# NOTE! No enum here means it is an open value.
enum:
- api.addressing.moname.kinlhp.com
default: api.addressing.moname.kinlhp.com
description: This value is assigned by the service provider.
port:
# NOTE! No enum here means it is an open value.
enum:
- 8080
default: 8080
description: This value is assigned by the service provider.
paths:
/v1/countries:
#
# Escaped forward-slash is necessary when using JSON references.
# https://spec.openapis.org/oas/latest.html#operationref-examples
$ref: 'components/pathItems/v1/countries.yaml#/getCountries'
/v1/countries/{numeric-code}:
#
# Escaped forward-slash is necessary when using JSON references.
# https://spec.openapis.org/oas/latest.html#operationref-examples
$ref: 'components/pathItems/v1/countries.yaml#/getCountry'
tags:
- name: countries
description: Countries resource related.
# components/commons/examples/4xx.yaml
notFound:
summary: ¬Found-summary 404 Not Found.
description: *notFound-summary
value:
code: 404
description: Not Found
# components/commons/examples/parameters.yaml
pageIndex:
summary: &pageIndex-summary Zero-based page index, must not be negative.
description: *pageIndex-summary
value: 0
pageSize:
summary: &pageSize-summary The size of the page to be returned, must be greater than 0.
description: *pageSize-summary
value: 25
# components/commons/parameters/in/query.yaml
pageIndex:
name: page-index
in: query
description: Zero-based page index, must not be negative.
required: true
deprecated: false
allowEmptyValue: false
style: form
explode: false
allowReserved: false
schema:
$ref: '../../schemas/parameters.yaml#/pageIndex'
examples:
pageIndex:
#ref: '../../examples/parameters.yaml#/pageIndex'
$ref: '../components/commons/examples/parameters.yaml#/pageIndex' # Open-API generation issue.
pageSize:
name: page-size
in: query
description: The size of the page to be returned, must be greater than 0.
required: true
deprecated: false
allowEmptyValue: false
style: form
explode: false
allowReserved: false
schema:
$ref: '../../schemas/parameters.yaml#/pageSize'
examples:
pageSize:
#ref: '../../examples/parameters.yaml#/pageSize'
$ref: '../components/commons/examples/parameters.yaml#/pageSize' # Open-API generation issue.
# components/commons/responses/2xx.yaml
noContent:
description: 204 No Content.
# components/commons/responses/4xx.yaml
notFound:
description: 404 Not Found.
content:
application/json:
schema:
$ref: '../schemas/4xx.yaml#/notFoundResponse'
examples:
notFound:
$ref: '../examples/4xx.yaml#/notFound'
# components/commons/schemas/4xx.yaml
errorCode:
nullable: false
readOnly: true
deprecated: false
multipleOf: 1
maximum: 499
minimum: 400
type: integer
description:
format: int32
default: 404
errorDescription:
nullable: false
readOnly: true
deprecated: false
maxLength: 31
minLength: 4
type: string
description:
default: Not Found
notFoundResponse:
nullable: false
readOnly: true
deprecated: false
title: notFoundResponse
required:
- code
- description
type: object
properties:
code:
$ref: '#/errorCode'
description:
$ref: '#/errorDescription'
description:
# components/commons/schemas/parameters.yaml
pageIndex:
nullable: false
readOnly: true
deprecated: false
multipleOf: 1
maximum: 2147483647
minimum: 0
type: integer
description:
format: int32
default: 0
pageSize:
nullable: false
readOnly: true
deprecated: false
multipleOf: 1
maximum: 100
minimum: 1
type: integer
description:
format: int32
default: 25
# components/examples/countries.yaml
countriesResponse:
summary: &countriesResponse-summary Countries.
description: *countriesResponse-summary
value:
countries:
- alpha2Code: AF
alpha3Code: AFG
englishName: Afghanistan
frenchName: Afghanistan (l')
internetCctld: .af
numericCode: 4
portugueseName: Afeganistão
- alpha2Code: BV
alpha3Code: BVT
englishName: Bouvet Island
frenchName: Bouvet (l'Île)
internetCctld:
numericCode: 74
portugueseName: Ilha Bouvet
- alpha2Code: ZM
alpha3Code: ZMB
englishName: Zambia
frenchName: Zambie (la)
internetCctld: .zm
numericCode: 894
portugueseName: Zâmbia
countryResponse:
summary: &countryResponse-summary Country.
description: *countryResponse-summary
value:
alpha2Code: BR
alpha3Code: BRA
englishName: Brazil
frenchName: Brésil (le)
internetCctld: .br
numericCode: 76
portugueseName: Brasil
# components/examples/parameters.yaml
numericCode:
summary: &numericCode-summary ISO 3166-1 numeric codes are three-digit (left padded with zero) country codes defined in ISO 3166-1.
description: *numericCode-summary
value: 76
# components/parameters/in/path.yaml
numericCode:
name: numeric-code
in: path
description: ISO 3166-1 numeric codes are three-digit (left padded with zero) country codes defined in ISO 3166-1.
required: true
deprecated: false
allowEmptyValue: false
style: simple
explode: false
allowReserved: false
schema:
$ref: '../../schemas/countries.yaml#/numericCode'
examples:
pageIndex:
#ref: '../../examples/parameters.yaml#/numericCode'
$ref: '../components/examples/parameters.yaml#/numericCode' # Open-API generation issue.
# components/pathItems/v1/countries.yaml
getCountries:
summary: &getCountries-summary Countries resource.
description: *getCountries-summary
get:
tags:
- countries
summary: &getCountries-get-summary GET Countries.
description: *getCountries-get-summary
operationId: getCountries
parameters:
- $ref: '../../commons/parameters/in/query.yaml#/pageIndex'
- $ref: '../../commons/parameters/in/query.yaml#/pageSize'
responses:
default:
$ref: '../../commons/responses/2xx.yaml#/noContent'
200:
$ref: '../../responses/countries.yaml#/getCountries/ok'
deprecated: false
getCountry:
summary: &getCountry-summary Countries resource.
description: *getCountry-summary
get:
tags:
- countries
summary: &getCountry-get-summary GET Country.
description: *getCountry-get-summary
operationId: getCountry
parameters:
- $ref: '../../parameters/in/path.yaml#/numericCode'
responses:
default:
$ref: '../../commons/responses/4xx.yaml#/notFound'
200:
$ref: '../../responses/countries.yaml#/getCountry/ok'
deprecated: false
# components/responses/countries.yaml
getCountries:
ok:
description: 200 OK.
content:
application/json:
schema:
$ref: '../schemas/countries.yaml#/countriesResponse'
examples:
ok:
$ref: '../examples/countries.yaml#/countriesResponse'
getCountry:
ok:
description: 200 OK.
content:
application/json:
schema:
$ref: '../schemas/countries.yaml#/countryResponse'
examples:
ok:
$ref: '../examples/countries.yaml#/countryResponse'
# components/schemas/countries.yaml
alpha2Code:
nullable: false
readOnly: true
deprecated: false
maxLength: 2
minLength: 2
type: string
description:
alpha3Code:
nullable: false
readOnly: true
deprecated: false
maxLength: 3
minLength: 3
type: string
description:
countriesResponse:
nullable: false
readOnly: true
deprecated: false
title: countriesResponse
required:
- countries
type: object
properties:
countries:
nullable: false
readOnly: true
deprecated: false
title: countries
maxItems: 100
minItems: 1
uniqueItems: true
type: array
items:
$ref: '#/countryResponse'
countryResponse:
nullable: false
readOnly: true
deprecated: false
title: countryResponse
required:
- alpha2Code
- alpha3Code
- englishName
- frenchName
- numericCode
- portugueseName
type: object
properties:
alpha2Code:
$ref: '#/alpha2Code'
alpha3Code:
$ref: '#/alpha3Code'
englishName:
$ref: '#/englishName'
frenchName:
$ref: '#/frenchName'
internetCctld:
$ref: '#/internetCctld'
numericCode:
$ref: '#/numericCode'
portugueseName:
$ref: '#/portugueseName'
description:
englishName:
nullable: false
readOnly: true
deprecated: false
maxLength: 58
minLength: 4
type: string
description:
frenchName:
nullable: false
readOnly: true
deprecated: false
maxLength: 56
minLength: 4
type: string
description:
internetCctld:
nullable: false
readOnly: true
deprecated: false
maxLength: 7
type: string
description:
numericCode:
nullable: false
readOnly: true
deprecated: false
multipleOf: 1
maximum: 894
minimum: 4
type: integer
description:
format: int32
portugueseName:
nullable: false
readOnly: true
deprecated: false
maxLength: 46
minLength: 3
type: string
description:
# components/schemas/parameters.yaml
numericCode:
nullable: false
readOnly: true
deprecated: false
multipleOf: 1
maximum: 2147483647
minimum: 0
type: integer
description: ISO 3166-1 numeric codes are three-digit (left padded with zero) country codes defined in ISO 3166-1.
format: int32
default: 0
Command line used for generation
./mvnw --batch-mode --define skipTests --no-transfer-progress --threads 1C --update-snapshots clean package
Steps to reproduce
- Define all the specifications described in the files above
- Run the command described above to generate all files (canonical specification and Java classes)
Expected behavior
One endpoint with correct default ResponseEntity<CountriesResponseDTO> getCountries(Integer pageIndex, Integer pageSize) signature and another endpoint with correct default ResponseEntity<CountryResponseDTO> getCountry(Integer numericCode) signature:
// .../CountriesApiDelegate.java
@Generated(value = "org.openapitools.codegen.languages.SpringCodegen", date = "2023-06-05T22:29:29.944481946-03:00[America/Sao_Paulo]")
public interface CountriesApiDelegate {
default Optional<NativeWebRequest> getRequest() {
return Optional.empty();
}
/**
* GET /v1/countries : GET Countries.
* GET Countries.
*
* @param pageIndex Zero-based page index, must not be negative. (required)
* @param pageSize The size of the page to be returned, must be greater than 0. (required)
* @return 200 OK. (status code 200)
* or 204 No Content. (status code 200)
* @see CountriesApi#getCountries
*/
default ResponseEntity<CountriesResponseDTO> getCountries(Integer pageIndex,
Integer pageSize) throws Exception {
getRequest().ifPresent(request -> {
for (MediaType mediaType: MediaType.parseMediaTypes(request.getHeader("Accept"))) {
if (mediaType.isCompatibleWith(MediaType.valueOf("application/json"))) {
String exampleString = "{ \"countries\" : [ { \"alpha3Code\" : \"alpha3Code\", \"englishName\" : \"englishName\", \"alpha2Code\" : \"alpha2Code\", \"internetCctld\" : \"internetCctld\", \"frenchName\" : \"frenchName\", \"portugueseName\" : \"portugueseName\", \"numericCode\" : 75 }, { \"alpha3Code\" : \"alpha3Code\", \"englishName\" : \"englishName\", \"alpha2Code\" : \"alpha2Code\", \"internetCctld\" : \"internetCctld\", \"frenchName\" : \"frenchName\", \"portugueseName\" : \"portugueseName\", \"numericCode\" : 75 }, { \"alpha3Code\" : \"alpha3Code\", \"englishName\" : \"englishName\", \"alpha2Code\" : \"alpha2Code\", \"internetCctld\" : \"internetCctld\", \"frenchName\" : \"frenchName\", \"portugueseName\" : \"portugueseName\", \"numericCode\" : 75 }, { \"alpha3Code\" : \"alpha3Code\", \"englishName\" : \"englishName\", \"alpha2Code\" : \"alpha2Code\", \"internetCctld\" : \"internetCctld\", \"frenchName\" : \"frenchName\", \"portugueseName\" : \"portugueseName\", \"numericCode\" : 75 }, { \"alpha3Code\" : \"alpha3Code\", \"englishName\" : \"englishName\", \"alpha2Code\" : \"alpha2Code\", \"internetCctld\" : \"internetCctld\", \"frenchName\" : \"frenchName\", \"portugueseName\" : \"portugueseName\", \"numericCode\" : 75 } ] }";
ApiUtil.setExampleResponse(request, "application/json", exampleString);
break;
}
}
});
return new ResponseEntity<>(HttpStatus.NOT_IMPLEMENTED);
}
/**
* GET /v1/countries/{numeric-code} : GET Country.
* GET Country.
*
* @param numericCode ISO 3166-1 numeric codes are three-digit (left padded with zero) country codes defined in ISO 3166-1. (required)
* @return 200 OK. (status code 200)
* or 404 Not Found. (status code 200)
* @see CountriesApi#getCountry
*/
default ResponseEntity<CountryResponseDTO> getCountry(Integer numericCode) throws Exception {
getRequest().ifPresent(request -> {
for (MediaType mediaType: MediaType.parseMediaTypes(request.getHeader("Accept"))) {
if (mediaType.isCompatibleWith(MediaType.valueOf("application/json"))) {
String exampleString = "{ \"alpha3Code\" : \"alpha3Code\", \"englishName\" : \"englishName\", \"alpha2Code\" : \"alpha2Code\", \"internetCctld\" : \"internetCctld\", \"frenchName\" : \"frenchName\", \"portugueseName\" : \"portugueseName\", \"numericCode\" : 75 }";
ApiUtil.setExampleResponse(request, "application/json", exampleString);
break;
}
}
});
return new ResponseEntity<>(HttpStatus.NOT_IMPLEMENTED);
}
}
Observed behavior
One endpoint with correct default ResponseEntity<CountriesResponseDTO> getCountries(Integer pageIndex, Integer pageSize) signature and another endpoint with wrong default ResponseEntity<CountriesResponseDTO> getCountry(Integer numericCode) signature:
// .../CountriesApiDelegate.java
@Generated(value = "org.openapitools.codegen.languages.SpringCodegen", date = "2023-06-05T22:29:29.944481946-03:00[America/Sao_Paulo]")
public interface CountriesApiDelegate {
default Optional<NativeWebRequest> getRequest() {
return Optional.empty();
}
/**
* GET /v1/countries : GET Countries.
* GET Countries.
*
* @param pageIndex Zero-based page index, must not be negative. (required)
* @param pageSize The size of the page to be returned, must be greater than 0. (required)
* @return 200 OK. (status code 200)
* or 204 No Content. (status code 200)
* @see CountriesApi#getCountries
*/
default ResponseEntity<CountriesResponseDTO> getCountries(Integer pageIndex,
Integer pageSize) throws Exception {
getRequest().ifPresent(request -> {
for (MediaType mediaType: MediaType.parseMediaTypes(request.getHeader("Accept"))) {
if (mediaType.isCompatibleWith(MediaType.valueOf("application/json"))) {
String exampleString = "{ \"countries\" : [ { \"alpha3Code\" : \"alpha3Code\", \"englishName\" : \"englishName\", \"alpha2Code\" : \"alpha2Code\", \"internetCctld\" : \"internetCctld\", \"frenchName\" : \"frenchName\", \"portugueseName\" : \"portugueseName\", \"numericCode\" : 75 }, { \"alpha3Code\" : \"alpha3Code\", \"englishName\" : \"englishName\", \"alpha2Code\" : \"alpha2Code\", \"internetCctld\" : \"internetCctld\", \"frenchName\" : \"frenchName\", \"portugueseName\" : \"portugueseName\", \"numericCode\" : 75 }, { \"alpha3Code\" : \"alpha3Code\", \"englishName\" : \"englishName\", \"alpha2Code\" : \"alpha2Code\", \"internetCctld\" : \"internetCctld\", \"frenchName\" : \"frenchName\", \"portugueseName\" : \"portugueseName\", \"numericCode\" : 75 }, { \"alpha3Code\" : \"alpha3Code\", \"englishName\" : \"englishName\", \"alpha2Code\" : \"alpha2Code\", \"internetCctld\" : \"internetCctld\", \"frenchName\" : \"frenchName\", \"portugueseName\" : \"portugueseName\", \"numericCode\" : 75 }, { \"alpha3Code\" : \"alpha3Code\", \"englishName\" : \"englishName\", \"alpha2Code\" : \"alpha2Code\", \"internetCctld\" : \"internetCctld\", \"frenchName\" : \"frenchName\", \"portugueseName\" : \"portugueseName\", \"numericCode\" : 75 } ] }";
ApiUtil.setExampleResponse(request, "application/json", exampleString);
break;
}
}
});
return new ResponseEntity<>(HttpStatus.NOT_IMPLEMENTED);
}
/**
* GET /v1/countries/{numeric-code} : GET Country.
* GET Country.
*
* @param numericCode ISO 3166-1 numeric codes are three-digit (left padded with zero) country codes defined in ISO 3166-1. (required)
* @return 200 OK. (status code 200)
* or 404 Not Found. (status code 200)
* @see CountriesApi#getCountry
*/
default ResponseEntity<CountriesResponseDTO> getCountry(Integer numericCode) throws Exception {//BUG: Here should be ResponseEntity<CountryResponseDTO>
getRequest().ifPresent(request -> {
for (MediaType mediaType: MediaType.parseMediaTypes(request.getHeader("Accept"))) {
if (mediaType.isCompatibleWith(MediaType.valueOf("application/json"))) {
String exampleString = "{ \"countries\" : [ { \"alpha3Code\" : \"alpha3Code\", \"englishName\" : \"englishName\", \"alpha2Code\" : \"alpha2Code\", \"internetCctld\" : \"internetCctld\", \"frenchName\" : \"frenchName\", \"portugueseName\" : \"portugueseName\", \"numericCode\" : 75 }, { \"alpha3Code\" : \"alpha3Code\", \"englishName\" : \"englishName\", \"alpha2Code\" : \"alpha2Code\", \"internetCctld\" : \"internetCctld\", \"frenchName\" : \"frenchName\", \"portugueseName\" : \"portugueseName\", \"numericCode\" : 75 }, { \"alpha3Code\" : \"alpha3Code\", \"englishName\" : \"englishName\", \"alpha2Code\" : \"alpha2Code\", \"internetCctld\" : \"internetCctld\", \"frenchName\" : \"frenchName\", \"portugueseName\" : \"portugueseName\", \"numericCode\" : 75 }, { \"alpha3Code\" : \"alpha3Code\", \"englishName\" : \"englishName\", \"alpha2Code\" : \"alpha2Code\", \"internetCctld\" : \"internetCctld\", \"frenchName\" : \"frenchName\", \"portugueseName\" : \"portugueseName\", \"numericCode\" : 75 }, { \"alpha3Code\" : \"alpha3Code\", \"englishName\" : \"englishName\", \"alpha2Code\" : \"alpha2Code\", \"internetCctld\" : \"internetCctld\", \"frenchName\" : \"frenchName\", \"portugueseName\" : \"portugueseName\", \"numericCode\" : 75 } ] }";
ApiUtil.setExampleResponse(request, "application/json", exampleString);
break;
}
}
});
return new ResponseEntity<>(HttpStatus.NOT_IMPLEMENTED);
}
}
Aditional observation
If only one of the endpoints is specified the expected behavior is the desired one. I.e:
# specification.yaml:
# ...
paths:
/v1/countries:
#
# Escaped forward-slash is necessary when using JSON references.
# https://spec.openapis.org/oas/latest.html#operationref-examples
$ref: 'components/pathItems/v1/countries.yaml#/getCountries'
#/v1/countries/{numeric-code}:
#
# Escaped forward-slash is necessary when using JSON references.
# https://spec.openapis.org/oas/latest.html#operationref-examples
#$ref: 'components/pathItems/v1/countries.yaml#/getCountry'
tags:
- name: countries
description: Countries resource related.
or:
# specification.yaml:
paths:
#/v1/countries:
#
# Escaped forward-slash is necessary when using JSON references.
# https://spec.openapis.org/oas/latest.html#operationref-examples
#$ref: 'components/pathItems/v1/countries.yaml#/getCountries'
/v1/countries/{numeric-code}:
#
# Escaped forward-slash is necessary when using JSON references.
# https://spec.openapis.org/oas/latest.html#operationref-examples
$ref: 'components/pathItems/v1/countries.yaml#/getCountry'
tags:
- name: countries
description: Countries resource related.
NOTE!: The same behavior of Java classes is also observed in the canonical specification.
Related issues/PRs
I looked for issues and PRs related to this behavior but found nothing.
Suggest a fix/enhancement
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
Start with the supplied OpenAPI specification and the Java Spring generator invoked through org.openapitools:openapi-generator-maven-plugin:6.0.0, using the linked reproduction repository if needed. Run generation for the countries tag and compare the generated response types for the list and single-item endpoints; done means they reflect their distinct schemas rather than sharing one type.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- java, openapi, spring
- Domain
- api, tooling
- Issue type
- Bug
- Difficulty
- 4/5
- Estimated time
- 3-5 days
- Activity status
- Stale
- Clarity
- Mostly clear
- Newbie friendliness
- 35/100