How to fix Springdoc-openapi ignoring @Deprecated on non-primitive fields

This post aims to help you solve an issue with springdoc-openapi not handling the @Deprecated annotation properly.

You might come across a need to document a deprecation in your OpenAPI DTO like this:

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
record TestClass (
@Deprecated InnerClass someField
){
}
record TestClass ( @Deprecated InnerClass someField ){ }
record TestClass (
  @Deprecated InnerClass someField
){
}

Unfortunately, springdoc-openapi (at least in version 2.0.3) does not produce the correct OpenAPI 3.0 description for this. The description that it tries to produce is:

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
schema:
deprecated: true
$ref: '#/components/schemas/InnerClass'
schema: deprecated: true $ref: '#/components/schemas/InnerClass'
schema:
  deprecated: true
  $ref: '#/components/schemas/InnerClass'

As it is documented in the Swagger documentation, the deprecated: true value will be completely ignored. This is further confirmed by the OpenAPI 3.0.0 specification:

This object cannot be extended with additional properties and any properties added SHALL be ignored.

OpenAPI 3.0.0 specification on using $ref

Unfortunately, the maintainer of springdoc-openapi does not believe that this is a bug in his code (and will permanently delete any issue suggesting it, close any PRs fixing it without a comment, and ban you for trying to convince him otherwise), which means you must fix it yourself if you want it to work properly.

Fortunately, the fix is incredibly easy. All you need to do is implement a custom SchemaPropertyDeprecatingConverter:

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
import io.swagger.v3.core.converter.AnnotatedType;
import io.swagger.v3.core.converter.ModelConverter;
import io.swagger.v3.core.converter.ModelConverterContext;
import io.swagger.v3.oas.models.media.Schema;
import org.springdoc.core.converters.SchemaPropertyDeprecatingConverter;
import org.springframework.stereotype.Component;
import java.util.Iterator;
import static java.util.Collections.singletonList;
@Component
public class CustomSchemaPropertyDeprecatingConverter extends SchemaPropertyDeprecatingConverter {
@Override
public Schema<?> resolve(AnnotatedType type, ModelConverterContext context, Iterator<ModelConverter> chain) {
if (chain.hasNext()) {
Schema<?> resolvedSchema = chain.next().resolve(type, context, chain);
if (type.isSchemaProperty() && containsDeprecatedAnnotation(type.getCtxAnnotations())) {
if (resolvedSchema.get$ref() != null) {
// Sibling values alongside $ref are ignored in OpenAPI versions lower than 3.1. See:
// https://swagger.io/docs/specification/using-ref/#sibling
// To add properties to a $ref, it must be wrapped in allOf.
resolvedSchema = wrapInAllOf(resolvedSchema);
}
resolvedSchema.setDeprecated(true);
}
return resolvedSchema;
}
return null;
}
private Schema<?> wrapInAllOf(Schema<?> resolvedSchema) {
Schema<?> wrapperSchema = new Schema<>();
wrapperSchema.allOf(singletonList(resolvedSchema));
return wrapperSchema;
}
}
import io.swagger.v3.core.converter.AnnotatedType; import io.swagger.v3.core.converter.ModelConverter; import io.swagger.v3.core.converter.ModelConverterContext; import io.swagger.v3.oas.models.media.Schema; import org.springdoc.core.converters.SchemaPropertyDeprecatingConverter; import org.springframework.stereotype.Component; import java.util.Iterator; import static java.util.Collections.singletonList; @Component public class CustomSchemaPropertyDeprecatingConverter extends SchemaPropertyDeprecatingConverter { @Override public Schema<?> resolve(AnnotatedType type, ModelConverterContext context, Iterator<ModelConverter> chain) { if (chain.hasNext()) { Schema<?> resolvedSchema = chain.next().resolve(type, context, chain); if (type.isSchemaProperty() && containsDeprecatedAnnotation(type.getCtxAnnotations())) { if (resolvedSchema.get$ref() != null) { // Sibling values alongside $ref are ignored in OpenAPI versions lower than 3.1. See: // https://swagger.io/docs/specification/using-ref/#sibling // To add properties to a $ref, it must be wrapped in allOf. resolvedSchema = wrapInAllOf(resolvedSchema); } resolvedSchema.setDeprecated(true); } return resolvedSchema; } return null; } private Schema<?> wrapInAllOf(Schema<?> resolvedSchema) { Schema<?> wrapperSchema = new Schema<>(); wrapperSchema.allOf(singletonList(resolvedSchema)); return wrapperSchema; } }
import io.swagger.v3.core.converter.AnnotatedType;
import io.swagger.v3.core.converter.ModelConverter;
import io.swagger.v3.core.converter.ModelConverterContext;
import io.swagger.v3.oas.models.media.Schema;
import org.springdoc.core.converters.SchemaPropertyDeprecatingConverter;
import org.springframework.stereotype.Component;

import java.util.Iterator;

import static java.util.Collections.singletonList;

@Component
public class CustomSchemaPropertyDeprecatingConverter extends SchemaPropertyDeprecatingConverter {

    @Override
    public Schema<?> resolve(AnnotatedType type, ModelConverterContext context, Iterator<ModelConverter> chain) {
        if (chain.hasNext()) {
            Schema<?> resolvedSchema = chain.next().resolve(type, context, chain);
            if (type.isSchemaProperty() && containsDeprecatedAnnotation(type.getCtxAnnotations())) {
                if (resolvedSchema.get$ref() != null) {
                    // Sibling values alongside $ref are ignored in OpenAPI versions lower than 3.1. See:
                    // https://swagger.io/docs/specification/using-ref/#sibling
                    // To add properties to a $ref, it must be wrapped in allOf.
                    resolvedSchema = wrapInAllOf(resolvedSchema);
                }
                resolvedSchema.setDeprecated(true);
            }
            return resolvedSchema;
        }
        return null;
    }

    private Schema<?> wrapInAllOf(Schema<?> resolvedSchema) {
        Schema<?> wrapperSchema = new Schema<>();
        wrapperSchema.allOf(singletonList(resolvedSchema));
        return wrapperSchema;
    }
}

Tests prove that it works correctly:

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectWriter;
import com.jayway.jsonpath.JsonPath;
import io.swagger.v3.core.converter.*;
import io.swagger.v3.core.util.Json;
import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.parallel.Execution;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import static java.util.Collections.emptyList;
import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.jupiter.api.Assertions.assertNull;
import static org.junit.jupiter.api.parallel.ExecutionMode.SAME_THREAD;
import static org.mockito.Mockito.mock;
@Execution(SAME_THREAD)
class CustomSchemaPropertyDeprecatingConverterTest {
public static CustomSchemaPropertyDeprecatingConverter CONVERTER;
private static boolean ORIGINAL_ENUMS_AS_REF;
private static ModelConverters CONVERTERS;
private final ObjectWriter objectWriter = Json.mapper().writer().withDefaultPrettyPrinter();
@BeforeAll
static void beforeAll() {
ORIGINAL_ENUMS_AS_REF = io.swagger.v3.core.jackson.ModelResolver.enumsAsRef;
io.swagger.v3.core.jackson.ModelResolver.enumsAsRef = true;
CONVERTER = new CustomSchemaPropertyDeprecatingConverter();
CONVERTERS = new ModelConverters();
CONVERTERS.addConverter(CONVERTER);
}
@AfterAll
static void afterAll() {
io.swagger.v3.core.jackson.ModelResolver.enumsAsRef = ORIGINAL_ENUMS_AS_REF;
}
/**
* This is the behavior of the original SchemaPropertyDeprecatingConverter.
*/
@SuppressWarnings("RedundantOperationOnEmptyContainer")
@Test
void shouldReturnNullIfLastInChain() {
var type = new AnnotatedType();
ModelConverterContext context = mock(ModelConverterContext.class);
List<ModelConverter> modelConverters = emptyList();
Iterator<ModelConverter> chain = modelConverters.iterator();
var result = new CustomSchemaPropertyDeprecatingConverter().resolve(type, context, chain);
assertNull(result);
}
@Test
void shouldFixDeprecationProblem() throws Exception {
ResolvedSchema resolvedSchema = CONVERTERS.resolveAsResolvedSchema(
new AnnotatedType(TestClassWithDeprecations.class)
.resolveAsRef(true)
);
String expectedJson = """
{
"TestClassWithDeprecations" : {
"type" : "object",
"properties" : {
"nonDeprecatedEnum" : {
"$ref" : "#/components/schemas/TestEnum"
},
"deprecatedEnum" : {
"deprecated" : true,
"allOf" : [ {
"$ref" : "#/components/schemas/TestEnum"
} ]
},
"nonDeprecatedClass" : {
"$ref" : "#/components/schemas/TestInnerClass"
},
"deprecatedClass" : {
"deprecated" : true,
"allOf" : [ {
"$ref" : "#/components/schemas/TestInnerClass"
} ]
},
"normalDeprecatedField" : {
"type" : "string",
"deprecated" : true
}
}
},
"TestEnum" : {
"type" : "string",
"enum" : [ "VAL1", "VAL2" ]
},
"TestInnerClass" : {
"type" : "object"
}
}""";
assertComponentSchemaEquals(expectedJson, resolvedSchema.referencedSchemas);
}
@SuppressWarnings("rawtypes")
private void assertComponentSchemaEquals(String expectedJson,
Map<String, io.swagger.v3.oas.models.media.Schema> schema)
throws JsonProcessingException {
String componentSchemas = objectWriter.writeValueAsString(schema);
String expectedComponentSchemas = objectWriter.writeValueAsString(JsonPath.parse(expectedJson).read("$"));
assertThat(componentSchemas).isEqualTo(expectedComponentSchemas);
}
private record TestClassWithDeprecations(
TestEnum nonDeprecatedEnum,
@Deprecated TestEnum deprecatedEnum,
TestInnerClass nonDeprecatedClass,
@Deprecated TestInnerClass deprecatedClass,
@Deprecated String normalDeprecatedField
) {
private enum TestEnum {
VAL1, VAL2
}
}
private record TestInnerClass() {
}
}
import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.ObjectWriter; import com.jayway.jsonpath.JsonPath; import io.swagger.v3.core.converter.*; import io.swagger.v3.core.util.Json; import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.parallel.Execution; import java.util.Iterator; import java.util.List; import java.util.Map; import static java.util.Collections.emptyList; import static org.assertj.core.api.Assertions.assertThat; import static org.junit.jupiter.api.Assertions.assertNull; import static org.junit.jupiter.api.parallel.ExecutionMode.SAME_THREAD; import static org.mockito.Mockito.mock; @Execution(SAME_THREAD) class CustomSchemaPropertyDeprecatingConverterTest { public static CustomSchemaPropertyDeprecatingConverter CONVERTER; private static boolean ORIGINAL_ENUMS_AS_REF; private static ModelConverters CONVERTERS; private final ObjectWriter objectWriter = Json.mapper().writer().withDefaultPrettyPrinter(); @BeforeAll static void beforeAll() { ORIGINAL_ENUMS_AS_REF = io.swagger.v3.core.jackson.ModelResolver.enumsAsRef; io.swagger.v3.core.jackson.ModelResolver.enumsAsRef = true; CONVERTER = new CustomSchemaPropertyDeprecatingConverter(); CONVERTERS = new ModelConverters(); CONVERTERS.addConverter(CONVERTER); } @AfterAll static void afterAll() { io.swagger.v3.core.jackson.ModelResolver.enumsAsRef = ORIGINAL_ENUMS_AS_REF; } /** * This is the behavior of the original SchemaPropertyDeprecatingConverter. */ @SuppressWarnings("RedundantOperationOnEmptyContainer") @Test void shouldReturnNullIfLastInChain() { var type = new AnnotatedType(); ModelConverterContext context = mock(ModelConverterContext.class); List<ModelConverter> modelConverters = emptyList(); Iterator<ModelConverter> chain = modelConverters.iterator(); var result = new CustomSchemaPropertyDeprecatingConverter().resolve(type, context, chain); assertNull(result); } @Test void shouldFixDeprecationProblem() throws Exception { ResolvedSchema resolvedSchema = CONVERTERS.resolveAsResolvedSchema( new AnnotatedType(TestClassWithDeprecations.class) .resolveAsRef(true) ); String expectedJson = """ { "TestClassWithDeprecations" : { "type" : "object", "properties" : { "nonDeprecatedEnum" : { "$ref" : "#/components/schemas/TestEnum" }, "deprecatedEnum" : { "deprecated" : true, "allOf" : [ { "$ref" : "#/components/schemas/TestEnum" } ] }, "nonDeprecatedClass" : { "$ref" : "#/components/schemas/TestInnerClass" }, "deprecatedClass" : { "deprecated" : true, "allOf" : [ { "$ref" : "#/components/schemas/TestInnerClass" } ] }, "normalDeprecatedField" : { "type" : "string", "deprecated" : true } } }, "TestEnum" : { "type" : "string", "enum" : [ "VAL1", "VAL2" ] }, "TestInnerClass" : { "type" : "object" } }"""; assertComponentSchemaEquals(expectedJson, resolvedSchema.referencedSchemas); } @SuppressWarnings("rawtypes") private void assertComponentSchemaEquals(String expectedJson, Map<String, io.swagger.v3.oas.models.media.Schema> schema) throws JsonProcessingException { String componentSchemas = objectWriter.writeValueAsString(schema); String expectedComponentSchemas = objectWriter.writeValueAsString(JsonPath.parse(expectedJson).read("$")); assertThat(componentSchemas).isEqualTo(expectedComponentSchemas); } private record TestClassWithDeprecations( TestEnum nonDeprecatedEnum, @Deprecated TestEnum deprecatedEnum, TestInnerClass nonDeprecatedClass, @Deprecated TestInnerClass deprecatedClass, @Deprecated String normalDeprecatedField ) { private enum TestEnum { VAL1, VAL2 } } private record TestInnerClass() { } }
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectWriter;
import com.jayway.jsonpath.JsonPath;
import io.swagger.v3.core.converter.*;
import io.swagger.v3.core.util.Json;
import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.parallel.Execution;

import java.util.Iterator;
import java.util.List;
import java.util.Map;

import static java.util.Collections.emptyList;
import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.jupiter.api.Assertions.assertNull;
import static org.junit.jupiter.api.parallel.ExecutionMode.SAME_THREAD;
import static org.mockito.Mockito.mock;

@Execution(SAME_THREAD)
class CustomSchemaPropertyDeprecatingConverterTest {

    public static CustomSchemaPropertyDeprecatingConverter CONVERTER;
    private static boolean ORIGINAL_ENUMS_AS_REF;
    private static ModelConverters CONVERTERS;
    private final ObjectWriter objectWriter = Json.mapper().writer().withDefaultPrettyPrinter();

    @BeforeAll
    static void beforeAll() {
        ORIGINAL_ENUMS_AS_REF = io.swagger.v3.core.jackson.ModelResolver.enumsAsRef;
        io.swagger.v3.core.jackson.ModelResolver.enumsAsRef = true;

        CONVERTER = new CustomSchemaPropertyDeprecatingConverter();
        CONVERTERS = new ModelConverters();
        CONVERTERS.addConverter(CONVERTER);
    }

    @AfterAll
    static void afterAll() {
        io.swagger.v3.core.jackson.ModelResolver.enumsAsRef = ORIGINAL_ENUMS_AS_REF;
    }

    /**
     * This is the behavior of the original SchemaPropertyDeprecatingConverter.
     */
    @SuppressWarnings("RedundantOperationOnEmptyContainer")
    @Test
    void shouldReturnNullIfLastInChain() {
        var type = new AnnotatedType();
        ModelConverterContext context = mock(ModelConverterContext.class);
        List<ModelConverter> modelConverters = emptyList();
        Iterator<ModelConverter> chain = modelConverters.iterator();

        var result = new CustomSchemaPropertyDeprecatingConverter().resolve(type, context, chain);

        assertNull(result);
    }

    @Test
    void shouldFixDeprecationProblem() throws Exception {
        ResolvedSchema resolvedSchema = CONVERTERS.resolveAsResolvedSchema(
                new AnnotatedType(TestClassWithDeprecations.class)
                        .resolveAsRef(true)
        );

        String expectedJson = """
                {
                   "TestClassWithDeprecations" : {
                     "type" : "object",
                     "properties" : {
                       "nonDeprecatedEnum" : {
                         "$ref" : "#/components/schemas/TestEnum"
                       },
                       "deprecatedEnum" : {
                         "deprecated" : true,
                         "allOf" : [ {
                           "$ref" : "#/components/schemas/TestEnum"
                         } ]
                       },
                       "nonDeprecatedClass" : {
                         "$ref" : "#/components/schemas/TestInnerClass"
                       },
                       "deprecatedClass" : {
                         "deprecated" : true,
                         "allOf" : [ {
                           "$ref" : "#/components/schemas/TestInnerClass"
                         } ]
                       },
                       "normalDeprecatedField" : {
                         "type" : "string",
                         "deprecated" : true
                       }
                     }
                   },
                   "TestEnum" : {
                     "type" : "string",
                     "enum" : [ "VAL1", "VAL2" ]
                   },
                   "TestInnerClass" : {
                     "type" : "object"
                   }
                 }""";
        assertComponentSchemaEquals(expectedJson, resolvedSchema.referencedSchemas);
    }

    @SuppressWarnings("rawtypes")
    private void assertComponentSchemaEquals(String expectedJson,
                                             Map<String, io.swagger.v3.oas.models.media.Schema> schema)
            throws JsonProcessingException {
        String componentSchemas = objectWriter.writeValueAsString(schema);
        String expectedComponentSchemas = objectWriter.writeValueAsString(JsonPath.parse(expectedJson).read("$"));
        assertThat(componentSchemas).isEqualTo(expectedComponentSchemas);
    }

    private record TestClassWithDeprecations(
            TestEnum nonDeprecatedEnum,
            @Deprecated TestEnum deprecatedEnum,
            TestInnerClass nonDeprecatedClass,
            @Deprecated TestInnerClass deprecatedClass,
            @Deprecated String normalDeprecatedField
    ) {

        private enum TestEnum {
            VAL1, VAL2
        }
    }

    private record TestInnerClass() {
    }
}
Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.ObjectWriter;
import com.jayway.jsonpath.JsonPath;
import io.swagger.v3.oas.annotations.media.Schema;
import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.parallel.Execution;
import org.springdoc.core.configuration.SpringDocConfiguration;
import org.springdoc.core.converters.SchemaPropertyDeprecatingConverter;
import org.springdoc.core.properties.SpringDocConfigProperties;
import org.springdoc.webmvc.core.configuration.SpringDocWebMvcConfiguration;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.web.servlet.WebMvcAutoConfiguration;
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.MediaType;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.MvcResult;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import java.io.UnsupportedEncodingException;
import static org.junit.jupiter.api.parallel.ExecutionMode.SAME_THREAD;
import static org.assertj.core.api.Assertions.assertThat;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
@Execution(SAME_THREAD)
@SpringBootTest
@ContextConfiguration(classes = {
CustomSchemaPropertyDeprecatingConverterTest.TestConfiguration.class,
CustomSchemaPropertyDeprecatingConverterTest.ControllerWithDeprecation.class,
SpringDocConfiguration.class, SpringDocWebMvcConfiguration.class,
WebMvcAutoConfiguration.class
})
@AutoConfigureMockMvc
class CustomSchemaPropertyDeprecatingConverterIT {
private static boolean originalEnumsAsRef;
private final ObjectWriter objectWriter = new ObjectMapper().writerWithDefaultPrettyPrinter();
@Autowired
private MockMvc mockMvc;
@BeforeAll
static void beforeAll() {
originalEnumsAsRef = io.swagger.v3.core.jackson.ModelResolver.enumsAsRef;
io.swagger.v3.core.jackson.ModelResolver.enumsAsRef = true;
}
@AfterAll
static void afterAll() {
io.swagger.v3.core.jackson.ModelResolver.enumsAsRef = originalEnumsAsRef;
}
@Test
void shouldFixDeprecationProblem() throws Exception {
String expectedJson = """
{
"TestClassWithDeprecations" : {
"type" : "object",
"properties" : {
"nonDeprecatedEnum" : {
"$ref" : "#/components/schemas/TestEnum"
},
"deprecatedEnum" : {
"deprecated" : true,
"allOf" : [ {
"$ref" : "#/components/schemas/TestEnum"
} ]
},
"nonDeprecatedClass" : {
"$ref" : "#/components/schemas/TestInnerClass"
},
"deprecatedClass" : {
"deprecated" : true,
"allOf" : [ {
"$ref" : "#/components/schemas/TestInnerClass"
} ]
}
}
},
"TestEnum" : {
"type" : "string",
"enum" : [ "VAL1", "VAL2" ]
},
"TestInnerClass" : {
"type" : "object"
}
}""";
mockMvc.perform(get("/v3/api-docs").accept(MediaType.APPLICATION_JSON))
.andDo(mvcResult -> assertComponentSchemaEquals(expectedJson, mvcResult));
}
private void assertComponentSchemaEquals(String
expectedJson, MvcResult mvcResult)
throws UnsupportedEncodingException, JsonProcessingException {
String json = mvcResult.getResponse().getContentAsString();
String componentSchemas = objectWriter.writeValueAsString(
JsonPath.parse(json).read("$.components.schemas"));
String expectedComponentSchemas = objectWriter.writeValueAsString(JsonPath.parse(expectedJson).read("$"));
assertThat(componentSchemas).isEqualTo(expectedComponentSchemas);
}
private enum TestEnum {
VAL1, VAL2
}
@Configuration
public static class TestConfiguration {
@Bean
public SpringDocConfigProperties springDocConfigProperties() {
return new SpringDocConfigProperties();
}
@Bean
public SchemaPropertyDeprecatingConverter schemaPropertyDeprecatingConverter() {
return new CustomSchemaPropertyDeprecatingConverter();
}
}
@RestController
public static class ControllerWithDeprecation {
@GetMapping
public TestClassWithDeprecations testEndpoint() {
return null;
}
}
private record TestClassWithDeprecations(
TestEnum nonDeprecatedEnum,
@Deprecated TestEnum deprecatedEnum,
TestInnerClass nonDeprecatedClass,
@Deprecated TestInnerClass deprecatedClass
) {
}
private record TestInnerClass() {
}
}
import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.ObjectWriter; import com.jayway.jsonpath.JsonPath; import io.swagger.v3.oas.annotations.media.Schema; import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.parallel.Execution; import org.springdoc.core.configuration.SpringDocConfiguration; import org.springdoc.core.converters.SchemaPropertyDeprecatingConverter; import org.springdoc.core.properties.SpringDocConfigProperties; import org.springdoc.webmvc.core.configuration.SpringDocWebMvcConfiguration; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.autoconfigure.web.servlet.WebMvcAutoConfiguration; import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.http.MediaType; import org.springframework.test.context.ContextConfiguration; import org.springframework.test.web.servlet.MockMvc; import org.springframework.test.web.servlet.MvcResult; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RestController; import java.io.UnsupportedEncodingException; import static org.junit.jupiter.api.parallel.ExecutionMode.SAME_THREAD; import static org.assertj.core.api.Assertions.assertThat; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; @Execution(SAME_THREAD) @SpringBootTest @ContextConfiguration(classes = { CustomSchemaPropertyDeprecatingConverterTest.TestConfiguration.class, CustomSchemaPropertyDeprecatingConverterTest.ControllerWithDeprecation.class, SpringDocConfiguration.class, SpringDocWebMvcConfiguration.class, WebMvcAutoConfiguration.class }) @AutoConfigureMockMvc class CustomSchemaPropertyDeprecatingConverterIT { private static boolean originalEnumsAsRef; private final ObjectWriter objectWriter = new ObjectMapper().writerWithDefaultPrettyPrinter(); @Autowired private MockMvc mockMvc; @BeforeAll static void beforeAll() { originalEnumsAsRef = io.swagger.v3.core.jackson.ModelResolver.enumsAsRef; io.swagger.v3.core.jackson.ModelResolver.enumsAsRef = true; } @AfterAll static void afterAll() { io.swagger.v3.core.jackson.ModelResolver.enumsAsRef = originalEnumsAsRef; } @Test void shouldFixDeprecationProblem() throws Exception { String expectedJson = """ { "TestClassWithDeprecations" : { "type" : "object", "properties" : { "nonDeprecatedEnum" : { "$ref" : "#/components/schemas/TestEnum" }, "deprecatedEnum" : { "deprecated" : true, "allOf" : [ { "$ref" : "#/components/schemas/TestEnum" } ] }, "nonDeprecatedClass" : { "$ref" : "#/components/schemas/TestInnerClass" }, "deprecatedClass" : { "deprecated" : true, "allOf" : [ { "$ref" : "#/components/schemas/TestInnerClass" } ] } } }, "TestEnum" : { "type" : "string", "enum" : [ "VAL1", "VAL2" ] }, "TestInnerClass" : { "type" : "object" } }"""; mockMvc.perform(get("/v3/api-docs").accept(MediaType.APPLICATION_JSON)) .andDo(mvcResult -> assertComponentSchemaEquals(expectedJson, mvcResult)); } private void assertComponentSchemaEquals(String expectedJson, MvcResult mvcResult) throws UnsupportedEncodingException, JsonProcessingException { String json = mvcResult.getResponse().getContentAsString(); String componentSchemas = objectWriter.writeValueAsString( JsonPath.parse(json).read("$.components.schemas")); String expectedComponentSchemas = objectWriter.writeValueAsString(JsonPath.parse(expectedJson).read("$")); assertThat(componentSchemas).isEqualTo(expectedComponentSchemas); } private enum TestEnum { VAL1, VAL2 } @Configuration public static class TestConfiguration { @Bean public SpringDocConfigProperties springDocConfigProperties() { return new SpringDocConfigProperties(); } @Bean public SchemaPropertyDeprecatingConverter schemaPropertyDeprecatingConverter() { return new CustomSchemaPropertyDeprecatingConverter(); } } @RestController public static class ControllerWithDeprecation { @GetMapping public TestClassWithDeprecations testEndpoint() { return null; } } private record TestClassWithDeprecations( TestEnum nonDeprecatedEnum, @Deprecated TestEnum deprecatedEnum, TestInnerClass nonDeprecatedClass, @Deprecated TestInnerClass deprecatedClass ) { } private record TestInnerClass() { } }
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.ObjectWriter;
import com.jayway.jsonpath.JsonPath;
import io.swagger.v3.oas.annotations.media.Schema;
import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.parallel.Execution;
import org.springdoc.core.configuration.SpringDocConfiguration;
import org.springdoc.core.converters.SchemaPropertyDeprecatingConverter;
import org.springdoc.core.properties.SpringDocConfigProperties;
import org.springdoc.webmvc.core.configuration.SpringDocWebMvcConfiguration;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.web.servlet.WebMvcAutoConfiguration;
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.MediaType;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.MvcResult;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

import java.io.UnsupportedEncodingException;

import static org.junit.jupiter.api.parallel.ExecutionMode.SAME_THREAD;
import static org.assertj.core.api.Assertions.assertThat;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;

@Execution(SAME_THREAD)
@SpringBootTest
@ContextConfiguration(classes = {
        CustomSchemaPropertyDeprecatingConverterTest.TestConfiguration.class,
        CustomSchemaPropertyDeprecatingConverterTest.ControllerWithDeprecation.class,
        SpringDocConfiguration.class, SpringDocWebMvcConfiguration.class,
        WebMvcAutoConfiguration.class
})
@AutoConfigureMockMvc
class CustomSchemaPropertyDeprecatingConverterIT {

    private static boolean originalEnumsAsRef;
    private final ObjectWriter objectWriter = new ObjectMapper().writerWithDefaultPrettyPrinter();

    @Autowired
    private MockMvc mockMvc;

    @BeforeAll
    static void beforeAll() {
        originalEnumsAsRef = io.swagger.v3.core.jackson.ModelResolver.enumsAsRef;
        io.swagger.v3.core.jackson.ModelResolver.enumsAsRef = true;
    }

    @AfterAll
    static void afterAll() {
        io.swagger.v3.core.jackson.ModelResolver.enumsAsRef = originalEnumsAsRef;
    }

    @Test
    void shouldFixDeprecationProblem() throws Exception {
        String expectedJson = """
                {
                   "TestClassWithDeprecations" : {
                     "type" : "object",
                     "properties" : {
                       "nonDeprecatedEnum" : {
                         "$ref" : "#/components/schemas/TestEnum"
                       },
                       "deprecatedEnum" : {
                         "deprecated" : true,
                         "allOf" : [ {
                           "$ref" : "#/components/schemas/TestEnum"
                         } ]
                       },
                       "nonDeprecatedClass" : {
                         "$ref" : "#/components/schemas/TestInnerClass"
                       },
                       "deprecatedClass" : {
                         "deprecated" : true,
                         "allOf" : [ {
                           "$ref" : "#/components/schemas/TestInnerClass"
                         } ]
                       }
                     }
                   },
                   "TestEnum" : {
                     "type" : "string",
                     "enum" : [ "VAL1", "VAL2" ]
                   },
                   "TestInnerClass" : {
                     "type" : "object"
                   }
                 }""";
        mockMvc.perform(get("/v3/api-docs").accept(MediaType.APPLICATION_JSON))
                .andDo(mvcResult -> assertComponentSchemaEquals(expectedJson, mvcResult));
    }

    private void assertComponentSchemaEquals(String
expectedJson, MvcResult mvcResult)
            throws UnsupportedEncodingException, JsonProcessingException {
        String json = mvcResult.getResponse().getContentAsString();
        String componentSchemas = objectWriter.writeValueAsString(
                JsonPath.parse(json).read("$.components.schemas"));
        String expectedComponentSchemas = objectWriter.writeValueAsString(JsonPath.parse(expectedJson).read("$"));

        assertThat(componentSchemas).isEqualTo(expectedComponentSchemas);
    }

    private enum TestEnum {
        VAL1, VAL2
    }

    @Configuration
    public static class TestConfiguration {

        @Bean
        public SpringDocConfigProperties springDocConfigProperties() {
            return new SpringDocConfigProperties();
        }

        @Bean
        public SchemaPropertyDeprecatingConverter schemaPropertyDeprecatingConverter() {
            return new CustomSchemaPropertyDeprecatingConverter();
        }
    }

    @RestController
    public static class ControllerWithDeprecation {

        @GetMapping
        public TestClassWithDeprecations testEndpoint() {

            return null;
        }
    }

    private record TestClassWithDeprecations(
            TestEnum nonDeprecatedEnum,
            @Deprecated TestEnum deprecatedEnum,
            TestInnerClass nonDeprecatedClass,
            @Deprecated TestInnerClass deprecatedClass
    ) {
    }

    private record TestInnerClass() {
    }
}

The custom component wraps any deprecated $ref in an allOf, creating a valid OpenAPI description, e.g.,:

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
schema:
deprecated: true
allOf:
- $ref: '#/components/schemas/InnerClass'
schema: deprecated: true allOf: - $ref: '#/components/schemas/InnerClass'
schema:
  deprecated: true
  allOf:
    - $ref: '#/components/schemas/InnerClass'

This should work without a problem with every client code generator, enabling you to easily mark non-primitive fields as @Deprecated. It will also work properly with enums, when io.swagger.v3.core.jackson.ModelResolver.enumsAsRef is true.

Daniel Frąk Written by:

Be First to Comment

Leave a Reply

Your email address will not be published. Required fields are marked *