2

I use open api generator (gradle's implementation) to generate controllers for my API in Java, but if my endpoint return nothing - OpenAPI generator generates return type as object type Void, but not as void.

I expect:

public void createPet() {}

But got:

public Void createPet() { return null; }

I tried to customize mustache template, but there was no any options for that. ConfigOptions in gradle task's configuration also doesn't have options for that.

Current .mustache api template:

/**
* NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech) ({{{generatorVersion}}}).
* https://openapi-generator.tech
* Do not edit the class manually.
*/
package {{package}};

{{#imports}}import {{import}};
{{/imports}}
{{#swagger2AnnotationLibrary}}
    import io.swagger.v3.oas.annotations.Operation;
    import io.swagger.v3.oas.annotations.Parameter;
    import io.swagger.v3.oas.annotations.Parameters;
    import io.swagger.v3.oas.annotations.media.Content;
    import io.swagger.v3.oas.annotations.media.Schema;
    import io.swagger.v3.oas.annotations.responses.ApiResponse;
    import io.swagger.v3.oas.annotations.security.SecurityRequirement;
    import io.swagger.v3.oas.annotations.tags.Tag;
{{/swagger2AnnotationLibrary}}
{{#swagger1AnnotationLibrary}}
    import io.swagger.annotations.*;
{{/swagger1AnnotationLibrary}}
{{#jdk8-no-delegate}}
    {{#virtualService}}
        import io.virtualan.annotation.ApiVirtual;
        import io.virtualan.annotation.VirtualService;
    {{/virtualService}}
    import org.springframework.http.HttpStatus;
    import org.springframework.http.MediaType;
{{/jdk8-no-delegate}}
{{#useBeanValidation}}
    import org.springframework.validation.annotation.Validated;
{{/useBeanValidation}}
{{#useSpringController}}
    import org.springframework.stereotype.Controller;
{{/useSpringController}}
import org.springframework.web.bind.annotation.*;
{{#jdk8-no-delegate}}
    {{^reactive}}
        import org.springframework.web.context.request.NativeWebRequest;
    {{/reactive}}
{{/jdk8-no-delegate}}
import org.springframework.web.multipart.MultipartFile;
{{#reactive}}
    import org.springframework.web.server.ServerWebExchange;
    import reactor.core.publisher.Flux;
    import reactor.core.publisher.Mono;
    import org.springframework.http.codec.multipart.Part;
{{/reactive}}

{{#useBeanValidation}}
    import javax.validation.Valid;
    import javax.validation.constraints.*;
{{/useBeanValidation}}
import java.util.List;
import java.util.Map;
{{#jdk8-no-delegate}}
    import java.util.Optional;
{{/jdk8-no-delegate}}
{{^jdk8-no-delegate}}
    {{#useOptional}}
        import java.util.Optional;
    {{/useOptional}}
{{/jdk8-no-delegate}}
{{#async}}
    import java.util.concurrent.{{^jdk8}}Callable{{/jdk8}}{{#jdk8}}CompletableFuture{{/jdk8}};
{{/async}}
import javax.annotation.Generated;

{{>generatedAnnotation}}
{{#useBeanValidation}}
    @Validated
{{/useBeanValidation}}
{{#useSpringController}}
    @Controller
{{/useSpringController}}
{{#swagger2AnnotationLibrary}}
    @Tag(name = "{{{baseName}}}", description = "the {{{baseName}}} API")
{{/swagger2AnnotationLibrary}}
{{#swagger1AnnotationLibrary}}
    @Api(value = "{{{baseName}}}", description = "the {{{baseName}}} API")
{{/swagger1AnnotationLibrary}}
{{#operations}}
    {{#virtualService}}
        @VirtualService
    {{/virtualService}}
    public interface {{classname}} {
    {{#jdk8-default-interface}}
        {{^isDelegate}}
            {{^reactive}}

                default Optional<NativeWebRequest> getRequest() {
                    return Optional.empty();
                    }
            {{/reactive}}
        {{/isDelegate}}
        {{#isDelegate}}

                default {{classname}}Delegate getDelegate() {
                return new {{classname}}Delegate() {};
                }
        {{/isDelegate}}
    {{/jdk8-default-interface}}
    {{#operation}}

            /**
            * {{httpMethod}} {{{path}}}{{#summary}} : {{.}}{{/summary}}
        {{#notes}}
                * {{.}}
        {{/notes}}
            *
        {{#allParams}}
                * @param {{paramName}} {{description}}{{#required}} (required){{/required}}{{^required}} (optional{{#defaultValue}}, default to {{.}}{{/defaultValue}}){{/required}}
        {{/allParams}}
            * @return {{#responses}}{{message}} (status code {{code}}){{^-last}}
                *         or {{/-last}}{{/responses}}
        {{#isDeprecated}}
                * @deprecated
        {{/isDeprecated}}
        {{#externalDocs}}
                * {{description}}
                * @see <a href="{{url}}">{{summary}} Documentation</a>
        {{/externalDocs}}
            */
        {{#virtualService}}
                @ApiVirtual
        {{/virtualService}}
        {{#swagger2AnnotationLibrary}}
                @Operation(
                operationId = "{{{operationId}}}",
            {{#summary}}
                    summary = "{{{.}}}",
            {{/summary}}
            {{#vendorExtensions.x-tags}}
                    tags = { {{#vendorExtensions.x-tags}}"{{tag}}"{{^-last}}, {{/-last}}{{/vendorExtensions.x-tags}} },
            {{/vendorExtensions.x-tags}}
                responses = {
            {{#responses}}
                    @ApiResponse(responseCode = "{{{code}}}", description = "{{{message}}}"{{#baseType}}, content = @Content(mediaType = "application/json", schema = @Schema(implementation =  {{{baseType}}}.class)){{/baseType}}){{^-last}},{{/-last}}
            {{/responses}}
                }{{#hasAuthMethods}},
                security = {
            {{#authMethods}}
                    @SecurityRequirement(name = "{{name}}"{{#isOAuth}}, scopes={ {{#scopes}}"{{scope}}"{{^-last}}, {{/-last}}{{/scopes}} }{{/isOAuth}}){{^-last}},{{/-last}}
            {{/authMethods}}
                }{{/hasAuthMethods}}
                )
        {{/swagger2AnnotationLibrary}}
        {{#swagger1AnnotationLibrary}}
                @ApiOperation(
                tags = { {{#vendorExtensions.x-tags}}"{{tag}}"{{^-last}}, {{/-last}}{{/vendorExtensions.x-tags}} },
                value = "{{{summary}}}",
                nickname = "{{{operationId}}}",
                notes = "{{{notes}}}"{{#returnBaseType}},
                response = {{{.}}}.class{{/returnBaseType}}{{#returnContainer}},
                responseContainer = "{{{.}}}"{{/returnContainer}}{{#hasAuthMethods}},
                authorizations = {
            {{#authMethods}}
                {{#isOAuth}}
                        @Authorization(value = "{{name}}", scopes = {
                    {{#scopes}}
                            @AuthorizationScope(scope = "{{scope}}", description = "{{description}}"){{^-last}},{{/-last}}
                    {{/scopes}}
                        }){{^-last}},{{/-last}}
                {{/isOAuth}}
                {{^isOAuth}}
                        @Authorization(value = "{{name}}"){{^-last}},{{/-last}}
                {{/isOAuth}}
            {{/authMethods}} }{{/hasAuthMethods}}
                )
                @ApiResponses({
            {{#responses}}
                    @ApiResponse(code = {{{code}}}, message = "{{{message}}}"{{#baseType}}, response = {{{.}}}.class{{/baseType}}{{#containerType}}, responseContainer = "{{{.}}}"{{/containerType}}){{^-last}},{{/-last}}
            {{/responses}}
                })
        {{/swagger1AnnotationLibrary}}
        {{#implicitHeaders}}
            {{#swagger2AnnotationLibrary}}
                    @Parameters({
                {{#headerParams}}
                    {{>paramDoc}}{{^-last}},{{/-last}}
                {{/headerParams}}
            {{/swagger2AnnotationLibrary}}
            {{#swagger1AnnotationLibrary}}
                    @ApiImplicitParams({
                {{#headerParams}}
                    {{>implicitHeader}}{{^-last}},{{/-last}}
                {{/headerParams}}
            {{/swagger1AnnotationLibrary}}
                })
        {{/implicitHeaders}}
            @RequestMapping(
            method = RequestMethod.{{httpMethod}},
            value = "{{{path}}}"{{#singleContentTypes}}{{#hasProduces}},
            produces = "{{{vendorExtensions.x-accepts}}}"{{/hasProduces}}{{#hasConsumes}},
            consumes = "{{{vendorExtensions.x-contentType}}}"{{/hasConsumes}}{{/singleContentTypes}}{{^singleContentTypes}}{{#hasProduces}},
            produces = { {{#produces}}"{{{mediaType}}}"{{^-last}}, {{/-last}}{{/produces}} }{{/hasProduces}}{{#hasConsumes}},
            consumes = { {{#consumes}}"{{{mediaType}}}"{{^-last}}, {{/-last}}{{/consumes}} }{{/hasConsumes}}{{/singleContentTypes}}
            )
        {{#jdk8-default-interface}}default {{/jdk8-default-interface}}{{#responseWrapper}}{{.}}<{{/responseWrapper}}{{>returnTypes}}{{#responseWrapper}}>{{/responseWrapper}} {{#delegate-method}}_{{/delegate-method}}{{operationId}}(
        {{#allParams}}{{>queryParams}}{{>pathParams}}{{>headerParams}}{{>bodyParams}}{{>formParams}}{{>cookieParams}}{{^-last}},
        {{/-last}}{{/allParams}}{{#reactive}}{{#hasParams}},
        {{/hasParams}}{{#swagger2AnnotationLibrary}}@Parameter(hidden = true){{/swagger2AnnotationLibrary}}{{#springFoxDocumentationProvider}}@ApiIgnore{{/springFoxDocumentationProvider}} final ServerWebExchange exchange{{/reactive}}{{#vendorExtensions.x-spring-paginated}}{{#hasParams}},
        {{/hasParams}}{{#springFoxDocumentationProvider}}@ApiIgnore {{/springFoxDocumentationProvider}}{{#springDocDocumentationProvider}}@ParameterObject {{/springDocDocumentationProvider}}final Pageable pageable{{/vendorExtensions.x-spring-paginated}}
            ){{^jdk8-default-interface}};{{/jdk8-default-interface}}{{#jdk8-default-interface}}{{#unhandledException}} throws Exception{{/unhandledException}} {
        {{#delegate-method}}
                return {{operationId}}({{#allParams}}{{paramName}}{{^-last}}, {{/-last}}{{/allParams}}{{#reactive}}{{#hasParams}}, {{/hasParams}}exchange{{/reactive}}{{#vendorExtensions.x-spring-paginated}}, pageable{{/vendorExtensions.x-spring-paginated}});
                }

                // Override this method
            {{#jdk8-default-interface}}default {{/jdk8-default-interface}} {{#responseWrapper}}{{.}}<{{/responseWrapper}}{{>returnTypes}}{{#responseWrapper}}>{{/responseWrapper}} {{operationId}}({{#allParams}}{{^isFile}}{{^isBodyParam}}{{>optionalDataType}}{{/isBodyParam}}{{#isBodyParam}}{{^reactive}}{{{dataType}}}{{/reactive}}{{#reactive}}{{^isArray}}Mono<{{{dataType}}}>{{/isArray}}{{#isArray}}Flux<{{{baseType}}}>{{/isArray}}{{/reactive}}{{/isBodyParam}}{{/isFile}}{{#isFile}}{{#reactive}}Flux<Part>{{/reactive}}{{^reactive}}MultipartFile{{/reactive}}{{/isFile}} {{paramName}}{{^-last}}, {{/-last}}{{/allParams}}{{#reactive}}{{#hasParams}}, {{/hasParams}}{{#springFoxDocumentationProvider}}@ApiIgnore{{/springFoxDocumentationProvider}} final ServerWebExchange exchange{{/reactive}}{{#vendorExtensions.x-spring-paginated}}, {{#springFoxDocumentationProvider}}@ApiIgnore{{/springFoxDocumentationProvider}} final Pageable pageable{{/vendorExtensions.x-spring-paginated}}){{#unhandledException}} throws Exception{{/unhandledException}} {
        {{/delegate-method}}
        {{^isDelegate}}
            {{>methodBody}}
        {{/isDelegate}}
        {{#isDelegate}}
                return getDelegate().{{operationId}}({{#allParams}}{{paramName}}{{^-last}}, {{/-last}}{{/allParams}}{{#reactive}}{{#hasParams}}, {{/hasParams}}exchange{{/reactive}}{{#vendorExtensions.x-spring-paginated}}, pageable{{/vendorExtensions.x-spring-paginated}});
        {{/isDelegate}}
            }{{/jdk8-default-interface}}

    {{/operation}}
        }
{{/operations}}

generate task example in gradle.build

openApiGenerate {
    generatorName = "spring"
    inputSpec = "$projectDir/src/main/resources/swagger/api.yaml".toString()
    outputDir = "$buildDir/generated".toString()
    apiPackage = "org.example.api"
    modelPackage = "org.example.resource"
    templateDir = "$projectDir/src/main/resources/templates/".toString()

    configOptions = [
        java8: "false",
        serializableModel: "true",
        interfaceOnly: "true",
    ]
}


api.yaml

swagger: "2.0"
info:
  version: 1.0.0
  title: Swagger Petstore
  license:
    name: MIT
host: petstore.swagger.io
basePath: /v1
schemes:
  - http
consumes:
  - application/json
produces:
  - application/json
paths:
  /pets:
    get:
      summary: List all pets
      operationId: listPets
      tags:
        - pets
      parameters:
        - name: limit
          in: query
          description: How many items to return at one time (max 100)
          required: false
          type: integer
          format: int32
      responses:
        "200":
          description: A paged array of pets
          headers:
            x-next:
              type: string
              description: A link to the next page of responses
          schema:
            $ref: '#/definitions/Pets'
        default:
          description: unexpected error
          schema:
            $ref: '#/definitions/Error'
    post:
      summary: Create a pet
      operationId: createPets
      tags:
        - pets
      responses:
        200:
          description: Ok
  /pets/{petId}:
    get:
      summary: Info for a specific pet
      operationId: showPetById
      tags:
        - pets
      parameters:
        - name: petId
          in: path
          required: true
          description: The id of the pet to retrieve
          type: string
      responses:
        "200":
          description: Expected response to a valid request
          schema:
            $ref: '#/definitions/Pets'
        default:
          description: unexpected error
          schema:
            $ref: '#/definitions/Error'
definitions:
  Pet:
    type: "object"
    required:
      - id
      - name
    properties:
      id:
        type: integer
        format: int64
      name:
        type: string
      tag:
        type: string
  Pets:
    type: array
    items:
      $ref: '#/definitions/Pet'
  Error:
    type: "object"
    required:
      - code
      - message
    properties:
      code:
        type: integer
        format: int32
      message:
        type: string

2 Answers2

3

Introduction

Let's consider the 5.3.1 version of openapi-generator as the current version.

Summary

It does not seem to be feasible to force the generator to use the void return type instead of Void.

As a last resort, it may be considered to create a fork of the generator and implement the desired behavior.

Details

Already requested feature

The feature is already requested by the GitHub issue: Change return types for Spring openapi-generator-maven-plugin generated interfaces · Issue #6135 · OpenAPITools/openapi-generator.

The related question: java - Change return types for Spring openapi-generator-maven-plugin generated interfaces - Stack Overflow.

Source code

The spring generator (generatorName: spring) is represented by the org.openapitools.codegen.languages.SpringCodegen class.

The Void type detection is implemented by the SpringCodegen class.

Please, see the related part of the source code: openapi-generator/SpringCodegen.java at v5.3.1 · OpenAPITools/openapi-generator:

private void doDataTypeAssignment(String returnType, DataTypeAssigner dataTypeAssigner) {
    final String rt = returnType;
    if (rt == null) {
        dataTypeAssigner.setReturnType("Void");
    } else if (rt.startsWith("List")) {
-1

I found the workaround for this. I've used "find and replace task" for it.

  1. Generate API using openApiGenerate task
  2. Find and replace "Void" type with void

How to find and replace:

Gradle task replace string in .java file