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:
@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:
$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
:
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;
public class CustomSchemaPropertyDeprecatingConverter extends SchemaPropertyDeprecatingConverter {
public Schema<?> resolve(AnnotatedType type, ModelConverterContext context, Iterator<ModelConverter> chain) {
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);
private Schema<?> wrapInAllOf(Schema<?> resolvedSchema) {
Schema<?> wrapperSchema = new Schema<>();
wrapperSchema.allOf(singletonList(resolvedSchema));
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:
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 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;
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();
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);
io.swagger.v3.core.jackson.ModelResolver.enumsAsRef = ORIGINAL_ENUMS_AS_REF;
* This is the behavior of the original SchemaPropertyDeprecatingConverter.
@SuppressWarnings("RedundantOperationOnEmptyContainer")
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);
void shouldFixDeprecationProblem() throws Exception {
ResolvedSchema resolvedSchema = CONVERTERS.resolveAsResolvedSchema(
new AnnotatedType(TestClassWithDeprecations.class)
String expectedJson = """
"TestClassWithDeprecations" : {
"$ref" : "#/components/schemas/TestEnum"
"$ref" : "#/components/schemas/TestEnum"
"$ref" : "#/components/schemas/TestInnerClass"
"$ref" : "#/components/schemas/TestInnerClass"
"normalDeprecatedField" : {
"enum" : [ "VAL1", "VAL2" ]
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 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() {
}
}
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;
@ContextConfiguration(classes = {
CustomSchemaPropertyDeprecatingConverterTest.TestConfiguration.class,
CustomSchemaPropertyDeprecatingConverterTest.ControllerWithDeprecation.class,
SpringDocConfiguration.class, SpringDocWebMvcConfiguration.class,
WebMvcAutoConfiguration.class
class CustomSchemaPropertyDeprecatingConverterIT {
private static boolean originalEnumsAsRef;
private final ObjectWriter objectWriter = new ObjectMapper().writerWithDefaultPrettyPrinter();
static void beforeAll() {
originalEnumsAsRef = io.swagger.v3.core.jackson.ModelResolver.enumsAsRef;
io.swagger.v3.core.jackson.ModelResolver.enumsAsRef = true;
io.swagger.v3.core.jackson.ModelResolver.enumsAsRef = originalEnumsAsRef;
void shouldFixDeprecationProblem() throws Exception {
String expectedJson = """
"TestClassWithDeprecations" : {
"$ref" : "#/components/schemas/TestEnum"
"$ref" : "#/components/schemas/TestEnum"
"$ref" : "#/components/schemas/TestInnerClass"
"$ref" : "#/components/schemas/TestInnerClass"
"enum" : [ "VAL1", "VAL2" ]
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);
public static class TestConfiguration {
public SpringDocConfigProperties springDocConfigProperties() {
return new SpringDocConfigProperties();
public SchemaPropertyDeprecatingConverter schemaPropertyDeprecatingConverter() {
return new CustomSchemaPropertyDeprecatingConverter();
public static class ControllerWithDeprecation {
public TestClassWithDeprecations testEndpoint() {
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.,:
- $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
.
Related:
Be First to Comment