4

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"
  ]
}

Reproducible Example

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

M. Justin
  • 14,487
  • 7
  • 91
  • 130
  • 1
    There is an option with [JsonProperty](https://www.baeldung.com/jackson-serialize-enums#3-using-jsonproperty), [JsonCreator](https://www.baeldung.com/jackson-serialize-enums#4-using-jsoncreator) and overriding [`toString()`](https://stackoverflow.com/questions/62677965/using-schemaallowablevalues-for-enum-param-using-openapi-in-spring-boot). Can you check whether these work pls? – aksappy Aug 11 '21 at 18:23
  • `JsonProperty` and (as alluded to in the question) `toString()` work. No such luck with `JsonCreator` (though given that creates an enum from a value and doesn't declaratively specify a mapping, I don't think there's really any reasonable way it _could_ work). – M. Justin Aug 11 '21 at 18:46
  • 1
    @aksappy Thanks; I've expanded the `JsonProperty` suggestion into an answer. – M. Justin Aug 11 '21 at 18:49

3 Answers3

4

This was due to bug #3998 in versions of Swagger Core prior to 2.2.5. In those versions of the library, @JsonValue is handled properly when on a public method, but not when on a field. Upgrading to version 2.2.5 of Swagger Core or later will cause the example to work as desired without modification.

Alternatively, for versions of Swagger Core before 2.2.5, adding a public accessor method will have the desired effect:

public enum Suit {
    HEARTS("Hearts"), DIAMONDS("Diamonds"), CLUBS("Clubs"), SPADES("Spades");

    private final String jsonValue;

    Suit(String jsonValue) { this.jsonValue = jsonValue; }

    @JsonValue
    public String getJsonValue() {
        return jsonValue;
    }
}
M. Justin
  • 14,487
  • 7
  • 91
  • 130
2

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.

Type-specific customizer with explicit list

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);
    }
}

Global enum customizer using @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();
    }
}
M. Justin
  • 14,487
  • 7
  • 91
  • 130
0

One solution is to replace the @JsonValue implementation with @JsonProperty:

public enum Suit {
    @JsonProperty("Hearts") HEARTS,
    @JsonProperty("Diamonds") DIAMONDS,
    @JsonProperty("Clubs") CLUBS,
    @JsonProperty("Spades") SPADES;
}

Note that this does result in some duplication if the value is programmatically needed, as it will need to be specified both in @JsonProperty as well as a value on the enum.

M. Justin
  • 14,487
  • 7
  • 91
  • 130