I have a Spring Boot application using springdoc-openapi to generate Swagger API documentation for my controllers. One of the enums used in the JSON request/response has a different JSON representation than its value
/toString()
. This is achieved using the Jackson @JsonValue
annotation:
public enum Suit {
HEARTS("Hearts"), DIAMONDS("Diamonds"), CLUBS("Clubs"), SPADES("Spades");
@JsonValue
private final String jsonValue;
Suit(String jsonValue) { this.jsonValue = jsonValue; }
}
However, the generated Swagger API docs use the enum value (specifically, the value of toString()
) rather than the JSON representation (per @JsonValue
) when listing the enum values:
{
"openapi": "3.0.1",
"info": { "title": "OpenAPI definition", "version": "v0" },
"servers": [
{ "url": "http://localhost:8080", "description": "Generated server url" }
],
"paths": { ... },
"components": {
"schemas": {
"PlayingCard": {
"type": "object",
"properties": {
"suit": {
"type": "string",
"enum": [ "Hearts", "Diamonds", "Clubs", "Spades" ]
},
"value": { "type": "integer", "format": "int32" }
}
}
}
}
}
There is closed issue #1101 in the springdoc-openapi project which requests allowing @JsonValue
to affect the enum serialization. However, that issue was closed since no PR was submitted for it.
How can I get the enum list to match the actual JSON type accepted/returned by the REST endpoint, and not the toString()
values?
My first thought to solving this issue was to use the @Schema(allowableValues = {...}]
annotation from Swagger Core. However, whether by bug or by design, this adds to the list of values, rather than replacing it:
@Schema(allowableValues = {"Hearts", "Diamonds", "Clubs", "Spades"})
public enum Suit {
HEARTS("Hearts"), DIAMONDS("Diamonds"), CLUBS("Clubs"), SPADES("Spades");
// ...
}
"suit": {
"type": "string",
"enum": [
"HEARTS",
"DIAMONDS",
"CLUBS",
"SPADES",
"Hearts",
"Diamonds",
"Clubs",
"Spades"
]
}
plugins {
id 'org.springframework.boot' version '2.5.3'
id 'io.spring.dependency-management' version '1.0.11.RELEASE'
id 'java'
}
sourceCompatibility = '11'
repositories {
mavenCentral()
}
dependencies {
implementation 'io.swagger.core.v3:swagger-annotations:2.1.10'
implementation 'org.springdoc:springdoc-openapi-ui:1.5.10'
implementation 'org.springframework.boot:spring-boot-starter-web'
}
package com.example.springdoc;
import com.fasterxml.jackson.annotation.JsonValue;
public class PlayingCard {
private Suit suit;
private Integer value;
public Suit getSuit() { return suit; }
public void setSuit(Suit suit) { this.suit = suit; }
public Integer getValue() { return value; }
public void setValue(Integer value) { this.value = value; }
public enum Suit {
HEARTS("Hearts"), DIAMONDS("Diamonds"), CLUBS("Clubs"), SPADES("Spades");
@JsonValue
private final String jsonValue;
Suit(String jsonValue) { this.jsonValue = jsonValue; }
}
}
package com.example.springdoc;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping("/playingCard")
public class PlayingCardController {
@PostMapping
public PlayingCard echo(@RequestBody PlayingCard card) {
return card;
}
}
Swagger URL: http://localhost:8080/v3/api-docs
A PropertyCustomizer
Spring bean can be created to customize the property. This can be done either for the specific enum type, or globally for all enums.
The following customizer will explicitly set the enum values for the specific enum type:
import com.fasterxml.jackson.databind.JavaType;
import io.swagger.v3.core.converter.AnnotatedType;
import io.swagger.v3.oas.models.media.Schema;
import io.swagger.v3.oas.models.media.StringSchema;
import org.springdoc.core.customizers.PropertyCustomizer;
import org.springframework.stereotype.Component;
import java.util.List;
@Component
public class SuitPropertyCustomizer implements PropertyCustomizer {
@Override
public Schema customize(Schema property, AnnotatedType type) {
if (property instanceof StringSchema && isSuit(type)) {
property.setEnum(List.of("Hearts", "Diamonds", "Clubs", "Spades"));
}
return property;
}
private boolean isSuit(AnnotatedType type) {
return type.getType() instanceof JavaType t
&& t.isTypeOrSubTypeOf(Suit.class);
}
}
@JsonValue
The following customizer will use the Jackson String representation for all enum types, meaning the @JsonValue
annotation will be used where applicable.
import com.fasterxml.jackson.databind.JavaType;
import com.fasterxml.jackson.databind.ObjectMapper;
import io.swagger.v3.core.converter.AnnotatedType;
import io.swagger.v3.core.util.Json;
import io.swagger.v3.oas.models.media.Schema;
import io.swagger.v3.oas.models.media.StringSchema;
import org.springdoc.core.customizers.PropertyCustomizer;
import org.springframework.stereotype.Component;
import java.util.Arrays;
import java.util.stream.Collectors;
@Component
public class EnumValuePropertyCustomizer implements PropertyCustomizer {
@Override
public Schema customize(Schema property, AnnotatedType type) {
if (property instanceof StringSchema && isEnumType(type)) {
ObjectMapper objectMapper = Json.mapper();
property.setEnum(Arrays.stream(((JavaType) type.getType()).getRawClass().getEnumConstants())
.map(e -> objectMapper.convertValue(e, String.class))
.collect(Collectors.toList()));
}
return property;
}
private boolean isEnumType(AnnotatedType type) {
return type.getType() instanceof JavaType t && t.getType().isEnumType();
}
}
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With