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:


JSON serialization & deserialzation (see also)

https://sites.google.com/site/pawneecity/sprint-boot/json-serialization-spring-boot


Reference

SpringDoc OpenAPI (full documentation)

https://springdoc.org/

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:


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.

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.


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:

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

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: