diff --git a/springdoc-openapi-starter-common/src/main/java/org/springdoc/core/deserializers/TypeSetDeserializer.java b/springdoc-openapi-starter-common/src/main/java/org/springdoc/core/deserializers/TypeSetDeserializer.java new file mode 100644 index 000000000..92a64954b --- /dev/null +++ b/springdoc-openapi-starter-common/src/main/java/org/springdoc/core/deserializers/TypeSetDeserializer.java @@ -0,0 +1,39 @@ +package org.springdoc.core.deserializers; + +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.databind.DeserializationContext; +import com.fasterxml.jackson.databind.JsonDeserializer; +import com.fasterxml.jackson.databind.JsonNode; + +import java.io.IOException; +import java.util.LinkedHashSet; +import java.util.Set; + +/** + * Reads an OpenAPI 3.1 schema "type" that swagger-core serializes as a scalar + * {@code "type": "integer"} back into the Set field, while still supporting + * the array form {@code "type": ["string","null"]}. + */ +public class TypeSetDeserializer extends JsonDeserializer> { + + @Override + public Set deserialize(JsonParser p, DeserializationContext ctxt) throws IOException { + JsonNode node = ctxt.readTree(p); + if (node == null || node.isNull()) { + return null; + } + Set types = new LinkedHashSet<>(); + if (node.isArray()) { + node.forEach(n -> { + if (!n.isNull()) { + types.add(n.asText()); + } + }); + } + else { + types.add(node.asText()); + } + return types.isEmpty() ? null : types; + } + +} diff --git a/springdoc-openapi-starter-common/src/main/java/org/springdoc/core/mixins/SchemaTypeMixin.java b/springdoc-openapi-starter-common/src/main/java/org/springdoc/core/mixins/SchemaTypeMixin.java new file mode 100644 index 000000000..34722a823 --- /dev/null +++ b/springdoc-openapi-starter-common/src/main/java/org/springdoc/core/mixins/SchemaTypeMixin.java @@ -0,0 +1,15 @@ +package org.springdoc.core.mixins; + +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import org.springdoc.core.deserializers.TypeSetDeserializer; + +import java.util.Set; + +public interface SchemaTypeMixin { + + @JsonProperty("type") + @JsonDeserialize(using = TypeSetDeserializer.class) + void setTypes(Set types); + +} diff --git a/springdoc-openapi-starter-common/src/main/java/org/springdoc/core/providers/ObjectMapperProvider.java b/springdoc-openapi-starter-common/src/main/java/org/springdoc/core/providers/ObjectMapperProvider.java index 11124f58e..821aa10f2 100644 --- a/springdoc-openapi-starter-common/src/main/java/org/springdoc/core/providers/ObjectMapperProvider.java +++ b/springdoc-openapi-starter-common/src/main/java/org/springdoc/core/providers/ObjectMapperProvider.java @@ -35,11 +35,13 @@ import io.swagger.v3.core.util.Yaml; import io.swagger.v3.core.util.Yaml31; import io.swagger.v3.oas.models.OpenAPI; +import io.swagger.v3.oas.models.media.JsonSchema; import io.swagger.v3.oas.models.media.Schema; import org.springdoc.core.mixins.SortedOpenAPIMixin; import org.springdoc.core.mixins.SortedOpenAPIMixin31; import org.springdoc.core.mixins.SortedSchemaMixin; import org.springdoc.core.mixins.SortedSchemaMixin31; +import org.springdoc.core.mixins.SchemaTypeMixin; import org.springdoc.core.properties.SpringDocConfigProperties; import org.springdoc.core.properties.SpringDocConfigProperties.ApiDocs.OpenApiVersion; @@ -74,6 +76,7 @@ public ObjectMapperProvider(SpringDocConfigProperties springDocConfigProperties) if (openApiVersion == OpenApiVersion.OPENAPI_3_1) { jsonMapper = Json31.mapper(); yamlMapper = Yaml31.mapper(); + jsonMapper.addMixIn(JsonSchema.class, SchemaTypeMixin.class); if (springDocConfigProperties.isUseArbitrarySchemas()) { System.setProperty(Schema.USE_ARBITRARY_SCHEMA_PROPERTY, "true"); } diff --git a/springdoc-openapi-starter-common/src/test/java/org/springdoc/core/utils/SpringDocUtilsTest.java b/springdoc-openapi-starter-common/src/test/java/org/springdoc/core/utils/SpringDocUtilsTest.java new file mode 100644 index 000000000..b5e3e6838 --- /dev/null +++ b/springdoc-openapi-starter-common/src/test/java/org/springdoc/core/utils/SpringDocUtilsTest.java @@ -0,0 +1,64 @@ +package org.springdoc.core.utils; + +import io.swagger.v3.oas.models.media.JsonSchema; +import org.junit.jupiter.api.Test; +import org.springdoc.core.properties.SpringDocConfigProperties; +import org.springdoc.core.providers.ObjectMapperProvider; + +import java.util.Set; + +import static org.junit.jupiter.api.Assertions.*; + +class SpringDocUtilsTest { + + @Test + void singleTypeForJsonSchemaJsonCloning() { + SpringDocConfigProperties props = new SpringDocConfigProperties(); + props.getApiDocs().setVersion(SpringDocConfigProperties.ApiDocs.OpenApiVersion.OPENAPI_3_1); + ObjectMapperProvider provider = new ObjectMapperProvider(props); + + JsonSchema jsonSchema = new JsonSchema(); + jsonSchema.setTypes(Set.of("integer")); + + JsonSchema cloned = SpringDocUtils.cloneViaJson(jsonSchema, JsonSchema.class, provider.jsonMapper()); + + // The object is cloned properly, we do not get the type cast fallback + assertNotSame(jsonSchema, cloned); + assertNotNull(cloned); + assertEquals(Set.of("integer"), cloned.getTypes()); + } + + @Test + void nullTypeBecomesNullTypesForJsonSchemaJsonCloning() { + SpringDocConfigProperties props = new SpringDocConfigProperties(); + props.getApiDocs().setVersion(SpringDocConfigProperties.ApiDocs.OpenApiVersion.OPENAPI_3_1); + ObjectMapperProvider provider = new ObjectMapperProvider(props); + + JsonSchema jsonSchema = new JsonSchema(); + + JsonSchema cloned = SpringDocUtils.cloneViaJson(jsonSchema, JsonSchema.class, provider.jsonMapper()); + + // The object is cloned properly, we do not get the type cast fallback + assertNotSame(jsonSchema, cloned); + assertNotNull(cloned); + assertNull(cloned.getTypes()); + } + + @Test + void typeArrayIsRetainedForJsonSchemaJsonCloning() { + SpringDocConfigProperties props = new SpringDocConfigProperties(); + props.getApiDocs().setVersion(SpringDocConfigProperties.ApiDocs.OpenApiVersion.OPENAPI_3_1); + ObjectMapperProvider provider = new ObjectMapperProvider(props); + + JsonSchema jsonSchema = new JsonSchema(); + jsonSchema.setTypes(Set.of("integer", "null")); + + JsonSchema cloned = SpringDocUtils.cloneViaJson(jsonSchema, JsonSchema.class, provider.jsonMapper()); + + // The object is cloned properly, we do not get the type cast fallback + assertNotSame(jsonSchema, cloned); + assertNotNull(cloned); + assertEquals(Set.of("integer", "null"), cloned.getTypes()); + } + +} \ No newline at end of file