Can Springfox 3 generate Integer boundaries based on JSR 303 #Min/#Max in OAS3 API docs? - spring-boot

I created a Spring Boot application with a RestController that validates the DTO passed to a POST method based on JSR 303 validation annotations. API docs are generated using Springfox.
Validations are applied correctly and show up in the OAS2 API Docs. They are incomplete in the OAS3 API docs however - no minimum/maximum boundaries are generated for the Integer fields.
I'm using Spring Boot 2.5.2 (this is important as the latest version, 2.6.2, has issues with Springfox) and Springfox 3.0.0.
Since I found no specific hints in documentation + Springfox issue tracking and JSR303 support is working for the most part, I think this is a bug or oversight in Springfox OAS3 support. In the meantime I found a workaround which I will post as an answer - if I missed anything or there are better solutions I'd be happy to hear about that.
Details:
Controller
#Slf4j
#RestController
public class MyController {
#PostMapping
public void send(#Valid #RequestBody MyDTO dto) {
log.info("{}", dto);
}
}
DTO
#Value
public class MyDTO {
#Size(max = 200)
String text;
#Max(2)
#Min(1)
Integer number;
#Max(4)
#Min(3)
int number2;
#Max(6)
#Min(5)
BigDecimal decimal;
}
OAS2 DTO schema (extracted from http://localhost:8080/v2/api-docs)
{
"MyDTO": {
"type": "object",
"properties": {
"decimal": {
"type": "number",
"minimum": 5,
"maximum": 6,
"exclusiveMinimum": false,
"exclusiveMaximum": false
},
"number": {
"type": "integer",
"format": "int32",
"minimum": 1,
"maximum": 2,
"exclusiveMinimum": false,
"exclusiveMaximum": false
},
"number2": {
"type": "integer",
"format": "int32",
"minimum": 3,
"maximum": 4,
"exclusiveMinimum": false,
"exclusiveMaximum": false
},
"text": {
"type": "string",
"minLength": 0,
"maxLength": 200
}
},
"title": "MyDTO"
}
}
OAS3 DTO schema (extracted from http://localhost:8080/v3/api-docs)
{
"schemas": {
"MyDTO": {
"title": "MyDTO",
"type": "object",
"properties": {
"decimal": {
"maximum": 6,
"exclusiveMaximum": false,
"minimum": 5,
"exclusiveMinimum": false,
"type": "number",
"format": "bigdecimal"
},
"number": {
"type": "integer",
"format": "int32"
},
"number2": {
"type": "integer",
"format": "int32"
},
"text": {
"maxLength": 200,
"minLength": 0,
"type": "string"
}
}
}
}
}

After debugging Springfox I learned that the class springfox.documentation.oas.mappers.SchemaMapper in springfox-oas converts a "general model" to a format for OAS3.
In the "general model", field boundaries are represented by an "NumericElementFacet". A specific property that is being mapped is a subclass of "Schema".
The problem seems to happen here:
https://github.com/springfox/springfox/blob/bc9d0cad83e5dfdb30ddb487594fbc33fc1ba28c/springfox-oas/src/main/java/springfox/documentation/oas/mappers/SchemaMapper.java#L385
Properties represented by a "NumberSchema" are handled correctly (e.g. BigDecimal), their boundaries from the "NumericElementFacet" are applied. Integer fields (and further tests have shown: also Short and Long) are however represented by "IntegerSchema", which isn't handled there so the boundaries are not applied to the resulting API.
So what I did as a workaround was subclassing SchemaMapper, post-processing the results of mapProperties and registering the subclass as #Primary to override the springfox component:
import io.swagger.v3.oas.models.media.Schema;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.annotation.Primary;
import org.springframework.stereotype.Component;
import springfox.documentation.oas.mappers.*;
import springfox.documentation.schema.*;
import springfox.documentation.service.ModelNamesRegistry;
import java.util.*;
#Primary
#Component
#Slf4j
public class IntegerBoundarySupportingOasSchemaMapper extends SchemaMapper {
#Override
#SuppressWarnings("rawtypes")
protected Map<String, Schema> mapProperties(
Map<String, PropertySpecification> properties,
ModelNamesRegistry modelNamesRegistry) {
var result = super.mapProperties(properties, modelNamesRegistry);
result.values()
.stream()
// "integer" seems to cover at least Java Short, Integer and Long.
.filter(property -> "integer".equals(property.getType()))
.forEach(property -> properties.get(property.getName())
.getFacets()
.stream()
.filter(NumericElementFacet.class::isInstance)
.map(NumericElementFacet.class::cast)
.findFirst()
.ifPresent(f -> {
log.trace("Adding boundaries to API field {} (min={}, max={})",
property.getName(),
f.getMinimum(),
f.getMaximum());
property.setMaximum(f.getMaximum());
property.exclusiveMaximum(f.getExclusiveMaximum());
property.setMinimum(f.getMinimum());
property.exclusiveMinimum(f.getExclusiveMinimum());
}));
return result;
}
}
In my case, this is working fine, so maybe its helping someone else, too.
Side note: The SchemaMapper is called every time I retrieve http://localhost:8080/v3/api-docs, that might be something to keep in mind when considering other time-consuming modifications of the schema.

Related

springdoc-openapi-ui add JWT header parameter to generated swagger

In my spring boot app I have endpoints which are validated by header parameter in my springboot app.
Current swagger json looks like this:
// part of current swagger.json
...
"paths": {
"/path1/{param1}": {
"get": {
"parameters": [
{
"name": "param1",
"in": "path",
"type": "string",
"required": true
}
]
}
}
}
...
I want to add missing parameter using springdoc-openapi-ui configuration so it would look like this:
// I want to achieve swagger.json which contains additional parameter
...
"paths": {
"/path1/{param1}": {
"get": {
"parameters": [
{
"name": "param1",
"in": "path",
"type": "string",
"required": true
},
{
"name": "missingParam",
"in": "header",
"type": "string",
"required": true
}
]
}
}
}
...
I tried achieving that by adding to my appplication.yml solution from Common Parameters for Various Paths
#application.yml
...
components:
parameters:
hiddenParam:
in: header
name: missingParam
required: true
schema:
type: string
paths:
/path1:
get:
parameters:
- $ref: '#/components/parameters/hiddenParam'
But it doesn't work.
My questions:
Is there a way to modify my swagger result using application configuration?
I want to define parameter template and add it to all endpoints, how can I achieve that?
You can add the global parameters like header using OperationCustomizer as shown below. This will add your parameter to every service
#Configuration
public class SwaggerConfiguration {
#Bean
public OperationCustomizer customGlobalHeaders() {
return (Operation operation, HandlerMethod handlerMethod) -> {
Parameter missingParam1 = new Parameter()
.in(ParameterIn.HEADER.toString())
.schema(new StringSchema())
.name("missingParam1")
.description("header description2")
.required(true);
Parameter missingParam2 = new Parameter()
.in(ParameterIn.HEADER.toString())
.schema(new StringSchema())
.name("missingParam2")
.description("header description2")
.required(true);
operation.addParametersItem(missingParam1);
operation.addParametersItem(missingParam2);
return operation;
};
}
}
Finally I decided to use different approach.
I defined security scheme and applied it globally as authorization header.
#Bean
public OpenAPI customOpenAPI() {
return new OpenAPI()
.info(new Info().title("My App").version("1.0.0"))
// Components section defines Security Scheme "mySecretHeader"
.components(new Components()
.addSecuritySchemes("mySecretHeader", new SecurityScheme()
.type(SecurityScheme.Type.APIKEY)
.in(SecurityScheme.In.HEADER)
.name("missingParam")))
// AddSecurityItem section applies created scheme globally
.addSecurityItem(new SecurityRequirement().addList("mySecretHeader"));
}
Now swagger-ui.html allows testing endpoints with authorization header or without it according to tester requirements.
Cheers!

Spring MVC Converter and Swagger doc: how to?

I use converters in my Spring MCV controllers. In this example, the String from the path variable is mapped into a UserId:
#GetMapping(path = "/user/{user-id}")
public User get(#Parameter(description = "User id", required = true, example = "3fa85f64-5717-4562-b3fc-2c963f66afa6")
#PathVariable("user-id")
UserId userId) {
return userService.get(userId)
}
It seems to annoy Swagger as the generated doc requires an object as parameter and not a plain string:
...
"/api/v1/user/{user-id}": {
"get": {
"operationId": "get",
"parameters": [
{
"name": "user-id",
"in": "path",
"schema": {
"$ref": "#/components/schemas/UserId"
},
}
],
...
with the UserId schema:
"UserId": {
"type": "object",
"properties": {
"value": {
"type": "string",
"format": "uuid"
}
}
}
And thus the Swagger UI cannot be used because either the parameter is considered as invalid when a single string is provided, either the data is actually invalid when the object format is used.
What is an option to fix that?
To achieve that, the schema parameter of the #Parameter annotation is the answer.
The above example becomes:
#Parameter(description = "User id",
required = true,
schema = #Schema(implementation = String.class), // this is new
example = "3fa85f64-5717-4562-b3fc-2c963f66afa6")

Spring Boot ConfigurationProperties metadata generation - wrong default values

So I'm trying to generate my own configuration metadata from #ConfigurationProperties items, following documentation available at https://docs.spring.io/spring-boot/docs/current/reference/html/configuration-metadata.html#configuration-metadata-annotation-processor. Note that I'm using Spring Boot 2.0 and Kotlin.
It works fine, except for one thing: the defaultValue fields are not getting filled with my default values, but instead just contain standard values like 0 or false.
My #ConfigurationProperties file looks like this:
#Component
#ConfigurationProperties("flowr.epg")
class EpgProperties {
/** Whether or not schedules should be computed/refreshed on (re)start */
var refreshOnRestart = true
/** Number of threads used to store new images */
var nImagesUploadThreads = 10
}
The result looks like this:
{
"hints": [],
"groups": [
{
"sourceType": "com.taktik.flowr.epg.properties.EpgProperties",
"name": "flowr.epg",
"type": "com.taktik.flowr.epg.properties.EpgProperties"
}
],
"properties": [
{
"sourceType": "com.taktik.flowr.epg.properties.EpgProperties",
"defaultValue": false,
"name": "flowr.epg.refresh-on-restart",
"description": "Whether or not schedules should be computed\/refreshed on (re)start",
"type": "java.lang.Boolean"
},
{
"sourceType": "com.taktik.flowr.epg.properties.EpgProperties",
"defaultValue": 0,
"name": "flowr.epg.n-images-upload-threads",
"description": "Number of threads used to store new images in Ozone",
"type": "java.lang.Integer"
}
}
The documentation is kind of poor about that defaultValue field. Am I doing something wrong? Is it possible to fill that defaultValue field automatically? If yes, what am I doing wrong?

Spring Integration Java DSL: How to loop the paged Rest service?

How to loop the paged Rest service with the Java DSL Http.outboundGatewaymethod?
The rest URL is for example
http://localhost:8080/people?page=3
and it returns for example
"content": [
{"name": "Mike",
"city": "MyCity"
},
{"name": "Peter",
"city": "MyCity"
},
...
]
"pageable": {
"sort": {
"sorted": false,
"unsorted": true
},
"pageSize": 20,
"pageNumber": 3,
"offset": 60,
"paged": true,
"unpaged": false
},
"last": false,
"totalElements": 250,
"totalPages": 13,
"first": false,
"sort": {
"sorted": false,
"unsorted": true
},
"number": 3,
"numberOfElements": 20,
"size": 20
}
where the variable totalPages tells the total pages amount.
So if the implementation
integrationFlowBuilder
.handle(Http
.outboundGateway("http://localhost:8080/people?page=3")
.httpMethod(HttpMethod.GET)
.expectedResponseType(String.class))
access one page, how to loop all the pages?
The easiest way to do this is like wrapping the call to this Http.outboundGateway() with the #MessagingGateway and provide a page number as an argument:
#MessagingGateway
public interface HttpPagingGateway {
#Gateway(requestChannel = "httpPagingGatewayChannel")
String getPage(int page);
}
Then you get a JSON as a result, where you can convert it into some domain model or just perform a JsonPathUtils.evaluate() (based on json-path) to get the value of the last attribute to be sure that you need to call that getPage() for the page++ or not.
The page argument is going to be a payload of the message to send and that can be used as an uriVariable:
.handle(Http
.outboundGateway("http://localhost:8080/people?page={page}")
.httpMethod(HttpMethod.GET)
.uriVariable("page", Message::getPayload)
.expectedResponseType(String.class))
Of course, we can do something similar with Spring Integration, but there are going to be involved filter, router and some other components.
UPDATE
First of all I would suggest you to create a domain model (some Java Bean), let's say PersonPageResult, to represent that JSON response and this type to the expectedResponseType(PersonPageResult.class) property of the Http.outboundGateway(). The RestTemplate together with the MappingJackson2HttpMessageConverter out-of-the-box will do the trick for you to return such an object as a reply for the downstream processing.
Then, as I said before, looping would be better done from some Java code, which you could wrap to the service activator call. For this purpose you should daclare a gateway like this:
public interface HttpPagingGateway {
PersonPageResult getPage(int page);
}
Pay attention: no annotations at all. The trick is done via IntegrationFlow:
#Bean
public IntegrationFlow httpGatewayFlow() {
return IntegrationFlows.from(HttpPagingGateway.class)
.handle(Http
.outboundGateway("http://localhost:8080/people?page={page}")
.httpMethod(HttpMethod.GET)
.uriVariable("page", Message::getPayload)
.expectedResponseType(PersonPageResult.class))
}
See IntegrationFlows.from(Class<?> aClass) JavaDocs.
Such a HttpPagingGateway can be injected into some service with hard looping logic:
int page = 1;
boolean last = false;
while(!last) {
PersonPageResult result = this.httpPagingGateway.getPage(page++);
last = result.getLast();
List<Person> persons = result.getPersons();
// Process persons
}
For processing those persons I would suggest to have separate IntegrationFlow, which may start from the gateway as well or you can just send a Message<List<Person>> to its input channel.
This way you will separate concerns about paging and processing and will have a simple loop logic in some POJO method.

Spring Data ElasticSearch Build In IN query returning partial match

I am new to elastic search spring data, Today I was trying to get In query working with Spring data ES repository.
I have to do a lookup for list of user names, and if its exactly match in the index, need to get those users back as result.
I tried to use the built in repository 'In' method to do so, but it returns partial matches, please help me to make this working like SQL IN query.
Here is my repository code:
public interface UserRepository extends ElasticsearchRepository<EsUser, String>
{
public List<EsUser> findByUserAccountUserNameIn(Collection<String> terms);
}
REQUEST:
{"terms":["vijay", "arun"], "type":"NAME"}
RESPONSE:
[
{
"userId": "236000",
"fbId": "",
"userAccount": {
"userName": "arun",
"urlFriendlyName": "arun",
},
"userProfile": {
},
"userStats": {
}
},
{
"userId": "6228",
"userAccount": {
"userName": "vijay",
"urlFriendlyName": "vijay",
},
"userProfile": {
},
"userStats": {
}
},
{
"userId": "236000",
"fbId": "",
"userAccount": {
"userName": "arun singh",
"urlFriendlyName": "arun-singh",
},
"userProfile": {
},
"userStats": {
}
}
{
"userId": "236000",
"fbId": "",
"userAccount": {
"userName": "vijay mohan",
"urlFriendlyName": "vijay-mohan",
},
"userProfile": {
},
"userStats": {
}
}
]
This is because your userAccount.userName field is an analyzed string, and thus, the two tokens arun and singh have been indexed. Your query then matches the first token, which is normal.
In order to prevent this and guarantee an exact match you need to declare your field as not_analyzed, like this:
#Field(index = FieldIndex.not_analyzed)
private String userName;
Then you'll need to delete your index and the associated template in /_template, restart your application so a new template and index are created with the proper field mapping.
Then your query will work.

Resources