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

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!

Related

Springdocs: Specifying an explicit type for Paged responses

I'm working on a "global search" for my application.
Currently, I'm using hibernate-search to search for instances of multiple different objects and return them to the user.
The relevant code looks as follows:
Search.session(entityManager)
.search(ModelA.classs, ModelB.class)
.where(...)
.sort(...)
.fetch(skip, count);
Skip and count are calculated based on a Pageable and the result is used to create an instance of Page, which will be returned to the controller.
This works as I'd expect, however, the types generated by swagger-docs obviously doesn't know, what the type within the Page is, and therefore uses Object.
I'd like to expose the correct types, as I use them to generate the types for the frontend application.
I was able to set the type to an array, when overwriting the schema like this:
#ArraySchema(schema = #Schema(anyOf = {ModelA.class, ModelB.class}))
public Page<?> search(Pageable pageable) {
However, this just disregards the Page and also isn't correct.
The next thing I tried is extending the PageImpl, overwriting the getContent method, and specifying the same schema on this method, but this wasn't included in the output at all.
Next was implementing Page<T> myself (and later removing the implements reference to Page<T>) and specifying the same schema on getContent, iterator, and the field itself, but also to no effect.
How do I tell spring-docs, what the content of the resulting Page might be?
I stumbled upon this when trying to solve a similar problem
Inspired from this thread Springdoc with a generic return type i came up with the following solution, and it seems to apply to your case also. Code examples are in Kotlin.
I introduced a stub class that will just act as the Schema for the response:
private class PageModel(
#Schema(oneOf = [ModelA::class, ModelB::class]))
content: List<Object>
): PageImpl<Object>(content)
Then i annotated my Controller like this:
#Operation(
responses = [
ApiResponse(
responseCode = "200",
content = [Content(schema = Schema(implementation = PageModel::class))]
)
]
)
fun getPage(pageable: Pageable): Page<Object>
This generated this api response:
"PageModel": {
"properties": {
"content": {
"items": {
"oneOf": [
{
"$ref": "#/components/schemas/ModelA"
},
{
"$ref": "#/components/schemas/ModelB"
}
],
"type": "object"
},
"type": "array"
},
... -> more page stuff from spring's PageImpl<>
And in the "responses" section for the api call:
"responses": {
"200": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/PageModel"
}
}
},
"description": "OK"
}
All generated openapi doc is similar to the autogenerated json when returning a Page, it just rewrites the "content" array property to have a specific type.

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

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.

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 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