springdoc-openapi (Spring Boot)
Introduction
The springdoc-openapi Java library helps automating the generation of API documentation using Spring Boot projects.
It's an alternative to Springfox (which it's abandoned and the latest version didn't support Spring Boot >= 2.2).
This library supports:
OpenAPI 3
Spring Boot 3.x (SpringDoc >= 2.0.0)
Spring Boot 2.x and 1.5.x (SpringDoc <= 1.7.0), see compatibility matrix
JSR-303, specifically for @NotNull, @Min, @Max, and @Size.
Swagger-ui
Oauth 2
JSON serialization & deserialzation (see also)
https://sites.google.com/site/pawneecity/sprint-boot/json-serialization-spring-boot
Reference
SpringDoc OpenAPI (full documentation)
SpringDoc OpenAPI (GitHub)
https://github.com/springdoc/springdoc-openapi
SpringDoc OpenAPI with SpringBoot compatibility matrix
https://springdoc.org/#what-is-the-compatibility-matrix-of-springdoc-openapi-with-spring-boot
swagger-annotations API (v2.2.7)
https://docs.swagger.io/swagger-core/v2.2.7/apidocs/
Sample Baeldung spring-boot-springdoc
https://www.baeldung.com/spring-rest-openapi-documentation
Springdoc OpenApi3 Swagger [JWT & OAuth 2 Client Credentials]
https://ravikrs.com/posts/2022/05/springdoc-openapi3-swagger/
OpenAPI 3 Data Models (Schemas)
Data Types
Reference: https://swagger.io/docs/specification/data-models/
--------------------------------------------------------------------------------------------------
type format keywords
--------------------------------------------------------------------------------------------------
string date, date-time, password, byte, binary minLength, maxLength, pattern
·Non-standard formats: email, uuid, uri, hostname, ipv4, ipv6 and others
·Files use 'byte' if base64-encoded or 'binary' if binary file content
·Dates, eg: "2017-07-21", "2017-07-21T17:32:28Z", use RFC 3339, section 5.6
--------------------------------------------------------------------------------------------------
number -, float, double minimum, exclusiveMinimum , maximum, exclusiveMaximum, multipleOf
--------------------------------------------------------------------------------------------------
integer -, int32, int64
--------------------------------------------------------------------------------------------------
boolean
--------------------------------------------------------------------------------------------------
array items, minItems, maxItems, uniqueItems
--------------------------------------------------------------------------------------------------
object properties, required, additionalProperties, minProperties, maxProperties
--------------------------------------------------------------------------------------------------
Notes:
OpenAPI 3.0 does not have an explicit null type as in JSON Schema, but you can use nullable: true to specify that the value may be null.
RFC 3339, section 5.6 > https://tools.ietf.org/html/rfc3339#section-5.6
oneOf, anyOf, allOf, not
Reference: https://swagger.io/docs/specification/data-models/oneof-anyof-allof-not/
OpenAPI 3.0 provides several keywords which you can use to combine schemas.
oneOf – validates the value against exactly one of the subschemas
allOf – validates the value against all the subschemas
anyOf – validates the value against any (one or more) of the subschemas
Besides these, there is a not keyword which you can use to make sure the value is not valid against the specified schema.
Configuration
For Spring Boot >= 3 (config)
For spring-boot v3 support, make sure you use springdoc-openapi v2 > https://springdoc.org/v2/ (not for sb <3)
Migrating from springdoc-openapi v1 to v2 > https://springdoc.org/v2/#migrating-from-springdoc-v1
pom.xml
<project>
<properties>
<springdoc-openapi.version>2.2.0</springdoc-openapi.version>
<therapi.version>0.15.0</therapi.version>
</properties>
<dependencies>
<!-- provides swagger-ui -->
<dependency>
<groupId>org.springdoc</groupId>
<artifactId>springdoc-openapi-starter-webmvc-ui</artifactId>
<version>${springdoc-openapi.version}</version>
</dependency>
<!--[Swagger-UI] Springdoc Javadoc runtime library -->
<dependency>
<groupId>org.springdoc</groupId>
<artifactId>springdoc-openapi-starter-common</artifactId>
<version>${springdoc-openapi.version}</version>
</dependency>
<!--[Swagger-UI] Javadoc support runtime library -->
<dependency>
<groupId>com.github.therapi</groupId>
<artifactId>therapi-runtime-javadoc</artifactId>
<version>${therapi.version}</version>
</dependency>
<!-- Swagger v3 annotations (springdoc & openapi-generator) -->
<dependency>
<groupId>io.swagger.parser.v3</groupId>
<artifactId>swagger-parser</artifactId>
<version>2.1.9</version>
</dependency>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<!-- version>${maven-compiler-plugin.version}</version -->
<configuration>
<annotationProcessorPaths>
<path>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>${lombok.version}</version>
</path>
<!--[Swagger-UI] Springdoc Javadoc annotation processor -->
<path>
<groupId>com.github.therapi</groupId>
<artifactId>therapi-runtime-javadoc-scribe</artifactId>
<version>${therapi.version}</version>
</path>
<!-- (sb3 disabled)
<path>
<groupId>org.hibernate</groupId>
<artifactId>hibernate-jpamodelgen</artifactId>
<version>${hibernate.version}</version>
</path>
-->
</annotationProcessorPaths>
<encoding>${project.build.sourceEncoding}</encoding>
<!--[Swagger-UI] Springdoc needed SpringBoot>=3.2 (parameter name discovery) -->
<parameters>true</parameters>
</configuration>
<dependencies>
<!-- Java bytecode manipulation framework -->
<dependency>
<groupId>org.ow2.asm</groupId>
<artifactId>asm</artifactId>
<version>9.4</version>
</dependency>
</dependencies>
</plugin>
application.properties
#Springdoc
springdoc.api-docs.version=openapi-3-1
springdoc.show-actuator=true
SpringdocConfig.java
Sample OpenAPI Bean configuration method (using Spring Boot 3.0):
package edu.cou.myapp_gendata;
import java.time.Instant;
import org.springdoc.core.models.GroupedOpenApi;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import io.swagger.v3.oas.annotations.OpenAPIDefinition;
import io.swagger.v3.oas.models.Components;
import io.swagger.v3.oas.models.OpenAPI;
import io.swagger.v3.oas.models.info.Info;
import io.swagger.v3.oas.models.security.SecurityScheme;
import io.swagger.v3.oas.models.servers.Server;
import lombok.val;
/**
* OpenAPI Swagger-UI configuration bean.<br>
*
* --- OpenAPI Swagger-UI URLs ---<br>
* http://localhost:8080/swagger-ui.html<br>
* http://localhost:8080/v3/api-docs<br>
* <br>
*/
@OpenAPIDefinition
@Configuration
public class SpringdocConfig {
/** Security scheme key for: Bearer Token */
public static final String BEARER_TOKEN_KEY = "bearer-token";
/** Security scheme key for: OAuth2 Client Credentials */
public static final String OAUTH2_CLIENT_CREDENTIALS_KEY = "oauth2-client-credentials";
/**
* Definition: app.
* @return -
*/
@Bean
GroupedOpenApi appOpenApi() {
var packagesToScan = new String[] { "edu.cou.myapp_gendata.controller" };
var pathsToMatch = new String[] { "/r/**" };
var pathsToExclude = new String[] { "/r/caches/**", "/r/gendata/**", "/r/merida/**" };
return GroupedOpenApi.builder().group("app").packagesToScan(packagesToScan).pathsToMatch(
pathsToMatch).pathsToExclude(pathsToExclude).build();
}
/**
* Definition: export.
*
* @return -
*/
@Bean
GroupedOpenApi gendataOpenApi() {
String[] paths = { "/r/gendata/**" }; // NOSONAR
return GroupedOpenApi.builder().group("gendata").pathsToMatch(paths).build();
}
/**
* Definition: misc.
*
* @return -
*/
@Bean
GroupedOpenApi miscOpenApi() {
String[] paths = { "/*", "/r/caches/**", "/r/merida/**", "/r/tech/**" }; // NOSONAR
return GroupedOpenApi.builder().group("misc").pathsToMatch(paths).build();
}
/**
* OpenAPI Swagger-UI configuration.
*
* @return -
*/
@Bean
OpenAPI customOpenAPI() {
val locUrl = "http://localhost:8080";
val devUrlApi = "https://TBD.execute-api.eu-west-1.amazonaws.com/dev/gat_gendata_ws";
val devUrlDns = "https://myapp.cou.uk/ws";
val testUrl = "https://myapp.test.cou.edu/ws";
val preUrl = "https://myapp.pre.cou.edu/ws";
val proUrl = "https://myapp.cou.edu/ws";
// DOC. Property 'version'. Required by spec [https://editor.swagger.io/]
val infoVersion = Instant.now().toString();
val myapp = App.SYSTEM.toString();
val oauthAuthorizationUrl = "https://auth.test.cou.edu/auth2/authorize"; //?
val oauthTokenUrl = "https://auth.test.cou.edu/auth2/token";
return new OpenAPI().addServersItem(new Server().url(locUrl)).addServersItem(new Server().url(
devUrlApi)).addServersItem(new Server().url(devUrlDns)).addServersItem(new Server().url(
testUrl)).addServersItem(new Server().url(preUrl)).addServersItem(new Server().url(
proUrl))//
.info(new Info().version(infoVersion).title(myapp).description(
"WARNING: If having Swagger UI issues w/ Chrome then use Firefox instead."))//
.components(new Components().addSecuritySchemes(BEARER_TOKEN_KEY, new SecurityScheme().type(
SecurityScheme.Type.HTTP).scheme("bearer").description(
"OAuth access token in the scope myapp.gendata"))
.addSecuritySchemes(OAUTH2_CLIENT_CREDENTIALS_KEY, new SecurityScheme().type(
SecurityScheme.Type.OAUTH2).flows(new OAuthFlows().clientCredentials(new OAuthFlow().authorizationUrl(oauthAuthorizationUrl).tokenUrl(oauthTokenUrl).scopes(new Scopes().addString("myapp.export", "Client Credentials")))))
);
}
}
WebSecurityCofig.java
This sample adds anonymous access for loading Swagger-UI, using Spring Boot 3.
package edu.uoc.gpradoc;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.annotation.Order;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityCustomizer;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.firewall.StrictHttpFirewall;
import jakarta.inject.Inject;
/**
* Note: Order lower values have higher priority. Higher values will only execute if no adapter with
* lower value matches.
*/
@Configuration
@EnableWebSecurity
public class WebSecurityConfig {
/**
* Springdoc v2.
*
* @param http
* @return -
* @throws Exception
*/
@Order(6)
@Bean
SecurityFilterChain filterChainSpringDocV2(HttpSecurity http) throws Exception {
final String[] pathAnonymous = { "/swagger-ui.html", "/swagger-ui/**", "/v3/api-docs/**" };
http// sb3
.securityMatcher(pathAnonymous) // only invoke if matching
.authorizeHttpRequests(authorize -> authorize//
.anyRequest().anonymous()//
) //
;
http.csrf().disable();
http.headers().frameOptions().disable();
// DOC. Avoid generation of cookie JSESSIONID (Swagger)
http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
return http.build();
}
}
For SpringBoot < 3 (config)
pom.xml
<project
<properties>
<springdoc-openapi.version>1.5.12</springdoc-openapi.version>
<dependencies>
<dependency>
<!-- openapi-ui replaces swagger-ui -->
<groupId>org.springdoc</groupId>
<artifactId>springdoc-openapi-ui</artifactId>
<version>${springdoc-openapi.version}</version>
<exclusions>
<exclusion>
<groupId>org.springframework</groupId>
<artifactId>spring-web</artifactId>
</exclusion>
</exclusions>
</dependency>
<!-- Needed if using openapi-ui with data-rest pagination -->
<dependency>
<groupId>org.springdoc</groupId>
<artifactId>springdoc-openapi-data-rest</artifactId>
<version>${springdoc-openapi.version}</version>
<exclusions>
<exclusion>
<!-- was 5.0.4, but maven-compiler-plugin used dependency 9.1 -->
<groupId>org.ow2.asm</groupId>
<artifactId>asm</artifactId>
</exclusion>
</exclusions>
</dependency>
<!-- Should be added if using openapi-ui with spring-security -->
<dependency>
<groupId>org.springdoc</groupId>
<artifactId>springdoc-openapi-security</artifactId>
<version>${springdoc-openapi.version}</version>
</dependency>
<!-- Swagger v3 annotations (springdoc & openapi-generator) -->
<dependency>
<groupId>io.swagger.parser.v3</groupId>
<artifactId>swagger-parser</artifactId>
<version>2.0.29</version>
</dependency>
SpringdocConfig.java
Sample OpenAPI Bean configuration method (using Spring Boot 2.3):
package edu.cou.myapp;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import edu.cou.myapp.constants.SystemEnum;
import io.swagger.v3.oas.annotations.OpenAPIDefinition;
import io.swagger.v3.oas.models.Components;
import io.swagger.v3.oas.models.OpenAPI;
import io.swagger.v3.oas.models.info.Info;
import io.swagger.v3.oas.models.security.SecurityScheme;
import io.swagger.v3.oas.models.servers.Server;
/**
* OpenAPI Swagger-UI configuration bean.<br>
* <br>
*/
@OpenAPIDefinition
@Configuration
public class SpringdocConfig {
/**
* Definition: misc.
*
* @return -
*/
@Bean
public GroupedOpenApi miscOpenApi() {
String[] paths = { "/r/j/cachingController/**", "/r/j/tech/**" };
return GroupedOpenApi.builder().group("misc").pathsToMatch(paths).build();
}
/**
* Definition: app.
*
* @return -
*/
@Bean
public GroupedOpenApi appOpenApi() {
String[] packagesToScan = { "edu.cou.myapp.controller" };
String[] pathsToExclue = { "/r/j/cachingController/**", "/r/j/tech/**" };
return GroupedOpenApi.builder().group("app").packagesToScan(packagesToScan).pathsToExclude(
pathsToExclue).build();
}
/**
* OpenAPI Swagger-UI configuration.<br>
* http://localhost:8080/swagger-ui.html<br>
* http://localhost:8080/v3/api-docs<br>
*
* @return -
*/
@Bean
public OpenAPI customOpenAPI() {
final String locUrl = "http://localhost:8080";
final String devUrl = "https://myapp.dev.me.uk/myapp_back";
final String testUrl = "https://unkown-test.cou.edu/myapp_back";
final String preUrl = "https://unkown-pre.cou.edu/myapp_back";
final String proUrl = "https://unkown.cou.edu/myapp_back";
// DOC. Property 'version'. Required by spec [https://editor.swagger.io/]
val infoVersion = Instant.now().toString();
return new OpenAPI().addServersItem(new Server().url(locUrl)).addServersItem(new Server().url(
devUrl)).addServersItem(new Server().url(testUrl)).addServersItem(new Server().url(preUrl))
.addServersItem(new Server().url(proUrl)).info(new Info().version(infoVersion).title(SystemEnum.MYAPP.name())
.description(
"NOTE: If having Swagger UI issues w/ Chrome then use Firefox instead."))
.components(new Components().addSecuritySchemes("bearer-key", new SecurityScheme().type(
SecurityScheme.Type.HTTP).scheme("bearer").description(
"OAuth token in the scope myapp.whatever")));
}
}
How to add server urls to an specific GroupedOpenAPI?
@Bean
GroupedOpenApi gendataOpenApi() {
var paths = new String[] { "/r/gendata/**" };
final OpenApiCustomizer customizer = openApi -> openApi//
.addServersItem(new Server().url("http://myapp-test.mydomain.edu:80"))//
.addServersItem(new Server().url("http://myapp-pre.mydomain.edu:80"))//
.addServersItem(new Server().url("http://myapp.mydomain.edu:80"));
return GroupedOpenApi.builder().group("gendata").pathsToMatch(paths).addOpenApiCustomizer(
customizer).build();
}
Generate html, pdf from OpenAPI spec
Go to https://editor.swagger.io.
Paste your OpenAPI 3.0 YAML/JSON definition.
Select Generate Client > html.
Download & unzip the file.
Open the index.html page in a browser, e.g. Chrome.
Select File > Print, change the Destination to Save as PDF, and save the page.
OpenAPI 3, springdoc-openapi compatible, annotation examples
Interface or class documentation
Tag is used by OpenAPI spec to group operations:
@Tag(name = "pet", description = "Everything about your Pets")
Authorization
In order for Swagger UI to send the Authorization header, the REST operation must be decorated as follows:
@Operation(summary = "Whatever", security = { @SecurityRequirement(name = "bearer-key") })
Notes:
Without specifying any parameter
Alternatively, it can be placed only at the class level if it applies to all the operations:
@SecurityRequirement(name = "bearer-key")
Attribute w/ an array of elements
Tested fine w/ springdoc 2.2.0 in Spring Boot 3.2.0 (it fails w/ springdoc 2.3.0):
import io.swagger.v3.oas.annotations.media.ArraySchema;
import io.swagger.v3.oas.annotations.media.Schema;
...
@ArraySchema(
arraySchema = @Schema(allowableValues = { "ar", "bn", "es", "ca", "en", "cmn", "de",
"fr", "hi", "it", "jp", "pa", "pl", "pt", "ru", "ur", "yue" },
description = "Teaching alternatives languages IANA codes", example = "[\"pl\", \"yue\"]"),
minItems = 0,
schema = @Schema(type = "[Ljava.lang.String;", implementation = String.class,
requiredMode = Schema.RequiredMode.REQUIRED),
uniqueItems = true
)
private LinkedHashSet<String> teachingLangAlternativeIanaCodes;
Response w/ an array of elements
Sample returning a set (items in array are unique) having at least one item :
import io.swagger.v3.oas.annotations.media.ArraySchema;
import io.swagger.v3.oas.annotations.responses.ApiResponse;
import io.swagger.v3.oas.annotations.responses.ApiResponses;
...
@ApiResponses(value = { //
@ApiResponse(responseCode = "200", description = "OK", content = @Content(mediaType = MediaType.APPLICATION_JSON_VALUE, array = @ArraySchema(minItems = 1, schema = @Schema(implementation = MonitorOutDto.class), uniqueItems = true))), //
HTTP Header parameters
@Parameter(hidden = true) @RequestHeader(name = "Authorization", required = true) @NotBlank String headerAuthorization //
@Schema
· Decorating a class
By default, the schema name is generated with the class name. The 'title' attribute replaces it.
@Schema(description = "A house", title = "House")
public class HouseDto {
· Decorating an attribute
@Schema(description = "My attr", hidden = false, requiredMode = RequiredMode.REQUIRED)
private MyType myAttribute;
@Schema(allowableValues = {"1000", "5000"}, description = "My attr", example = "1000", type = "Ljava.lang.Integer;")
private Integer myAttribute;
Pagination (using Pageable)
For JPA pagination, see:
https://sites.google.com/site/pawneecity/sprint-boot/data-jpa-and-querydsl-spring-boot#h.8h51dfka35ys
Imports:
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Slice;
import org.springframework.data.domain.Sort.Direction;
import org.springframework.data.web.PageableDefault;
import org.springdoc.core.annotations.ParameterObject;
Javadoc:
* @param pageable Eg: {"page": 0, "size": 8, "sort": ["id,desc", "name,asc"]}
Operation:
@PreAuthorize("hasRole('ROLE_ADMIN')")
@GetMapping(value = "")
public ResponseEntity<Slice<AssignmentPetitionLineDto>> getDataSent(//
@Parameter(description = "Language", required = true, example = "ca") @RequestParam(
name = "lang", required = true) @NotNull @LangIso639p1Val String lang, //
@Parameter(description = "Request identifier", required = true,
example = "76ec1aa3-98b0-464a-b370-dc5a16ba9472") @RequestParam(name = "rId",
required = true) @NotNull UUID rId, //
@Parameter(description = "Calendar", required = true, example = "20212") @RequestParam(
name = "calendarCode", required = true) @NotNull @Pattern(
regexp = "^\\d{5}$") String calendarCode, //
@PageableDefault(size=3, sort={"prop1", "prop2"}, direction= Direction.DESC @ParameterObject Pageable pageable //
) {
// a] Verify rId
this.appRequestService.validate(rId);
log.info(String.format("INI getDataSent(%s,%s,%s)", rId, calendarCode, pageable));
// b] Delegate business logic
Slice<AssignmentPetitionLine> res = this.assignmentService.getDataSent(lang, calendarCode,
pageable);
// c] Build response
Slice<AssignmentPetitionLineDto> ret = AssignmentPetitionLineDto.of(res);
log.info(String.format("END getDataSent(%s,%s,%s)", rId, calendarCode, pageable));
return new ResponseEntity<>(ret, HttpStatus.OK);
}
Generic (parametrized type) class for Swagger API response
As of Swagger 2.0, it doesn't support parametrized types as responses. Here is a workaround.
Sample operation returning a parametrized type:
@ApiResponses(value = { //
@ApiResponse(responseCode = "200", description = "OK", content = @Content(
mediaType = "application/json", schema = @Schema(
implementation = SliceAssignmentPetitionDetailDto.class)))
)
public ResponseEntity<Slice<AssignmentPetitionDetailDto>> myMethod( @Parameter(description = "Pageable", required = true) Pageable pageable )
The private class at the controller:
/*- For Swagger response contract generation only */
private class SliceAssignmentPetitionDetailDto extends SliceImpl<AssignmentPetitionDetailDto>{
private static final long serialVersionUID = 7083154898222511806L;
public SliceAssignmentPetitionDetailDto(List<AssignmentPetitionDetailDto> content,
Pageable pageable, boolean hasNext) {
super(content, pageable, hasNext);
// unused, just for Swagger contract generation
}
}
oneOf in a RequestBody
(Non-working solution, as of springdoc 1.6.9) Annotating the controller body parameter with 'oneOf' doesn't create schemas in the generated spec:
@io.swagger.v3.oas.annotations.parameters.RequestBody(content = @Content(schema = @Schema(
oneOf = { GetLearningObjectProductionStatusOwnInDto.class,
GetLearningObjectProductionStatusExternalInDto.class }))) @RequestBody(
required = true) @NotNull @Valid GetLearningObjectProductionStatusInDto in
(Working solution) Annotate the superclass, may be interface or abstract, for spring-doc to generate the schemas in spec:
@NoArgsConstructor
@AllArgsConstructor
@Data
@EqualsAndHashCode(callSuper = true)
@Schema(oneOf = { GetLearningObjectProductionStatusOwnInDto.class,
GetLearningObjectProductionStatusExternalInDto.class })
public abstract class GetLearningObjectProductionStatusInDto
GET with request body (workaround)
It's valid, under certain circumstances, to send a body in a GET operation as per RFC 9110:
"A client SHOULD NOT generate content in a GET request unless it is made directly to an origin server that has previously indicated, in or out of band, that such a request has a purpose and will be adequately supported."
Springdoc doesn't support it because its documentation says:
GET, DELETE and HEAD are no longer allowed to have request body because it does not have defined semantics as per RFC 7231
but RFC 7231 is now obsolete, replaced by RFC 9110 (published in 2022-jun).
Workaround:
This workaround will make Springdoc properly generate the OpenApi contract. Swagger-UI will properly generate the cURL command but won't make the invocation showing message "TypeError: Window.fetch: HEAD or GET Request cannot have a body." (the cURL can be copied and executed out of Swagger-UI).
It's based in > https://github.com/springdoc/springdoc-openapi/issues/129
The trick has these steps:
At the @Operation annotation, add the attribute 'requestBody'.
Hide from Swagger the request body parameter with Parameter(hidden = true)
Example:
@Operation(summary = "[isAuthenticated] Get Learning Object Delivery by id",
requestBody = @io.swagger.v3.oas.annotations.parameters.RequestBody(
description = "REST request common technical fields", content = @Content(schema = @Schema(
implementation = GetInDto.class)), required = true))
@ApiResponses(value = { // NOSONAR
@ApiResponse(code = 200, message = "OK", response = LearningObjectDeliveryDto.class), //
@ApiResponse(code = 403, message = "User might not have the necessary permissions",
response = ErrorMessage.class), //
@ApiResponse(code = 500, message = "An unexpected condition was encountered",
response = ErrorMessage.class), //
@ApiResponse(code = 503, message = "Service unavailable", response = ErrorMessage.class), //
@ApiResponse(code = 504, message = "No timely response received from 3rd party system",
response = ErrorMessage.class) //
})
@GetMapping(value = "/learning-object-deliverys/{id}")
public ResponseEntity<LearningObjectDeliveryDto> getLearningObjectDelivery( //
@RequestHeader(name = "X-Requested-With",
required = false) @Nullable String headerXRequestedWith, //
@Parameter(hidden = true) @RequestHeader(name = "Authorization",
required = true) @Nullable String authorizationHeader, //
//
@PathVariable("id") @NotNull Long id, //
//
@io.swagger.v3.oas.annotations.Parameter(hidden = true) @NotNull @Valid GetInDto in //
)
DELETE with request body (workaround)
A DELETE operation can have an optional request body and it can have an optional response body.
Some API Gateway software may be unable to forward to the backend the header Content-Type and the payload.
A workaround is to pass the values in the query string instead of the request body. This is achieved by indicating to render the object parameter in the query string with the Parameter annotation attribute "in = ParameterIn.QUERY":
@Operation(summary = "Whatever.")
@DeleteMapping(value = "/learning-object-deliverys-otpet25002/{id}")
public ResponseEntity<Void> deleteLearningOjectDeliveryOtpet25002( // NOSONAR
@RequestHeader(name = "X-Requested-With",
required = false) @Nullable String headerXRequestedWith, //
@Parameter(hidden = true) @RequestHeader(name = "Authorization",
required = true) @Nullable String authorizationHeader, //
@RequestHeader(name = "X-Api-Key", required = true) @NotBlank String sessionId, //
//
@PathVariable("id") @NotNull Long id, //
//
@Parameter(description = "Technical properties",
in = ParameterIn.QUERY) /* @RequestParam */ @NotNull @Valid OpInDto in //
) {
return this.deleteLearningOjectDelivery(headerXRequestedWith, authorizationHeader,
sessionId, id, in);
}
Optional attribute: not provided vs provided with null value (PATCH)
Warning: It's highly discouraged to to design a REST API assuming default values for non-provided parameters. Otherwise, it wouldn't be possible to invoke that operation without filtering by that criterion.
Sometimes it's needed to differentiate between a value (attribute of parameter) not provided in the request and a value explicitly set to null. This can be useful, eg, for an attribute or request parameter where treatment will be different if it's not provided or if it's explicitly provided with value null.
Typical use cases:
PATCH operation where only explicitly provided attributes will be updated (even if the value is null) but not provided attributes won't be updated. A
REST operation with optional filters where if the parameter is not provided then it isn't used for filtering. If it's provided with a null value then only entities matching that null value for the parameter are returned.
Sample inner class with Optional attributes:
/**
* Inner class.<br>
* -By default, Jackson deserializes JSON String null to Java String "". Behavior is changed in the setter invoked by Jackson.<br>
* -By default, Jackson serializes all attributes. Behavior is changed to exclude null (but not Optional.empty).
*/
@JsonInclude(JsonInclude.Include.NON_NULL)
@NoArgsConstructor
@Data
@EqualsAndHashCode(callSuper = true)
@Schema(description = "ONLY inform those JSON properties to be modified"
+ " (other properties must NOT appear in the JSON request)")
private class TestOptionalAttrs {
@Schema(nullable = true)
@Nullable // NOSONAR (if absent property)
Optional<Integer> int1;
@Schema(nullable = true)
@Nullable // NOSONAR (if absent property)
Optional<Integer> int2;
@Schema(nullable = true)
@Nullable // NOSONAR (if absent property)
Optional<Integer> int3;
@Schema(nullable = true)
@Nullable // NOSONAR (if absent property)
Optional<String> str4;
@SuppressWarnings("unused")
public void setStr4(String s) {
this.str4 = Optional.ofNullable(s.isEmpty() ? null : s);
}
@Schema(nullable = true)
@Nullable // NOSONAR (if absent property)
Optional<String> str5;
@SuppressWarnings("unused")
public void setStr5(String s) {
this.str5 = Optional.ofNullable(s.isEmpty() ? null : s);
}
@Schema(nullable = true)
@Nullable // NOSONAR (if absent property)
Optional<String> str6;
@SuppressWarnings("unused")
public void setStr6(String s) {
this.str6 = Optional.ofNullable(s.isEmpty() ? null : s);
}
}
Sample REST GET operation using the attributes as optional parameters:
/**
* Test optional attrs.
*
* @param optAttrs Always instantiated, but Swagger needs (at)Nullable for not requiring it
* @return Interpreted TestOptionalAttrs instance
*/
@GetMapping(value = "/test-optional")
public ResponseEntity<TestOptionalAttrs> testOptional( //
@Parameter(description = "Test optional attrs", name = "TestOptionalAttrs",
required = false) @Nullable TestOptionalAttrs optAttrs //
) {
return new ResponseEntity<>(optAttrs, HttpStatus.OK);
}
Sample request (note properties 2 and 5 are not provided, 3 and 6 are provided with null value)
curl "http://localhost:8080/rest/export/test-optional?int1=27&int3=&str4=MyValue&str6="
The deserialized Java Object is (notice property not provided and property provided with null are different):
TestOptionalAttrs(int1=Optional[27], int2=null, int3=Optional.empty, str4=Optional[MyValue], str5=null, str6=Optional.empty)
An the response of the operation (notice that only attributes deserialized from the request are serialized):
{
"int1": 27,
"int3": null,
"str4": "MyValue",
"str6": null
}
Swagger Editor (create or modify an OpenAPI spec)
Online Editor available at > https://editor.swagger.io/
Sample (Handcrafted OAuth userinfo spec)
{
"openapi": "3.0.1",
"info": {
"title": "COU OAuth (handcrafted)",
"description": "This is a handcrafted OpenAPI 3 spec for the COU OAuth userinfo operation.",
"contact": {
"email": "wsparcie@cou.edu"
},
"version": "0.9.2"
},
"servers": [
{
"url": "https://auth.test.cou.edu:443"
},
{
"url": "https://oauth.test.am.cou.edu:443"
}
],
"paths": {
"/userinfo": {
"get": {
"tags": [
"oauth"
],
"summary": "Get user info which includes id, affiliations, etc.",
"operationId": "getUserinfo",
"responses": {
"200": {
"description": "OK",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Oauth2Userinfo"
}
}
}
},
"401": {
"description": "Unauthenticated",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Oauth2Error"
}
}
}
},
"default": {
"description": "Default error",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Oauth2Error"
}
}
}
}
},
"security": [
{
"bearer-key": []
}
]
}
}
},
"components": {
"schemas": {
"Oauth2Userinfo": {
"description": "COU OAuth2 Userinfo structure",
"type": "object",
"required": [
"sub"
],
"properties": {
"campusSession": {
"type": "string"
},
"displayName": {
"type": "string"
},
"eduPersonAffiliation": {
"uniqueItems": true,
"type": "array",
"items": {
"type": "string"
}
},
"email": {
"type": "string"
},
"employeeNumber": {
"type": "string"
},
"preferredLanguage": {
"type": "string"
},
"rat": {
"type": "integer",
"format": "int64"
},
"sub": {
"type": "string"
},
"username": {
"type": "string"
}
}
},
"Oauth2Error": {
"required": [
"error"
],
"properties": {
"error": {
"$ref": "#/components/schemas/Oauth2ErrorObject"
}
}
},
"Oauth2ErrorObject": {
"required": [
"code"
],
"properties": {
"code": {
"type": "integer",
"format": "int32"
},
"status": {
"type": "string"
},
"reason": {
"type": "string"
},
"message": {
"type": "string"
}
}
}
},
"securitySchemes": {
"bearer-key": {
"type": "http",
"scheme": "bearer"
}
}
}
}
Generate Java Client for OpenAPI REST WS (openapi-generator-maven-plugin)
Reference:
OpenAPI Generator > https://github.com/OpenAPITools/openapi-generator
Generators List > https://openapi-generator.tech/docs/generators/