I am trying to create springdoc swagger documentation, and I would like to represent a request body having data type Map<String, Object> in a better readable way for clients. But when I declare #io.swagger.v3.oas.annotations.parameters.RequestBody(content = #Content(schema = #Schema(implementation = Map.class) the Schema is coming as String(attached the screenshot below)
Method declaration
#Operation(security = {#SecurityRequirement(name = "bearer-key")}, summary = "Create Data", operationId = "createData", description = "Create createData for the **`type`**. ")
#ApiResponses(value = {
#ApiResponse(responseCode = "201", description = "Data created", content = #Content(schema = #Schema(implementation = Map.class),
examples = {#ExampleObject(value = "{\n" +
" \"id\": \"927d810c-3ac5-4584-ba58-7c11befabf54\",\n" +
"}")})),
#ApiResponse(responseCode = "400", description = "BAD Request")})
#PostMapping(value = "/data/type", produces = APPLICATION_JSON_VALUE, consumes = APPLICATION_JSON_VALUE)
#io.swagger.v3.oas.annotations.parameters.RequestBody(content = #Content(schema = #Schema(implementation = Map.class),
examples = {#ExampleObject(value = "{\n" +
" \"label\":\"tourism\",\n" +
" \"location\":\"France\"\n" +
" }")}))
ResponseEntity<Map<String, Object>> createData(#Parameter(name = "type", required = true) #PathVariable("type") String type, #Parameter(name = "request payload") #Valid #RequestBody Map<String, Object> body);
Though the Spring boot automatically infers the type based on the method signature, it is not clear for the data type Map. For instance, by default, the type Map<String, Object> will be inferred as below
But I would like to show the Schema in a more understandable way for the client who refers to my API. I could see there is a closed ticket without a proper solution in Github. As per my requirement, the request body should be a type agnostic and dynamic key-value pairs, so there is no other way apart from receiving the request as Map<String, Object>. has anyone implemented a better way with type Map rather than creating a custom request/response model?
I have one API endpoint, the request body expects a HashMap. There is not much information on how to fix the "Example value" issue. Prasanth's answer lead me to the right place. I'm posting my solution for completeness but all credit goes to him. (PS: I tried to upvote his answer but I don't have enough "points")
The configurations side:
#Configuration
#OpenAPIDefinition
public class DocsConfiguration {
#Bean
public OpenAPI customOpenAPI() {
Schema newUserSchema = new Schema<Map<String, Object>>()
.addProperties("name",new StringSchema().example("John123"))
.addProperties("password",new StringSchema().example("P4SSW0RD"))
.addProperties("image",new StringSchema().example("https://robohash.org/John123.png"));
return new OpenAPI()
//.servers(servers)
.info(new Info()
.title("Your app title")
.description("App description")
.version("1.0")
.license(new License().name("GNU/GPL").url("https://www.gnu.org/licenses/gpl-3.0.html"))
)
.components(new Components()
.addSchemas("NewUserBody" , newUserSchema)
);
}
}
The controller side:
#io.swagger.v3.oas.annotations.parameters.RequestBody(
content = #Content(mediaType = MediaType.APPLICATION_JSON_VALUE,
schema = #Schema(ref = "#/components/schemas/NewUserBody")))
#PostMapping("/v1/users")
public Response<User> upsertUser(#RequestBody HashMap<String,Object> user) {
//Your code here
}
Sharing my working approach for the issue, I have done a workaround for the #io.swagger.v3.oas.annotations.parameters.RequestBody(content = #Content(schema = #Schema(implementation = Map.class) the Schema is coming as String issue.
I have declared a custom schema called Map in the OpenAPI bean declaration as below
new OpenAPI()
.components(new Components()
.addSchemas("Map", new Schema<Map<String, Object>>().addProperties("< * >", new ObjectSchema())
))
.....
.....
and used the above schema in the Schema declaration as below
#io.swagger.v3.oas.annotations.parameters.RequestBody(
content = #Content(mediaType = APPLICATION_JSON_VALUE,
schema = #Schema(ref = "#/components/schemas/Map"))
The above approach can be used in the place of ApiResponse also as below
#io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "200",
content = #Content(mediaType = APPLICATION_JSON_VALUE,
schema = #Schema(ref = "#/components/schemas/Map"))
Note: If we use the above custom schema approach, we don't need to alter or ignore any of the types which SpringDoc is using internally.
This is the default behaviour of the springdoc-openapi library in order to ignore other injectable parameters supported by Spring MVC.
https://docs.spring.io/spring/docs/5.1.x/spring-framework-reference/web.html#mvc-ann-arguments
If you want to change this behaviour, you can just exlcude it as follow:
SpringDocUtils.getConfig().removeRequestWrapperToIgnore(Map.class);
Id like to update rodiri's answer for my situation. I had to combine the answer by rodiri and this answer by Ondřej Černobila to the SO question SpringDoc - How to Add schemas programmatically. I am using java 11, spring-boot-starter-parent 2.5.6, and springdoc-openapi-ui 1.5.12 which I believe is using swagger 3.52.5
<!-- https://mvnrepository.com/artifact/org.springdoc/springdoc-openapi-ui -->
<dependency>
<groupId>org.springdoc</groupId>
<artifactId>springdoc-openapi-ui</artifactId>
<version>1.5.12</version>
</dependency>
My config
import io.swagger.v3.oas.annotations.OpenAPIDefinition;
import io.swagger.v3.oas.models.media.ObjectSchema;
import io.swagger.v3.oas.models.media.StringSchema;
import org.springdoc.core.customizers.OpenApiCustomiser;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
#Configuration
#OpenAPIDefinition
public class DocsConfiguration {
#Bean
public OpenApiCustomiser openApiCustomiser() {
return openApi -> {
var NewUserBodySchema = new ObjectSchema()
.name("NewUserBody")
.title("NewUserBody")
.description("Object description")
.addProperties("name", new StringSchema().example("John123"))
.addProperties("password", new StringSchema().example("P4SSW0RD"))
.addProperties("image", new StringSchema().example("https://robohash.org/John123.png"));
var schemas = openApi.getComponents().getSchemas();
schemas.put(NewUserBodySchema.getName(), NewUserBodySchema);
};
}
}
For my endpoint I am using a get that returns a Map so its different from the accepted answer.
#GetMapping(value = "/{userId}")
#Operation(
summary = "Get Something",
description = "Some desciption",
responses = {
#ApiResponse(
responseCode = "200",
description = "The Map Response",
content = {
#Content(
mediaType = MediaType.APPLICATION_JSON_VALUE,
schema = #Schema(ref = "#/components/schemas/NewUserBody")
)
})
}
)
public ResponseEntity<Map<String, Object>> getMap(#PathVariable String userId)
I ran into this problem myself today. As it turns out, this is actually a design problem with Swagger (#see related question).
Nonetheless, I tried my hand at it too, using the approaches from here and the other thread.
Here is my OpenAPI with one custom schema for a Map<Integer,String>:
#Configuration
#OpenAPIDefinition(
info = #io.swagger.v3.oas.annotations.info.Info(
title = "ACME Inc. REST API",
version = "1.0",
description = "This is an overview of all REST endpoints of this application",
contact = #io.swagger.v3.oas.annotations.info.Contact(name = "John Doe", url = "https://acme-inc.com/", email = "john.doe#acme-inc.com")
)
)
public class OpenAPIConfig {
public static final String ERROR_CODE_MAPPER = "ErrorCode-Mapper";
#Bean
public OpenApiCustomiser openApiCustomiser() {
return openApi -> {
Components components = openApi.getComponents();
for(Schema<?> schema: buildCustomSchemas()) {
components.addSchemas(schema.getName(), schema);
}
};
}
private static List<Schema<?>> buildCustomSchemas() {
ArrayList<Schema<?>> result = new ArrayList<>();
Schema<?> integerStringMap = new Schema<Map<Integer, String>>()
.name(ERROR_CODE_MAPPER)
.type("object")
.addProperty("error code", new StringSchema().example("Error message belonging to the error code")).example(getErrorCodeExample());
result.add(integerStringMap);
// Build more custom schemas...
return result;
}
private static Map<Integer, String> getErrorCodeExample() {
Map<Integer, String> example = new HashMap<>();
example.put(666, "Oh no..., the devil himself showed up and stopped your request");
return example;
}
}
(NOTE: Look up your swagger source code io.swagger.v3.oas.models.media for useful utility classes like StringSchema. You don't have write everything from scratch.)
And this is my REST endpoint:
#Operation(summary = "This endpoint returns a list of system error codes, that can occur during processing requests.")
#ApiResponses(value = {
#ApiResponse(
responseCode = "200",
description = "Map of all system error codes mapping to their messages",
content = {#Content(mediaType = MediaType.APPLICATION_JSON_VALUE, schema = #Schema(ref = "#/components/schemas/"+ ERROR_CODE_MAPPER))}
)
})
#GetMapping("/error-codes")
public Map<Integer, String> listErrorCodes() {
// return your map here...
}
This produces something like this:
It is important to know that in a JSON object the key is always of type string. So the type does not have to be written explicitly. With that in mind, this is the schema:
I created a HashMap extension class:
#Schema(description = "Response-Object Map<String, EcoBalance).")
public class EcoMap extends HashMap<String, EcoBalance> {
#JsonIgnore
#Override
public boolean isEmpty() {
return super.isEmpty();
}
}
use it as response object
#ApiResponse(responseCode = "200", content = #Content(mediaType = .., schema = #Schema(implementation = EcoMap.class)), headers = ..
be aware the OpenAPI 3 generator does not generate such a client-model, but is properly referenced in openapi.yml (and even validates).
I have an folowing endpoint:
#PostMapping(value = "/home", consumes = MediaType.APPLICATION_FORM_URLENCODED_VALUE)
public Mono<String> getData(ServerWebExchange exchange) { return Mono.empty(); }
The ServerWebExchange object is implemented in org.springframework.web.server.
When I run it, in Swagger all the getters objects are shown. While I only need the body (I want to hide the reqest and the respone objects).
Tried to use
.ignoredParameterTypes(Principal.class, ServerHttpRequest.class, ServerHttpResponse.class)
But, it didn't had any effect.
Is there a way to hide those?
Solution found:
Desable the SeverWebExchange interface for swagger
Configure requier input.
`
#PostMapping(value = "/home", consumes = MediaType.APPLICATION_FORM_URLENCODED_VALUE)
#ApiImplicitParams({
#ApiImplicitParam(name = "Body Params", paramType = "body")
})
public Mono<String> getData(
#ApiIgnore ServerWebExchange exchange
) {
return Mono.empty();
}
I instatiate docket like this
#Bean
public Docket api() {
return new Docket(DocumentationType.SWAGGER_2)
.select()
.apis(RequestHandlerSelectors.basePackage("com.config.internal"))
.paths(Predicates.or(PathSelectors.ant("/api**/**")))
.build();
}
I created a set of stub endpoints that imitate the real one for /login or /oauth.
#Api("Authentication")
#RequestMapping("/api")
public interface LoginEndpointApi {
#ApiOperation(value = "Github SSO endpoint", notes = "Endpoint for Github SSO authentication")
#ApiResponses({
#ApiResponse(code = 200, message = "HTML page of main application")
})
#GetMapping("/oauth/github")
default void oauthGithub() {
throw new UnsupportedOperationException();
}
#ApiOperation(value = "Get CSRF token", notes = "Returns current CSRF token")
#ApiResponses({
#ApiResponse(code = 200, message = "CSRF token response", response = String.class,
examples = #Example({#ExampleProperty(value = "015275eb-293d-4ce9-ba07-ff5e1c348092")}))
})
#GetMapping("/csrf-token")
default void csrfToken() {
throw new UnsupportedOperationException();
}
#ApiOperation(value = "Login endpoint", notes = "Login endpoint for authorization")
#ApiResponses({
#ApiResponse(code = 200, message = "Successful authentication")
})
#PostMapping("/login")
default void login(
#ApiParam(required = true, name = "login", value = "login body")
#RequestBody LoginRequest loginRequest) {
throw new UnsupportedOperationException();
}
}
But it doesn't recognize it. It is located in the same com.config.internal package as I described.
But the page swagger ui is empty and shows that No operations defined in spec!
What is the problem?
If you want to provide swagger documentation for your request mappings specified above you could simply describe it with .paths(Predicates.or(PathSelectors.ant("/api/**"))) path matchers. But if your path includes something more complicated like api + text without backslash separator then you should get known with
https://docs.spring.io/spring/docs/3.1.x/javadoc-api/org/springframework/util/AntPathMatcher.html
I have a Spring boot controller with a method like this:
// CREATE
#RequestMapping(method=RequestMethod.POST, value="/accounts")
public ResponseEntity<Account> createAccount(#RequestBody Account account,
#RequestHeader(value = "Authorization") String authorizationHeader,
UriComponentsBuilder uriBuilder) {
if (!account.getEmail().equalsIgnoreCase("")) {
account = accountService.createAccount(account);
HttpHeaders headers = new HttpHeaders();
System.out.println( "Account is null = " + (null == account)); //For debugging
headers.setLocation(uriBuilder.path("/accounts/{id}").buildAndExpand(account.getId()).toUri());
return new ResponseEntity<>(account, headers, HttpStatus.CREATED);
}
return new ResponseEntity<>(null, null, HttpStatus.BAD_REQUEST);
}
I'm trying to unit test it like this:
#Test
public void givenValidAccount_whenCreateAccount_thenSuccessed() throws Exception {
/// Arrange
AccountService accountService = mock(AccountService.class);
UriComponentsBuilder uriBuilder = mock(UriComponentsBuilder.class);
AccountController accountController = new AccountController(accountService);
Account account = new Account("someone#domain.com");
/// Act
ResponseEntity<?> createdAccount = accountController.createAccount(account, "", uriBuilder);
/// Assert
assertNotNull(createdAccount);
//assertEquals(HttpStatus.CREATED, createdAccount.getStatusCode());
}
but the account is always null. Any idea why is that ?
You may want to check my answer in How to test this method with spring boot test?
Not only you will find an answer to unit testing controllers but also how to include filters, handlers and interceptors in your test.
Hope this helps,
Jake
I think you need to when clause first of all.
when(accountController.createAccount(account, "", uriBuilder)).then(createAccount);
ResponseEntity<?> createdAccount = accountController.createAccount(account, "", uriBuilder);
I'm using Spring Boot 1.5.4, Spring Data REST, Spring Security. I created a #Controller mapped to a specific path that doesn't require authentication because it used from a sms gateway for report incoming texts.
So I've just to create a controller to read those parameters and then save the text on the db. And here there is the problem. To store data I use repositories that are secured, while in the controller I've not any kind of security (in fact I cannot ask the provider to secure its calls).
I tried to set an authentication context programatically but seems not working:
#Controller
#RequestMapping(path = "/api/v1/inbound")
#Transactional
public class InboundSmsController {
private Logger log = LogManager.getLogger();
#RequestMapping(method = RequestMethod.POST, path = "/incomingSms", produces = "text/plain;charset=ISO-8859-1")
public ResponseEntity<?> incomingSms(#RequestParam(name = "Sender", required = true) String sender,
#RequestParam(name = "Destination", required = true) String destination,
#RequestParam(name = "Timestamp", required = true) String timestamp,
#RequestParam(name = "Body", required = true) String body) {
log.info(String.format("Text received from %s to %s at %s with content: %s", sender, destination, timestamp, body));
setupAuthentication();
try {
int transitsWithSameTextToday = transitCertificateRepository.countByTextAndDate(body, Instant.now()); //This is the method that raises an Auth exception
....
....
} finally(){
clearAuthentication();
}
SecurityContext context;
/**
* Set in the actual context the authentication for the system user
*/
private void setupAuthentication() {
context = SecurityContextHolder.createEmptyContext();
Collection<GrantedAuthority> authorities = AuthorityUtils.createAuthorityList("ROLE_ADMIN");
Authentication authentication = new UsernamePasswordAuthenticationToken("system", "ROLE_ADMIN", authorities);
context.setAuthentication(authentication);
}
private void clearAuthentication() {
context.setAuthentication(null);
}
The method countByTextAndDate is annotated with #PreAuthorize("isAuthenticated()")
I'm surprised also setting the Auth context I've this error. Am I doing something wrong? Is this the best way to reach my goal?
I don't want to annotate my method with #PermitAll because that method is also exposed by Spring Data REST and I don't want anyone can use that.
You are looking for AccessDecisionManager's RunAsManager. Here's the link that could help you with this :
http://www.baeldung.com/spring-security-run-as-auth
Happy Coding!!!