I have a case where I have a search endpoint with many possible parameters. On the server side, I have a @BeanParam
keeping the method signature sane.
I would like similar functionality, to use a pojo to specify the possible params and not have to include them all in the method signature. Is there a way to do this?
@GET
Set<Extension> getByFilter(@RestQuery Map<String, String> filter);
seems close, but I would rather specify an actual pojo rather than deal with making a map.
@BeanParam
is definitely works on client side.
@BeanParam
A sample endpoint which allows a custom headper parameter X-Custom-Header
and three query params like: filter_name
, filter_age
, filter_active
is documented like:
---
openapi: 3.0.3
info:
title: so-77975157-quarkus-client-beanparam API
version: 1.0-SNAPSHOT
paths:
/api:
get:
tags:
- Example Resource
parameters:
- name: filter_active
in: query
schema:
type: boolean
- name: filter_age
in: query
schema:
format: int32
type: integer
- name: filter_name
in: query
schema:
type: string
- name: X-Custom-Header
in: header
schema:
type: string
responses:
"200":
description: OK
content:
application/json:
schema:
uniqueItems: true
type: array
items:
type: string
By default Openapi Generator will create a method and that's signature will contain every parameter one by one.
@GET
@Produces(MediaType.APPLICATION_JSON)
public Set<String> filter(@HeaderParam("X-Custom-Header") String headerParam,
@QueryParam("filter_name") String nameFilter,
@QueryParam("filter_age") Integer ageFilter,
@QueryParam("filter_active") Boolean activeFilter);
Using the following plugin configuration will generate interfaces using one parameter object annotated by @BeanParam
<plugin>
<groupId>org.openapitools</groupId>
<artifactId>openapi-generator-maven-plugin</artifactId>
<version>7.3.0</version>
<executions>
<execution>
<goals>
<goal>generate</goal>
</goals>
<configuration>
<cleanupOutput>true</cleanupOutput>
<verbose>true</verbose>
<inputSpec>${project.basedir}/src/main/resources/api.yaml</inputSpec>
<additionalProperties>disableMultipart=true</additionalProperties>
<generateApiTests>false</generateApiTests>
<generateModelTests>false</generateModelTests>
<generatorName>java</generatorName>
<library>microprofile</library>
<configOptions>
<interfaceOnly>true</interfaceOnly>
<useJakartaEe>true</useJakartaEe>
<configKey>myclient</configKey>
<serializationLibrary>jackson</serializationLibrary>
<useSingleRequestParameter>true</useSingleRequestParameter>
<microprofileRestClientVersion>3.0</microprofileRestClientVersion>
<useRuntimeException>true</useRuntimeException>
</configOptions>
</configuration>
</execution>
</executions>
</plugin>
Note #1 the magic is useSingleRequestParameter
configuration option.
Note #2 there are some hidden trap in the built-in template. E.g. an unvanted Apache CXF import definition which may cause compile errors. So, <additionalProperties>disableMultipart=true</additionalProperties>
property is important to set.
Now, the generated method signature will be:
@GET
@Produces({ "application/json" })
Set<String> apiGet(@BeanParam ApiGetRequest request) throws ApiException, ProcessingException;
and the ApiGetRequest
(sorry for that name, I didn't use operationId and/or tags in the sample)
public class ApiGetRequest {
private @QueryParam("filter_active") Boolean filterActive;
private @QueryParam("filter_age") Integer filterAge;
private @QueryParam("filter_name") String filterName;
private @HeaderParam("X-Custom-Header") String xCustomHeader;
private ApiGetRequest() {
}
public static ApiGetRequest newInstance() {
return new ApiGetRequest();
}
/**
* Set filterActive
* @param filterActive (optional)
* @return ApiGetRequest
*/
public ApiGetRequest filterActive(Boolean filterActive) {
this.filterActive = filterActive;
return this;
}
/**
* Set filterAge
* @param filterAge (optional)
* @return ApiGetRequest
*/
public ApiGetRequest filterAge(Integer filterAge) {
this.filterAge = filterAge;
return this;
}
/**
* Set filterName
* @param filterName (optional)
* @return ApiGetRequest
*/
public ApiGetRequest filterName(String filterName) {
this.filterName = filterName;
return this;
}
/**
* Set xCustomHeader
* @param xCustomHeader (optional)
* @return ApiGetRequest
*/
public ApiGetRequest xCustomHeader(String xCustomHeader) {
this.xCustomHeader = xCustomHeader;
return this;
}
}
On the server side the registered client will be available.
@Path("/foobar")
public class ServerSideResource {
@RestClient
ExampleResourceApi clientApi;
@GET
public Set<String> callSampleClient() {
return clientApi.apiGet(ApiGetRequest.newInstance()
.filterActive(true)
.filterName("zforgo")
);
}
}
However, Openapi Generator can generate functions with a single argument it has lack of utilization capabilities (like inheritance, reuse common parts, etc.)
Fortunately JakartaEE standard supports that, but it has to be created manually.
@RegisterRestClient(configKey = "person-api")
public interface PersonClient {
@GET
@Produces(MediaType.APPLICATION_JSON)
FilterResult<PersonDTO> search(@HeaderParam("X-Custom-Header") String headerParam,
@BeanParam PersonFilter personFilter,
@BeanParam PagingAndSorting pagingAndSorting);
}
As you can see, the result type is generic and paging and sorting capabilities are utilized to a separated argument.
PagingAndSorting:
public class PagingAndSorting {
public static final String param_PageIndex = "page_index";
public static final String param_PageSize = "page_size";
public static final String param_SortCriteria = "sort_criteria";
public static final String param_SortDirection = "sort_direction";
private static final String directionAscending = "Ascending";
public static final String pageUnlimited = "-1";
@QueryParam(param_SortCriteria)
public Optional<String> sortingCriteria;
@QueryParam(param_SortDirection)
@DefaultValue(directionAscending)
public Sort.Direction sortDirection;
@QueryParam(param_PageIndex)
public int pageIndex;
@QueryParam(param_PageSize)
@DefaultValue(pageUnlimited)
public int pageSize;
// getters, setters, builder ...
}
PersonFilter:
public class PersonFilter {
@QueryParam("filter_name")
private String nameFilter;
@QueryParam("filter_age")
private Integer ageFilter;
@QueryParam("filter_active")
private Boolean activeFilter;
// getters, setters, builder ...
}
FilterResult:
public class FilterResult<T> {
private final Pagination pagination;
private final List<T> items;
public FilterResult(List<T> items, Pagination pagination) {
this.items = items;
this.pagination = pagination;
}
@JsonCreator
public FilterResult(List<T> items, long totalCount, Integer pageCount, Integer pageIndex) {
this(items, new Pagination(totalCount, pageCount, pageIndex));
}
public List<T> getItems() {
return items;
}
public Pagination getPagination() {
return pagination;
}
public FilterResult<T> withPageSize(Integer pageSize) {
pagination.pageSize = pageSize;
return this;
}
}
Pagination:
public class Pagination {
protected long totalCount;
protected Integer pageCount;
protected Integer pageIndex;
protected Integer pageSize;
public Pagination(long totalCount, Integer pageCount, Integer pageIndex) {
this.totalCount = totalCount;
this.pageCount = pageCount;
this.pageIndex = pageIndex;
}
public long getTotalCount() {
return totalCount;
}
public Integer getPageCount() {
return pageCount;
}
public Integer getPageIndex() {
return pageIndex;
}
}
Finally the server side code sample:
@Path("/check")
public class ServerSideResource {
@RestClient
PersonClient personApi;
@GET
@Path("/person-search")
public FilterResult<PersonDTO> searchPerson() {
return personApi.search("FooBar",
// create and fill PersonFilter
personFilter()
.filterActive(true)
.filterName("zforgo"),
// create and fill PagingAndSorting
pagingAndSorting()
.pageSize(20)
.sortCriteria("name")
.sortDirection(Sort.Direction.Ascending)
);
}
}