Generate API's from swagger codegen without wrapping Flux<T> with Mono

Viewed 51

I'm creating a spring boot application using spring-webflux and generating the api's using swagger code generator. also using custom codegen template to control the output format. and i face the following undesired behaviour:-

in my openapi.yml file i defined an endpoint that returns an array, so i was expecting that the generated api interface will have a return type Flux<T> but i found that it wraps the return type with Mono as follows: Mono<Flux<T>>

How to prevent that wrapping so that i don't have to do an additional work in my API implementation ?

Here is my code:-

openapi.yml

openapi: 3.0.3
info:
  title: "instagram-post-service"
  description: ""
  termsOfService: ""
  version: 1.0.0
externalDocs:
  description: ""
  url: ""
servers:
  - url: http://localhost:8080
    description: Generated server url
tags:
  - name: Post
    description: Operations about posts
    externalDocs:
      description: ""
      url: ""
  - name: Comment
    description: Operations about comments
    externalDocs:
      description: ""
      url: ""
    
paths:
  /posts/:                  
    post:
      tags:
        - Post
      summary: Create a new Post
      description: ''
      operationId: createPost
      requestBody:
        description: ''
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/Post'
        required: true
      responses:
        '200':
          description: Successful operation
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Post'     

  /posts/findByUserId:
    get:
      tags:
        - Post
      summary: Finds posts by userId
      description: ''
      operationId: findPostsByUserId
      parameters:
        - name: userId
          in: query
          description: ''
          required: true
          schema:
            type: integer
            format: int32
      responses:
        '200':
          description: successful operation
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: '#/components/schemas/Post'          
        '400':
          description: Invalid userId


components:
  schemas:
    Post:
      type: object
      properties:
        id:
          type: integer
          format: int32
        title:
          type: string
        userId:
          type: integer    
          format: int32
        serviceAddress:
          type: string      

api.mustache

/**
 * 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}}
import org.springframework.http.ResponseEntity;
{{#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.CompletableFuture;
{{/async}}
import javax.annotation.processing.Generated;

{{>generatedAnnotation}}
{{#useBeanValidation}}
@Validated
{{/useBeanValidation}}
{{#useSpringController}}
@Controller
{{/useSpringController}}
{{#swagger2AnnotationLibrary}}
@Tag(name = "{{{baseName}}}", description = {{#tagDescription}}"{{{.}}}"{{/tagDescription}}{{^tagDescription}}"the {{{baseName}}} API"{{/tagDescription}})
{{/swagger2AnnotationLibrary}}
{{#swagger1AnnotationLibrary}}
@Api(value = "{{{baseName}}}", description = {{#tagDescription}}"{{{.}}}"{{/tagDescription}}{{^tagDescription}}"the {{{baseName}}} API"{{/tagDescription}})
{{/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}}
        {{#description}}
        description= "{{{.}}}",
        {{/description}}
        {{#vendorExtensions.x-tags.size}}
        tags = { {{#vendorExtensions.x-tags}}"{{tag}}"{{^-last}}, {{/-last}}{{/vendorExtensions.x-tags}} },
        {{/vendorExtensions.x-tags.size}}
        responses = {
            {{#responses}}
            @ApiResponse(responseCode = "{{{code}}}", description = "{{{message}}}"{{#baseType}}, content = {
                {{#produces}}
                @Content(mediaType = "{{{mediaType}}}", schema = @Schema(implementation = {{{baseType}}}.class)){{^-last}},{{/-last}}
                {{/produces}}
            }{{/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(
        {{#vendorExtensions.x-tags.size}}
        tags = { {{#vendorExtensions.x-tags}}"{{tag}}"{{^-last}}, {{/-last}}{{/vendorExtensions.x-tags}} },
        {{/vendorExtensions.x-tags.size}}
        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}}
    {{#implicitHeadersParams.0}}
    {{#swagger2AnnotationLibrary}}
    @Parameters({
        {{#implicitHeadersParams}}
        {{>paramDoc}}{{^-last}},{{/-last}}
        {{/implicitHeadersParams}}
    {{/swagger2AnnotationLibrary}}
    {{#swagger1AnnotationLibrary}}
    @ApiImplicitParams({
        {{#implicitHeadersParams}}
        {{>implicitHeader}}{{^-last}},{{/-last}}
        {{/implicitHeadersParams}}
    {{/swagger1AnnotationLibrary}}
    })
    {{/implicitHeadersParams.0}}
    @{{#lambda.titlecase}}{{#lambda.lowercase}}{{httpMethod}}{{/lambda.lowercase}}{{/lambda.titlecase}}Mapping(
        value = "{{{path}}}"{{#singleContentTypes}}{{#hasProduces}},
        produces = "{{{vendorExtensions.x-accepts}}}"{{/hasProduces}}{{#hasConsumes}},
        consumes = "{{{vendorExtensions.x-contentType}}}"{{/hasConsumes}}{{/singleContentTypes}}{{^singleContentTypes}}{{#hasProduces}},
        produces = { {{#produces}}"{{{mediaType}}}"{{#hasMore}}, {{/hasMore}}{{/produces}} }{{/hasProduces}}{{#hasConsumes}},
        consumes = { {{#consumes}}"{{{mediaType}}}"{{#hasMore}}, {{/hasMore}}{{/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}}
    ){{#unhandledException}} throws Exception{{/unhandledException}}{{^jdk8-default-interface}};{{/jdk8-default-interface}}{{#jdk8-default-interface}} {
        {{#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}}ResponseEntity<{{>returnTypes}}>>{{#responseWrapper}}{{/responseWrapper}} _{{operationId}}({{#allParams}}{{^isFile}}{{^isBodyParam}}{{>optionalDataType}}{{/isBodyParam}}{{#isBodyParam}}{{^reactive}}{{{dataType}}}{{/reactive}}{{#reactive}}{{^isArray}}{{{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}}

bodyParams.mustache

{{#isBodyParam}}@Parameter(name = "{{{description}}}" {{#required}},required=true{{/required}} {{^isContainer}}{{#allowableValues}}, allowableValues="{{{allowableValues}}}"{{/allowableValues}}{{/isContainer}}{{#defaultValue}}, defaultValue="{{{defaultValue}}}"{{/defaultValue}})  {{#useBeanValidation}}@Valid{{/useBeanValidation}} @RequestBody {{{dataType}}} {{paramName}}{{/isBodyParam}}

pom.xml

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.5.2</version>
        <relativePath /> <!-- lookup parent from repository -->
    </parent>

    <groupId>com.javaworld.instagram</groupId>
    <artifactId>post-service</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>post-service</name>
    <description>instagram posts micro service</description>

    <properties>
        <java.version>1.8</java.version>
        <swagger-annotations-version>1.6.6</swagger-annotations-version>
        <jackson-databind-nullable>0.2.1</jackson-databind-nullable>
        <org.mapstruct.version>1.3.1.Final</org.mapstruct.version>
    </properties>

    <dependencies>
        
        
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-webflux</artifactId>
        </dependency>
                
        <!-- ################### Testing dependencies ################################ -->  
    
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
            <exclusions>
                <exclusion>
                    <groupId>org.junit.vintage</groupId>
                    <artifactId>junit-vintage-engine</artifactId>
                </exclusion>
            </exclusions>
        </dependency>
    
        <dependency>
            <groupId>io.projectreactor</groupId>
            <artifactId>reactor-test</artifactId>
        </dependency>       
        
        <!-- ########################### DB & persistence layer dependencies ################################ -->

        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
        </dependency>
                
    
        <dependency>
            <groupId>org.hibernate</groupId>
            <artifactId>hibernate-core</artifactId>
        </dependency>

        <dependency>
            <groupId>org.hibernate</groupId>
            <artifactId>hibernate-validator</artifactId>
            <version>6.1.2.Final</version>
        </dependency>
        
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-jpa</artifactId>
        </dependency>

        <dependency>
            <groupId>com.h2database</groupId>
            <artifactId>h2</artifactId>
        </dependency>
        
        <!-- ################### dependencies needed for open-API code generator ################################ -->
        
        <dependency>
            <groupId>io.swagger</groupId>
            <artifactId>swagger-annotations</artifactId>
            <version>${swagger-annotations-version}</version>
        </dependency>

        <dependency>
            <groupId>org.openapitools</groupId>
            <artifactId>jackson-databind-nullable</artifactId>
            <version>${jackson-databind-nullable}</version>
        </dependency>

        <dependency>
            <groupId>io.springfox</groupId>
            <artifactId>springfox-boot-starter</artifactId>
            <version>3.0.0</version>
        </dependency>


        <!-- ################################ MapStruct ################################################ -->

        <dependency>
            <groupId>org.mapstruct</groupId>
            <artifactId>mapstruct</artifactId>
            <version>${org.mapstruct.version}</version>
        </dependency>


        <dependency>
            <groupId>org.mapstruct</groupId>
            <artifactId>mapstruct-processor</artifactId>
            <version>${org.mapstruct.version}</version>
        </dependency>

        <dependency>
            <groupId>javax.validation</groupId>
            <artifactId>validation-api</artifactId>
            <version>1.1.0.Final</version>
        </dependency>

    </dependencies>

    <build>
        <plugins>

            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
                <configuration>
                    <layers>
                        <enabled>true</enabled>
                        <!-- <configuration>${project.basedir}/src/layers.xml</configuration> -->
                    </layers>
                </configuration>
            </plugin>


            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-compiler-plugin</artifactId>
                <version>3.8.1</version>
                <configuration>
                    <source>1.8</source> 
                    <target>1.8</target> 
                    <annotationProcessorPaths>
                        <path>
                            <groupId>org.mapstruct</groupId>
                            <artifactId>mapstruct-processor</artifactId>
                            <version>${org.mapstruct.version}</version>
                        </path>
                        <!-- other annotation processors -->
                    </annotationProcessorPaths>
                    
                </configuration>
            </plugin>

            <!-- open API plug-in for API code generator -->
            <plugin>
                <groupId>org.openapitools</groupId>
                <artifactId>openapi-generator-maven-plugin</artifactId>
                <version>6.0.1</version>
                <executions>
                    <execution>
                        <goals>
                            <goal>generate</goal>
                        </goals>
                        <configuration>
                            <inputSpec>
                                ${project.basedir}/src/main/resources/openapi.yml
                            </inputSpec>
                            <templateDirectory>src/main/resources/openapi-templates</templateDirectory>
                            <generatorName>spring</generatorName>
                            <apiPackage>com.javaworld.instagram.postservice.server.api</apiPackage>
                            <modelPackage>com.javaworld.instagram.postservice.server.dto</modelPackage>
                            <modelNameSuffix>ApiDto</modelNameSuffix>
                            <configOptions>
                                <interfaceOnly>true</interfaceOnly>
                                <skipDefaultInterface>true</skipDefaultInterface>
                                <reactive>true</reactive>
                            </configOptions>

                        </configuration>
                    </execution>
                </executions>
            </plugin>
        </plugins>
    </build>

</project>

0 Answers
Related