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
21.65k stars 6.54k forks source link

[BUG][Java Spring] Generating wrong type for response when list and single responses under same tag #15768

Open kinlhp opened 1 year ago

kinlhp commented 1 year ago
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: &notFound-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
  1. Define all the specifications described in the files above
  2. 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

borsch commented 1 year ago

@kinlhp don't get me wrong but this is slightly to complex spec to analyze :sweat_smile: Can you provide smaller/shorter spec + generation configuration so that we would be able to reproduce this issue?

kinlhp commented 1 year ago

No problem, I just added the smallest details in case someone needs to have my exact scenario.

The problem, from the little I've analyzed, is that when there is more than one response data structure in the same external .yaml file, the generated Java code is not correct.

For example, I have the following two paths in the same tag:

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.

The expected code is:

/**
 * GET /v1/countries : GET Countries.
 * ...
 */
default ResponseEntity<CountriesResponseDTO> getCountries(Integer pageIndex, Integer pageSize) throws Exception {/* ... */}

/**
 * GET /v1/countries/{numeric-code} : GET Country.
 * ...
 */
default ResponseEntity<CountryResponseDTO> getCountry(Integer numericCode) throws Exception {/* ... */}

But the generated code is:

/**
 * GET /v1/countries : GET Countries.
 * ...
 */
default ResponseEntity<CountriesResponseDTO> getCountries(Integer pageIndex, Integer pageSize) throws Exception {/* ... */}

/**
 * GET /v1/countries/{numeric-code} : GET Country.
 * ...
 */
default ResponseEntity<CountriesResponseDTO> getCountry(Integer numericCode) throws Exception {/* ... */}

As a plugin configuration, I have the following defined:

<!-- openapitools -->
<plugin>
    <groupId>org.openapitools</groupId>
    <artifactId>openapi-generator-maven-plugin</artifactId>
    <version>${openapi-generator-maven-plugin.version}</version>
    <executions>
        <execution>
            <id>default-generate</id>
            <phase>${openapi-generator.phase}</phase>
            <goals>
                <goal>generate</goal>
            </goals>
            <configuration>
                <!-- README#openapi-generator-spring-metadata -->
                <generatorName>spring</generatorName>
                <!-- README#openapi-generator-spring-config-options -->
                <apiPackage>${openapi-generator.base-package}.endpoint</apiPackage>
                <artifactId>${project.artifactId}</artifactId>
                <artifactVersion>${revision}</artifactVersion>
                <generateApiTests>false</generateApiTests>
                <generateModelTests>false</generateModelTests>
                <groupId>${project.groupId}</groupId>
                <ignoreFileOverride>.openapi-generator-ignore</ignoreFileOverride>
                <inputSpec>${openapi-generator.input-spec}</inputSpec>
                <invokerPackage>${openapi-generator.base-package}.invoker</invokerPackage>
                <modelNameSuffix>DTO</modelNameSuffix>
                <modelPackage>${openapi-generator.base-package}.payload</modelPackage>
                <configOptions>
                    <!-- README#openapi-generator-spring-metadata -->
                    <generatorLanguage>Java</generatorLanguage>
                    <generatorType>SERVER</generatorType>
                    <!-- README#openapi-generator-spring-config-options -->
                    <artifactDescription>${project.description}</artifactDescription>
                    <artifactUrl>${project.url}</artifactUrl>
                    <basePackage>${openapi-generator.base-package}</basePackage>
                    <bigDecimalAsString>true</bigDecimalAsString>
                    <booleanGetterPrefix>is</booleanGetterPrefix>
                    <configPackage>${openapi-generator.base-package}.configuration</configPackage>
                    <delegatePattern>true</delegatePattern>
                    <developerEmail>lhp.kin@gmail.com</developerEmail>
                    <developerName>Luis Henrique Pereira</developerName>
                    <developerOrganization>${project.organization.name}</developerOrganization>
                    <developerOrganizationUrl>${project.organization.url}</developerOrganizationUrl>
                    <licenseName>${license.name}</licenseName>
                    <licenseUrl>${license.url}</licenseUrl>
                    <parentArtifactId>${project.parent.artifactId}</parentArtifactId>
                    <parentGroupId>${project.parent.groupId}</parentGroupId>
                    <parentVersion>${revision}</parentVersion>
                    <performBeanValidation>true</performBeanValidation>
                    <scmConnection>${project.scm.connection}</scmConnection>
                    <scmDeveloperConnection>${project.scm.developerConnection}</scmDeveloperConnection>
                    <scmUrl>${project.scm.url}</scmUrl>
                    <serializableModel>true</serializableModel>
                    <title>${project.description}</title>
                    <unhandledException>true</unhandledException>
                    <useSpringBoot3>true</useSpringBoot3>
                    <useSpringController>true</useSpringController>
                    <useTags>true</useTags>
                </configOptions>
            </configuration>
        </execution>
    </executions>
</plugin>