I'm struggling on how to reuse an @Header annotation within my API specification using Quarkus and Eclipse Microprofile. I have a lot of methods which return a paginated list of objects and I want to deliver the information about the
as X-... Header, this currently looks like
@GET
@Path("/obj1")
@Operation(description = "get filtered list of obj1")
@APIResponse(
responseCode = "200",
headers = { @Header(name = "X-Length"),@Header(name = "X-Offset") ,@Header(name = "X-Limit")},
content = @Content(
mediaType = MediaType.APPLICATION_JSON,
schema = @Schema(implementation = Obj1.class, type = SchemaType.ARRAY)
)
)
Since the headers are always the same for every method returning a list, I don't want to copy & paste them on every method, since I'm also currently not sure about the naming.
Is it possible, and if so how, to declare this array once and reuse it like
paginationHeaders = { @Header(name = "X-Length"),@Header(name = "X-Offset") ,@Header(name = "X-Limit")}
...
@APIResponse(...
headers = paginationHeaders
Is there maybe another standardized way to return pagination information (but not in the response object) using Quarkus?
I tried searching via Google, browsing the Quarkus docs and defining a custom annotation.
At first I'd like to mention that passing pagination information to HTTP headers is not the best idea. For example firewalls or proxy servers can modify (even remove) HTTP headers. I'm going to show my preferred solution later.
However, reusing header constants is not fully supported neither OpenAPI nor Quarkus the MicroProfile OpenAPI Specification (and Smallrye OpenAPI implementation) supports to modify OpenAPI documentation programatically.
A REST resource
@Path("/hello")
public class ExampleResource {
@Operation(
operationId = "listExamples",
summary = "List examples"
)
@APIResponse(
headers = @Header(name = "X-Paginated"),
responseCode = "200",
description = "Successful operation"
)
@APIResponse(
responseCode = "418",
description = "In case of Armageddon"
)
@GET
@Produces(MediaType.TEXT_PLAIN)
public List<String> filter() {
return List.of("Hello from RESTEasy Reactive");
}
@DELETE
public void remove() {
// Just to haven an other endpoint
}
}
The listExamples
operation has possible two responses, but the only one should contain pagination response headers. I've introduced a meta header X-Paginated
to mark which response has to be modified.
By implementing a custom OpenAPI filter it is possible to change OpenAPI definition programatically.
The following filter will change each occurrence of X-Paginated
header to X-Length
, X-Offset
and X-Limit
triple.
public class PaginatedHeaderOASFilter implements OASFilter {
@Override
public APIResponse filterAPIResponse(APIResponse apiResponse) {
return Optional.of(apiResponse)
.filter(isPaginated)
.map(mapResponse)
.orElse(apiResponse);
}
private static final Predicate<APIResponse> isPaginated = response -> Optional
.ofNullable(response.getHeaders())
.map(map -> map.containsKey("X-Paginated"))
.orElse(false);
private static final UnaryOperator<APIResponse> mapResponse = response -> {
response.removeHeader("X-Paginated");
return response
.addHeader("X-Length", OASFactory.createHeader()
.description("Description of X-Length")
.schema(OASFactory.createSchema().type(Schema.SchemaType.INTEGER))
)
.addHeader("X-Offset", OASFactory.createHeader().description("Description of X-Offset"))
.addHeader("X-Limit", OASFactory.createHeader().description("Description of X-Limit"));
};
}
Note: Don't forget to register custom OASFilter(s) in application configuration file e.g.:
mp.openapi.filter=io.github.zforgo.PaginatedHeaderOASFilter
As I started I prefer to pass pagination properties in response body. Luckily it can be reusable, because Smallrye supports generic response types.
Pagination
holds the pagination properties
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;
}
// getters / setters / others
}
FilterResult
contains the result list along with pagination
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));
}
// getters / setters / others
}
Now, an enpoint like
@GET
@Path("/orders")
@Produces(MediaType.APPLICATION_JSON)
public FilterResult<OrderDto> filterOrders() {
return null;
}
will be generated in OpenAPI documentation as:
paths:
/orders:
get:
responses:
"200":
description: OK
content:
application/json:
schema:
$ref: '#/components/schemas/FilterResultOrderDto'
components:
schemas:
FilterResultOrderDto:
type: object
properties:
pagination:
$ref: '#/components/schemas/Pagination'
items:
type: array
items:
$ref: '#/components/schemas/OrderDto'
OrderDto:
type: object
properties:
billingAddress:
type: string
Pagination:
type: object
properties:
totalCount:
format: int64
type: integer
pageCount:
format: int32
type: integer
pageIndex:
format: int32
type: integer
pageSize:
format: int32
type: integer