swagger-ui
swagger-ui copied to clipboard
Authorize button doesn't work if OpenAPI's securityScheme.type is upper-case
Finally! I pinned it down! It took me a while. Here's an MRE:
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.6.4</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.example</groupId>
<artifactId>swagger-ui-mre</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>swagger-ui-mre</name>
<description>swagger-ui-mre</description>
<properties>
<java.version>17</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springdoc</groupId>
<artifactId>springdoc-openapi-ui</artifactId>
<version>1.7.0</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<excludes>
<exclude>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</exclude>
</excludes>
</configuration>
</plugin>
</plugins>
</build>
</project>
server:
port: 8100
springdoc:
swagger-ui:
path: /swagger-ui
package com.example.swaggeruimre;
import io.swagger.v3.oas.annotations.OpenAPIDefinition;
import io.swagger.v3.oas.annotations.info.Info;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
@OpenAPIDefinition(info = @Info(version = "1.0", title = "Hello API"))
public class SwaggerUiMreApplication {
public static void main(String[] args) {
SpringApplication.run(SwaggerUiMreApplication.class, args);
}
}
package com.example.swaggeruimre;
import io.swagger.v3.oas.annotations.enums.SecuritySchemeType;
import io.swagger.v3.oas.annotations.security.SecurityScheme;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.Getter;
import lombok.NoArgsConstructor;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
@Tag(name = "Hello Controller")
@RestController
@SecurityScheme(type = SecuritySchemeType.HTTP, name = "bearer-key",
description = "authorization with JWT token", scheme = "bearer",
bearerFormat = "JWT")
public class HelloController {
@GetMapping("/hello")
public HelloMessage getHello() {
return new HelloMessage();
}
@Getter
@NoArgsConstructor
public static class HelloMessage {
private final String message = "Hello!";
}
}
The Authorize
button works!
Let's copy the OpenAPI JSON. It's a good JSON, one that produces a button
Now, let's imagine that we receive that JSON from elsewhere. For simplicity, the source and the recipient are going to be the same
server:
port: 8100
springdoc:
swagger-ui:
path: /swagger-ui
url: /open-api
package com.example.swaggeruimre;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class OpenApiController {
@GetMapping("/open-api")
public String getOpenApi() {
return """
{
"openapi": "3.0.1",
"info": {
"title": "Hello API",
"version": "1.0"
},
"servers": [
{
"url": "http://localhost:8100",
"description": "Generated server url"
}
],
"paths": {
"/hello": {
"get": {
"tags": [
"Hello Controller"
],
"operationId": "getHello",
"responses": {
"200": {
"description": "OK",
"content": {
"*/*": {
"schema": {
"$ref": "#/components/schemas/HelloMessage"
}
}
}
}
}
}
}
},
"components": {
"schemas": {
"HelloMessage": {
"type": "object",
"properties": {
"message": {
"type": "string"
}
}
}
},
"securitySchemes": {
"bearer-key": {
"type": "http",
"description": "authorization with JWT token",
"scheme": "bearer",
"bearerFormat": "JWT"
}
}
}
}
""";
}
}
Still works!
Now, let's change the JSON we return and make it a bad JSON. I already know the trick. Replace "type": "http"
with "type": "HTTP"
. The result:
Now the button doesn't work!
You may be wondering, "Why would you want to turn a good JSON into a bad JSON?" To simulate what OpenApiV3Parser
does!
Suppose I return not a string, but an object:
<!-- you need to add this -->
<dependency>
<groupId>io.swagger.parser.v3</groupId>
<artifactId>swagger-parser</artifactId>
<version>2.1.18</version>
</dependency>
package com.example.swaggeruimre;
import io.swagger.v3.oas.models.OpenAPI;
import io.swagger.v3.parser.OpenAPIV3Parser;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class OpenApiController {
@GetMapping("/open-api")
public OpenAPI getOpenApi() {
return new OpenAPIV3Parser().readContents("""
{
"openapi": "3.0.1",
"info": {
"title": "Hello API",
"version": "1.0"
},
"servers": [
{
"url": "http://localhost:8100",
"description": "Generated server url"
}
],
"paths": {
"/hello": {
"get": {
"tags": [
"Hello Controller"
],
"operationId": "getHello",
"responses": {
"200": {
"description": "OK",
"content": {
"*/*": {
"schema": {
"$ref": "#/components/schemas/HelloMessage"
}
}
}
}
}
}
}
},
"components": {
"schemas": {
"HelloMessage": {
"type": "object",
"properties": {
"message": {
"type": "string"
}
}
}
},
"securitySchemes": {
"bearer-key": {
"type": "http",
"description": "authorization with JWT token",
"scheme": "bearer",
"bearerFormat": "JWT"
}
}
}
}
""").getOpenAPI();
}
}
Let's see
Maybe we just need to tell our ObjectMapper
to ignore nulls?
package com.example.swaggeruimre;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class MyConfig {
@Bean
public ObjectMapper objectMapper() {
ObjectMapper objectMapper = new ObjectMapper();
objectMapper.setSerializationInclusion(JsonInclude.Include.NON_NULL);
return objectMapper;
}
}
Nope! It's precisely because once we return objects instead of strings, those nested enum properties, e.g. SecurityScheme.Type
, are serialized into upper-case strings which Swagger UI can't handle!
Considering you override toString()
for each of your enums to return lower-case values
// like so
public class SecurityScheme {
/**
* Gets or Sets type
*/
public enum Type {
APIKEY("apiKey"),
HTTP("http"),
OAUTH2("oauth2"),
OPENIDCONNECT("openIdConnect"),
MUTUALTLS("mutualTLS");
private String value;
Type(String value) {
this.value = value;
}
@Override
public String toString() {
return String.valueOf(value);
}
}
I imagine you had a problem with this before
I hate to break it to you, but it's not solved completely!
To recap:
- If we return an OpenAPI object (not a string),
ObjectMapper
serializes it in a way that results in upper-case enum values. YourtoString()
s are not called during serialization! - When Swagger UI's underlying OpenAPI has upper-case enum values, specifically
SecurityScheme.Type
, theAuthorize
button doesn't work!
I see these approaches:
- Make Swagger UI stomach upper-case (good)
- Tell Jackson to serialize your enums to lower-case strings (a kludge)
// here's one way to do it
public class SecurityScheme {
/**
* Gets or Sets type
*/
public enum Type {
APIKEY("apiKey"),
HTTP("http"),
OAUTH2("oauth2"),
OPENIDCONNECT("openIdConnect"),
MUTUALTLS("mutualTLS");
private String value;
Type(String value) {
this.value = value;
}
@Override
@JsonValue
public String toString() {
return String.valueOf(value);
}
}
I could fork and do the latter, but I believe it's better to do the former (I don't know how)