Spring OpenApi中GET请求的DTOs

huangapple go评论68阅读模式
英文:

Spring OpenApi DTOs in GET requests

问题

We use OpenAPI to generate the interface description for the services exposed in one of our server applications. We use that openapi.yaml then to generate client-sided libraries that will interact with these services for various languages. As the server backend uses the typical Maven + Java Spring Boot techstack we use springdoc-openapi-maven-plugin to generate the interface description and openapi-generator-maven-plugin to generate the client library project for Java based clients.

This setup works in general but for one of our services which allows the management of various tickets we have a couple of business methods that operate on a provided search filter. This is implemented through a DTO that has roughly 30+ properties that can be specified.

Within a Spring typical RestController named TicketController one of the methods is now defining a GET endpoint for querying a list of tickets that match the given search filter. In Java Spring syntax this is implemented as such:

@GetMapping
public Page<Ticket> getTickets(
    @ParameterObject TicketSearchFilterDTO filterDto,
    @ParameterObject @PageableDefault(size = 25) Pageable pageable
) {
  log.info("Getting tickets, filter {}, pageable {}", filterDto, pageable);
  TicketSearchFilter filter = ticketSearchFilterDTOConverter.convert(filterDto);
  return ticketSearchRepository.getTickets(filter, pageable);
}

which results in a OpenAPI interface definition like...

When generating the Java client code for this definition I will have a TicketControllerApi class available that provides the mentioned getTickets method. But instead of providing a TicketSearchFilterDTO as input parameter it exposes all of the 30+ possible search parameters as arguments to that method.

openApiClient.getTicketControllerApi().getTickets("abc123", ..., 0, 25, List.of());

where the last 3 arguments represent the Pageable parameters for page, size, and sort order. As we have so many search parameters, nulling out all of the not used search filters is a bit tedious if we i.e. only want to allow a search via the internal ID or the title of a ticket. When invoking this method, we see that a request to /tickets?startsWithInternalIdOrTitle=abc123&page=0&size=25 is made, which is picked up by our Spring ticket controller as intended. The client will only add properties to the URI that were specified and are therefore non-null values.

In an attempt to make the client code a bit less painful, I stumbled upon this Github issue where a user is using...

When generating the Java client stuff, the actual usage now changes to...

which actually matches what we aim for, however, when sending a request with this implementation, the OpenAPI client will serialize the DTO as is into the URI instead of the DTO's parameters when not null:

/tickets?filterDto=class%20TicketSearchFilterDTO%20%7B%0A%20%20%20%20startsWithInternalIdOrTitle%3A%20abc123%0A%20%20%20%20...&page=0&size=15

which our Spring backend controller can't handle and therefore will return a DTO that is actually null and therefore will result in the first 25 entries being returned.

The core issue should be contained in the way the filterDto parameter is added: queryParams.putAll(apiClient.parameterToMultiValueMap(null, "filterDto", filterDto));.

While manually customizing that code can surely solve this issue, this DTO is just one of many and we have plenty more controllers that operate on the same premise. Also, manually checking which properties a DTO defines involves reflection which might not be needed as the definition of the TicketSearchFilterDTO should be there already in the openapi.yaml definition.

Not that for PUT or POST requests where the DTO is added with @RequestBody @Valid annotations, OpenAPI is perfectly fine in generating methods that expose DTOs to clients directly when Java code is generated therefore. Also, the response is picked up by the Spring controller this way without much issues here. So the problem here only exists for GET requests where the properties of the DTO should be encoded into the URI when performing the actual request.

As this shouldn't be a too uncommon case in practice I wonder if there is a way to generate client code that allows DTOs in GET requests being added to the URI called when a parameter is non-null? Or is there a better alternative to allow the usage of DTOs within GET requests via generated OpenAPI client code out of the box?

英文:

We use OpenAPI to generate the interface description for the services exposed in one of our server applications. We use that openapi.yaml then to generate client-sided libraries that will interact with these services for various languages. As the server backend uses the typical Maven + Java Spring Boot techstack we use springdoc-openapi-maven-plugin

&lt;plugin&gt;
    &lt;groupId&gt;org.springdoc&lt;/groupId&gt;
    &lt;artifactId&gt;springdoc-openapi-maven-plugin&lt;/artifactId&gt;
    &lt;version&gt;1.4&lt;/version&gt;
    &lt;executions&gt;
        &lt;execution&gt;
            &lt;id&gt;integration-test&lt;/id&gt;
            &lt;goals&gt;
                &lt;goal&gt;generate&lt;/goal&gt;
            &lt;/goals&gt;
        &lt;/execution&gt;
    &lt;/executions&gt;
    &lt;configuration&gt;
        &lt;apiDocsUrl&gt;http://localhost:8822/v3/api-docs&lt;/apiDocsUrl&gt;
        &lt;outputFileName&gt;openapi.json&lt;/outputFileName&gt;
        &lt;outputDir&gt;${openapi.outputDir}&lt;/outputDir&gt;
    &lt;/configuration&gt;
&lt;/plugin&gt;

to generate the interface description and openapi-generator-maven-plugin

&lt;plugin&gt;
    &lt;groupId&gt;org.openapitools&lt;/groupId&gt;
    &lt;artifactId&gt;openapi-generator-maven-plugin&lt;/artifactId&gt;
    &lt;version&gt;6.3.0&lt;/version&gt;
    &lt;executions&gt;
        &lt;execution&gt;
            &lt;id&gt;spring-webclient&lt;/id&gt;
            &lt;phase&gt;post-integration-test&lt;/phase&gt;
            &lt;goals&gt;
                &lt;goal&gt;generate&lt;/goal&gt;
            &lt;/goals&gt;
            &lt;configuration&gt;
                &lt;inputSpec&gt;.../${openapi.outputDir}/openapi.json&lt;/inputSpec&gt;
                &lt;generatorName&gt;java&lt;/generatorName&gt;
                &lt;library&gt;webclient&lt;/library&gt;
                &lt;output&gt;${openapi.outputDir}/spring-webclient&lt;/output&gt;
                &lt;configOptions&gt;
                    &lt;groupId&gt;com.whatever&lt;/groupId&gt;
                    &lt;artifactId&gt;web-api-client&lt;/artifactId&gt;
                    &lt;artifactVersion&gt;${env.CLIENT_VERSION}&lt;/artifactVersion&gt;
                    &lt;apiPackage&gt;com.whatever.api&lt;/apiPackage&gt;
                    &lt;modelPackage&gt;com.whatever.dto&lt;/modelPackage&gt;
                    &lt;generateModelTests&gt;false&lt;/generateModelTests&gt;
                    &lt;generateApiTests&gt;false&lt;/generateApiTests&gt;
                &lt;/configOptions&gt;
            &lt;/configuration&gt;
        &lt;/execution&gt;
    &lt;/executions&gt;
&lt;/plugin&gt;

to generate the client library project for Java based clients.

This setup works in general but for one of our services which allows the management of various tickets we have a couple of business methods that operate on a provided search filter. This is implemented through a DTO that has roughly 30+ properties that can be specified.

@Data
@AllArgsConstructor
@NoArgsConstructor
@Schema
public class TicketSearchFilterDTO implements Serializable {

  @Schema(
          description = &quot;...&quot;
  )
  @JsonInclude(JsonInclude.Include.NON_NULL)
  private String startsWithInternalIdOrTitle;
  ...
}

Within a Spring typical RestController named TicketController one of the methods is now defining a GET endpoint for querying a list of tickets that match the given search filter. In Java Spring syntax this is implemented as such:

@GetMapping
public Page&lt;Ticket&gt; getTickets(
    @ParameterObject TicketSearchFilterDTO filterDto,
    @ParameterObject @PageableDefault(size = 25) Pageable pageable
) {
  log.info(&quot;Getting tickets, filter {}, pageable {}&quot;, filterDto, pageable);
  TicketSearchFilter filter = ticketSearchFilterDTOConverter.convert(filterDto);
  return ticketSearchRepository.getTickets(filter, pageable);
}

which results in a OpenAPI interface definition like

openapi: 3.0.1
info:
  title: OpenAPI definition
  version: &#39;@env.CI_COMMIT_REF_NAME@&#39;
servers:
  ...
paths:
  ...
  /tickets:
    get:
      operationId: getTickets
      parameters:
      - explode: true
        in: query
        name: startsWithInternalIdOrTitle
        required: false
        schema:
          description: ...
          type: string
        style: form
      ...
      - description: Zero-based page index (0..N)
        explode: true
        in: query
        name: page
        required: false
        schema:
          default: 0
          minimum: 0
          type: integer
        style: form
      - description: The size of the page to be returned
        explode: true
        in: query
        name: size
        required: false
        schema:
          default: 25
          minimum: 1
          type: integer
        style: form
      - description: &quot;Sorting criteria in the format: property,(asc|desc). Default\
          \ sort order is ascending. Multiple sort criteria are supported.&quot;
        explode: true
        in: query
        name: sort
        required: false
        schema:
          items:
            type: string
          type: array
        style: form
      responses:
        &quot;404&quot;:
          content:
            &#39;*/*&#39;:
              schema:
                $ref: &#39;#/components/schemas/NotFoundError&#39;
          description: Not Found
        &quot;400&quot;:
          content:
            &#39;*/*&#39;:
              schema:
                type: object
          description: Bad Request
        &quot;409&quot;:
          content:
            &#39;*/*&#39;:
              schema:
                type: object
          description: Conflict
        &quot;200&quot;:
          content:
            &#39;*/*&#39;:
              schema:
                $ref: &#39;#/components/schemas/PageTicket&#39;
          description: OK
      tags:
      - ticket-controller
      x-accepts: &#39;*/*&#39;

When generating the Java client code for this definition I will have a TicketControllerApi class available that provides the mentioned getTickets method. But instead of providing a TicketSearchFilterDTO as input parameter it exposes all of the 30+ possible search parameters as argument to that method.

openApiClient.getTicketControllerApi().getTickets(&quot;abc123&quot;, ..., 0, 25, List.of());

where the last 3 arguments represents the Pageable parameters for page, size and sort order. As we have so many search parameters nulling out all of the not used search filters is a bit tedious if we i.e. only want to allow a search via the internal ID or the title of a ticket. When invoking this method we see that a request to /tickets?startsWithInternalIdOrTitle=abc123&amp;page=0&amp;size=25 is made, which is picked up by our Spring ticket controller as intended. The client will only add properties to the URI that were specified and are therefore non-null values.

In an attempt to make the client code a bit less painful I stumbled on this Github issue where a user is using

@Parameter(
    explode = Explode.TRUE, 
    in = ParameterIn.QUERY, 
    content = @Content(
        schema = @Schema(implementation = TicketSearchFilterDTO.class, 
        ref = &quot;#/components/schemas/TicketSearchFilterDTO&quot;)
    )
) final TicketSearchFilterDTO filterDto

instead of

@ParameterObject final TicketSearchFilterDTO filterDto

as argument definition for the getTickets(...) method. This will change the OpenAPI definition to

openapi: 3.0.1
info:
  title: OpenAPI definition
  version: &#39;@env.CI_COMMIT_REF_NAME@&#39;
servers:
  ...
paths:
  ...
  /tickets:
    get:
      operationId: getTickets
      parameters:
      - content:
          &#39;*/*&#39;:
            schema:
              $ref: &#39;#/components/schemas/TicketSearchFilterDTO&#39;
        in: query
        name: filterDto
        required: true
      - description: Zero-based page index (0..N)
        explode: true
        in: query
        name: page
        required: false
        schema:
          default: 0
          minimum: 0
          type: integer
        style: form
      - description: The size of the page to be returned
        explode: true
        in: query
        name: size
        required: false
        schema:
          default: 25
          minimum: 1
          type: integer
        style: form
      - description: &quot;Sorting criteria in the format: property,(asc|desc). Default\
          \ sort order is ascending. Multiple sort criteria are supported.&quot;
        explode: true
        in: query
        name: sort
        required: false
        schema:
          items:
            type: string
          type: array
        style: form
      responses:
        &quot;404&quot;:
          content:
            &#39;*/*&#39;:
              schema:
                $ref: &#39;#/components/schemas/NotFoundError&#39;
          description: Not Found
        &quot;400&quot;:
          content:
            &#39;*/*&#39;:
              schema:
                type: object
          description: Bad Request
        &quot;409&quot;:
          content:
            &#39;*/*&#39;:
              schema:
                type: object
          description: Conflict
        &quot;200&quot;:
          content:
            &#39;*/*&#39;:
              schema:
                $ref: &#39;#/components/schemas/PageTicket&#39;
          description: OK
      tags:
      - ticket-controller
      x-accepts: &#39;*/*&#39;
  ...
components:
  ...
  schemas:
    ...
    TicketSearchFilterDTO:
      properties:
        startsWithInternalIdOrTitle:
          description: ...
          type: string
        ...
      type: object
    ...

When generating the Java client stuff the actual usage now changes to

TicketSearchFilterDTO searchFilter = new TicketSearchFilterDTO();
searchFilter.setStartsWithInternalIdOrTitle(&quot;abc123&quot;);
openApiClient.getTicketControllerApi().getTickets(searchFilter, 0, 25, List.of());

which actually matches what we aim for, however, when sending a request with this implementation the OpenAPI client will serialize the DTO as is into the URI instead of the DTO's parameters when not null:

/tickets?filterDto=class%20TicketSearchFilterDTO%20%7B%0A%20%20%20%20startsWithInternalIdOrTitle%3A%20abc123%0A%20%20%20%20...&amp;page=0&amp;size=15

which our Spring backend controller can't handle and therefore will return a DTO that is actually null and therefore will result in the first 25 entries being returned. The generated client Java code does look like this:

public Mono&lt;PageTicket&gt; getTickets(TicketSearchFilterDTO filterDto, Integer page, Integer size, List&lt;String&gt; sort) throws WebClientResponseException {
    ParameterizedTypeReference&lt;PageTicket&gt; localVarReturnType = new ParameterizedTypeReference&lt;PageTicket&gt;() {};
    return getTicketsRequestCreation(filterDto, page, size, sort).bodyToMono(localVarReturnType);
}

private ResponseSpec getTicketsRequestCreation(TicketSearchFilterDTO filterDto, Integer page, Integer size, List&lt;String&gt; sort) throws WebClientResponseException {
    Object postBody = null;
    // verify the required parameter &#39;filterDto&#39; is set
    if (filterDto == null) {
        throw new WebClientResponseException(&quot;Missing the required parameter &#39;filterDto&#39; when calling getTickets&quot;, HttpStatus.BAD_REQUEST.value(), HttpStatus.BAD_REQUEST.getReasonPhrase(), null, null, null);
    }
    // create path and map variables
    final Map&lt;String, Object&gt; pathParams = new HashMap&lt;String, Object&gt;();

    final MultiValueMap&lt;String, String&gt; queryParams = new LinkedMultiValueMap&lt;String, String&gt;();
    final HttpHeaders headerParams = new HttpHeaders();
    final MultiValueMap&lt;String, String&gt; cookieParams = new LinkedMultiValueMap&lt;String, String&gt;();
    final MultiValueMap&lt;String, Object&gt; formParams = new LinkedMultiValueMap&lt;String, Object&gt;();

    queryParams.putAll(apiClient.parameterToMultiValueMap(null, &quot;filterDto&quot;, filterDto));
    queryParams.putAll(apiClient.parameterToMultiValueMap(null, &quot;page&quot;, page));
    queryParams.putAll(apiClient.parameterToMultiValueMap(null, &quot;size&quot;, size));
    queryParams.putAll(apiClient.parameterToMultiValueMap(ApiClient.CollectionFormat.valueOf(&quot;multi&quot;.toUpperCase(Locale.ROOT)), &quot;sort&quot;, sort));

    final String[] localVarAccepts = { 
        &quot;*/*&quot;
    };
    final List&lt;MediaType&gt; localVarAccept = apiClient.selectHeaderAccept(localVarAccepts);
    final String[] localVarContentTypes = { };
    final MediaType localVarContentType = apiClient.selectHeaderContentType(localVarContentTypes);

    String[] localVarAuthNames = new String[] {  };

    ParameterizedTypeReference&lt;PageTicket&gt; localVarReturnType = new ParameterizedTypeReference&lt;PageTicket&gt;() {};
    return apiClient.invokeAPI(&quot;/tickets&quot;, HttpMethod.GET, pathParams, queryParams, postBody, headerParams, cookieParams, formParams, localVarAccept, localVarContentType, localVarAuthNames, localVarReturnType);
}

and so the core issue should be contained in the way the filterDto parameter is added: queryParams.putAll(apiClient.parameterToMultiValueMap(null, &quot;filterDto&quot;, filterDto));.

While manually customizing that code can surely solve this issue this DTO is just one of many and we have plenty more controllers that operate on the same premise. Also, manually checking which properties a DTO defines involves reflection which might not be needed as the definition of the TicketSearchFilterDTO should be there already in the openapi.yaml definition.

Not that for PUT or POST requests where the DTO is added with @RequestBody @Valid annotations, OpenAPI is perfectly fine in generating method that expose DTOs to clients directly when Java code is generated therefore. Also the response is picked up by the Spring controller this way without much issues here. So the problem here only exists for GET requests where the properties of the DTO should be encoded into the URI when performing the actual request.

As this shouldn't be a to uncommon case in practice I wonder if there is a way to generate client code that allows DTOs in GET requests being added to the URI called when a parameter is non-null? Or is there a better alternative to allow the usage of DTOs within GET requests via generated OpenAPI client code out of the box?

答案1

得分: 1

以下是您提供的代码的翻译:

在进一步调查问题并根据Swagger序列化文档检查后,生成给定控制器方法的openapi.yaml,如下所示:

  @GetMapping
  public Page&lt;Ticket&gt; getTickets(
    @Parameter(explode = Explode.TRUE,
            in = ParameterIn.QUERY,
            style = ParameterStyle.FORM
    ) final TicketSearchFilterDTO filterDto,
    @ParameterObject @PageableDefault(size = 25) Pageable pageable
  ) {
    log.info(&quot;Getting tickets, filter {}, pageable {}&quot;, filterDto, pageable);
    TicketSearchFilter filter = ticketSearchFilterDTOConverter.convert(filterDto);
    return ticketSearchRepository.getTickets(filter, pageable);
  }

这将生成一个类似以下的openapi.yaml输出:

openapi: 3.0.1
info:
  title: OpenAPI definition
  version: &#39;@env.CI_COMMIT_REF_NAME@&#39;
servers:
  ...
paths:
  ...
  /tickets:
    get:
      operationId: getTickets
      parameters:
      - explode: true
        in: query
        name: filterDto
        required: true
        schema:
          $ref: &#39;#/components/schemas/TicketSearchFilterDTO&#39;
        style: form
      - description: 从零开始的页面索引 (0..N)
        explode: true
        in: query
        name: page
        required: false
        schema:
          default: 0
          minimum: 0
          type: integer
        style: form
      - description: 要返回的页面大小
        explode: true
        in: query
        name: size
        required: false
        schema:
          default: 25
          minimum: 1
          type: integer
        style: form
      - description: &quot;以属性,(asc|desc)格式的排序标准。默认排序顺序是升序。支持多个排序标准。&quot;
        explode: true
        in: query
        name: sort
        required: false
        schema:
          items:
            type: string
          type: array
        style: form
      responses:
        &quot;404&quot;:
          content:
            &#39;*/*&#39;:
              schema:
                $ref: &#39;#/components/schemas/NotFoundError&#39;
          description: 未找到
        &quot;400&quot;:
          content:
            &#39;*/*&#39;:
              schema:
                type: object
          description: 错误请求
        &quot;409&quot;:
          content:
            &#39;*/*&#39;:
              schema:
                type: object
          description: 冲突
        &quot;200&quot;:
          content:
            &#39;*/*&#39;:
              schema:
                $ref: &#39;#/components/schemas/PageTicket&#39;
          description: 正常
      tags:
      - ticket-controller
      x-accepts: &#39;*/*&#39;
    ...
components:
  schemas:
    ...
    TicketSearchFilterDTO:
      properties:
        startsWithInternalIdOrTitle:
          description: ...
          type: string
        ...
      type: object
    ...

看起来一切都没问题。这将导致所述客户端API,该API将过滤选项封装在所需的DTO对象中。但是,openapi-generator-maven-plugin似乎在某种程度上缺乏对这些情况的覆盖。

生成器使用Mustache进行客户端生成,输入参数的相应api.mustache将生成如下代码作为RequestCreation子方法的一部分:

queryParams.putAll(apiClient.parameterToMultiValueMap(null, &quot;filterDto&quot;, filterDto));

这行代码特别负责将给定的DTO添加到最终由HTTP客户端调用的查询参数中。不幸的是,这将导致整个DTO以序列化形式添加为filterDto查询参数,而不是将其属性添加到查询参数中。然而,Spring后端不知道filterDto查询参数,因此无论输入如何,它都会返回前25个票。

Maven插件允许在&lt;configOptions&gt;部分之前添加以下行以指定自定义的Mustache模板:

&lt;templateDirectory&gt;
    ./open-api/templates
&lt;/templateDirectory&gt;

现在只需在上面提到的链接中的api.mustache中自定义代码,如下所示:

        if ({{paramName}}.getClass().getSimpleName().endsWith(&quot;DTO&quot;)) {
            // 遍历DTO的属性
            Class&lt;?&gt; clazz = {{paramName}}.getClass();
            for (Field field : clazz.getDeclaredFields()) {
                if (Modifier.isPrivate(field.getModifiers()) &amp;&amp; !Modifier.isStatic(field.getModifiers())) {
                    field.setAccessible(true);
                    String fieldName = field.getName();
                    try {
                        Object value = field.get({{paramName}});
                        queryParams.putAll(apiClient.parameterToMultiValueMap({{#collectionFormat}}ApiClient.CollectionFormat.valueOf(&quot;{{{.}}}&quot;.toUpperCase(Locale.ROOT)){{/collectionFormat}}{{^collectionFormat}}null{{/collectionFormat}}, fieldName, value));
                    } catch (Exception ex) {
                        ex.printStackTrace();
                    }
                }
            }
        } else {
            queryParams.putAll(apiClient.parameterToMultiValueMap({{#collectionFormat}}ApiClient.CollectionFormat.valueOf(&quot;{{{.}}}&quot;.toUpperCase(Locale.ROOT)){{/collectionFormat}}{{^collectionFormat}}null{{/collectionFormat}}, &quot;{{baseName}}&quot;, {{paramName}}));
        }

并在Mustache文件的顶部添加以下导入:

import java.lang.reflect.Field;
import java.lang.reflect.Modifier;

这将修改生成的代码,以便对于类名以DTO结尾的任何参数,将其直接添加到查询参数的操作替换为将其所有定义的属性添加到查询参数列表中。

虽然我更喜欢不需要首先使用反射的解决方案,而是直接使用组件方案的属性定义,但这至少能够解决目前的问题。

英文:

After investigating the issue at hand further and checking against the Swagger serialization documentation the generation of the openapi.yaml for the given controller method

  @GetMapping
  public Page&lt;Ticket&gt; getTickets(
    @Parameter(explode = Explode.TRUE,
            in = ParameterIn.QUERY,
            style = ParameterStyle.FORM
    ) final TicketSearchFilterDTO filterDto,
    @ParameterObject @PageableDefault(size = 25) Pageable pageable
  ) {
    log.info(&quot;Getting tickets, filter {}, pageable {}&quot;, filterDto, pageable);
    TicketSearchFilter filter = ticketSearchFilterDTOConverter.convert(filterDto);
    return ticketSearchRepository.getTickets(filter, pageable);
  }

which results in an openapi.yaml output like

openapi: 3.0.1
info:
  title: OpenAPI definition
  version: &#39;@env.CI_COMMIT_REF_NAME@&#39;
servers:
  ...
paths:
  ...
  /tickets:
    get:
      operationId: getTickets
      parameters:
      - explode: true
        in: query
        name: filterDto
        required: true
        schema:
          $ref: &#39;#/components/schemas/TicketSearchFilterDTO&#39;
        style: form
      - description: Zero-based page index (0..N)
        explode: true
        in: query
        name: page
        required: false
        schema:
          default: 0
          minimum: 0
          type: integer
        style: form
      - description: The size of the page to be returned
        explode: true
        in: query
        name: size
        required: false
        schema:
          default: 25
          minimum: 1
          type: integer
        style: form
      - description: &quot;Sorting criteria in the format: property,(asc|desc). Default\
          \ sort order is ascending. Multiple sort criteria are supported.&quot;
        explode: true
        in: query
        name: sort
        required: false
        schema:
          items:
            type: string
          type: array
        style: form
      responses:
        &quot;404&quot;:
          content:
            &#39;*/*&#39;:
              schema:
                $ref: &#39;#/components/schemas/NotFoundError&#39;
          description: Not Found
        &quot;400&quot;:
          content:
            &#39;*/*&#39;:
              schema:
                type: object
          description: Bad Request
        &quot;409&quot;:
          content:
            &#39;*/*&#39;:
              schema:
                type: object
          description: Conflict
        &quot;200&quot;:
          content:
            &#39;*/*&#39;:
              schema:
                $ref: &#39;#/components/schemas/PageTicket&#39;
          description: OK
      tags:
      - ticket-controller
      x-accepts: &#39;*/*&#39;
    ...
components:
  schemas:
    ...
    TicketSearchFilterDTO:
      properties:
        startsWithInternalIdOrTitle:
          description: ...
          type: string
        ...
      type: object
    ...

seems to be fine. This will result in mentioned client API that results in encapsulating the filter options within the desired DTO object. However, openapi-generator-maven-plugin seems to lack coverage of these cases somehow.

The generator uses Mustache for the client generation and the respective api.mustache for the input parameter will generate i.e. the following line as part of the RequestCreation sub method:

queryParams.putAll(apiClient.parameterToMultiValueMap(null, &quot;filterDto&quot;, filterDto));

This line is in particular responsible for adding the given DTO to the query parameters that will be called by the HTTP client at the end. And this will unfortunately result in the whole DTO being added as fliterDto query parameter in a serialized from rather than its properties being added to the query parameters. That filterDto query parameter is though unknown by the Spring backend and thus will return the first 25 tickets if find regardless of the input.

The Maven plugin though allows to specify custom Mustache templates i.e. via the following lines added before the &lt;configOptions&gt; section:

&lt;templateDirectory&gt;
    ./open-api/templates
&lt;/templateDirectory&gt;

Now it is just a matter of customizing api.mustache at the referenced line in the above-mentioned link with the following code:

        if ({{paramName}}.getClass().getSimpleName().endsWith(&quot;DTO&quot;)) {
            // iterate through DTOs properties
            Class&lt;?&gt; clazz = {{paramName}}.getClass();
            for (Field field : clazz.getDeclaredFields()) {
                if (Modifier.isPrivate(field.getModifiers()) &amp;&amp; !Modifier.isStatic(field.getModifiers())) {
                    field.setAccessible(true);
                    String fieldName = field.getName();
                    try {
                        Object value = field.get({{paramName}});
                        queryParams.putAll(apiClient.parameterToMultiValueMap({{#collectionFormat}}ApiClient.CollectionFormat.valueOf(&quot;{{{.}}}&quot;.toUpperCase(Locale.ROOT)){{/collectionFormat}}{{^collectionFormat}}null{{/collectionFormat}}, fieldName, value));
                    } catch (Exception ex) {
                        ex.printStackTrace();
                    }
                }
            }
        } else {
            queryParams.putAll(apiClient.parameterToMultiValueMap({{#collectionFormat}}ApiClient.CollectionFormat.valueOf(&quot;{{{.}}}&quot;.toUpperCase(Locale.ROOT)){{/collectionFormat}}{{^collectionFormat}}null{{/collectionFormat}}, &quot;{{baseName}}&quot;, {{paramName}}));
        }

and adding imports for

import java.lang.reflect.Field;
import java.lang.reflect.Modifier;

at the top of the Mustache file to modify the generated code so that any parameters which class name ends with DTO to replace the previous adding of the DTO object directly to the query parameters with adding all of its defined properties to the query parameter list.

While I'd prefer a solution that doesn't need reflection in first place and instead use the property definition from the component scheme directly, this at least gets the job done for now.

huangapple
  • 本文由 发表于 2023年5月11日 18:37:03
  • 转载请务必保留本文链接:https://go.coder-hub.com/76226703.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定