diff --git a/CHANGELOG.md b/CHANGELOG.md index 63d6679d6..247823694 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -35,6 +35,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Fixed - #3328, #3337 – `/v3/api-docs` fails with a `NullPointerException` when spring-hateoas is on the classpath without `HateoasProperties` +- #3314 – `Json Processing Exception occurred` is logged for every constrained parameter whose schema is not a `JsonSchema` - #3320 – `@Order` and `Ordered` ignored when applying customizers - #3319 – A `Page` nested in another schema is not replaced by `PagedModel` - #3313 – Springdoc auto-configurations rely on unspecified auto-configuration ordering 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 index 72a494d21..1be746c2b 100644 --- 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 @@ -30,15 +30,20 @@ import com.fasterxml.jackson.annotation.JsonProperty; import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import io.swagger.v3.core.jackson.mixin.Schema31Mixin; import org.springdoc.core.deserializers.TypeSetDeserializer; /** * The type Schema type mixin. Makes the OpenAPI 3.1 {@code type} readable back into the * {@code types} set, whichever of the two serialized forms it takes. + *

+ * It extends the swagger-core mixin so that it can be registered on {@code Schema} itself + * without losing the serialization it declares, which is what makes the deserializer apply + * to every schema subclass rather than to {@code JsonSchema} alone. * * @author Mattias-Sehlstedt */ -public interface SchemaTypeMixin { +public abstract class SchemaTypeMixin extends Schema31Mixin { /** * Sets types. @@ -47,6 +52,6 @@ public interface SchemaTypeMixin { */ @JsonProperty("type") @JsonDeserialize(using = TypeSetDeserializer.class) - void setTypes(Set types); + public abstract void setTypes(Set types); } diff --git a/springdoc-openapi-starter-common/src/main/java/org/springdoc/core/mixins/SortedSchemaMixin31.java b/springdoc-openapi-starter-common/src/main/java/org/springdoc/core/mixins/SortedSchemaMixin31.java index 7014b4727..02adfb4bd 100644 --- a/springdoc-openapi-starter-common/src/main/java/org/springdoc/core/mixins/SortedSchemaMixin31.java +++ b/springdoc-openapi-starter-common/src/main/java/org/springdoc/core/mixins/SortedSchemaMixin31.java @@ -36,8 +36,10 @@ import com.fasterxml.jackson.annotation.JsonInclude; import com.fasterxml.jackson.annotation.JsonProperty; import com.fasterxml.jackson.annotation.JsonPropertyOrder; +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; import com.fasterxml.jackson.databind.annotation.JsonSerialize; import io.swagger.v3.core.jackson.mixin.Schema31Mixin; +import org.springdoc.core.deserializers.TypeSetDeserializer; /** * The interface Sorted schema mixin 31. @@ -121,6 +123,15 @@ public interface SortedSchemaMixin31 { @JsonSerialize(using = Schema31Mixin.TypeSerializer.class) Set getTypes(); + /** + * Sets types. + * + * @param types the types + */ + @JsonProperty("type") + @JsonDeserialize(using = TypeSetDeserializer.class) + void setTypes(Set types); + /** * Add extension. * 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 a2a498b60..a403719b3 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,7 +35,6 @@ 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.SchemaTypeMixin; import org.springdoc.core.mixins.SortedOpenAPIMixin; @@ -76,7 +75,7 @@ public ObjectMapperProvider(SpringDocConfigProperties springDocConfigProperties) if (openApiVersion == OpenApiVersion.OPENAPI_3_1) { jsonMapper = Json31.mapper(); yamlMapper = Yaml31.mapper(); - jsonMapper.addMixIn(JsonSchema.class, SchemaTypeMixin.class); + jsonMapper.addMixIn(Schema.class, SchemaTypeMixin.class); if (springDocConfigProperties.isUseArbitrarySchemas()) { System.setProperty(Schema.USE_ARBITRARY_SCHEMA_PROPERTY, "true"); } @@ -99,8 +98,10 @@ public ObjectMapperProvider(SpringDocConfigProperties springDocConfigProperties) public static ObjectMapper createJson(SpringDocConfigProperties springDocConfigProperties) { OpenApiVersion openApiVersion = springDocConfigProperties.getApiDocs().getVersion(); ObjectMapper objectMapper; - if (openApiVersion == OpenApiVersion.OPENAPI_3_1) + if (openApiVersion == OpenApiVersion.OPENAPI_3_1) { objectMapper = ObjectMapperFactory.createJson31(); + objectMapper.addMixIn(Schema.class, SchemaTypeMixin.class); + } else objectMapper = ObjectMapperFactory.createJson(); 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 index eeb0de530..5c683eba5 100644 --- 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 @@ -26,7 +26,9 @@ import java.util.Set; +import io.swagger.v3.oas.models.media.ArraySchema; import io.swagger.v3.oas.models.media.JsonSchema; +import io.swagger.v3.oas.models.media.StringSchema; import org.junit.jupiter.api.Test; import org.springdoc.core.properties.SpringDocConfigProperties; import org.springdoc.core.properties.SpringDocConfigProperties.ApiDocs.OpenApiVersion; @@ -90,10 +92,61 @@ void typeArrayIsRetainedForJsonSchemaJsonCloning() { assertEquals(Set.of("integer", "null"), cloned.getTypes()); } + @Test + void singleTypeForSchemaSubclassJsonCloning() { + ObjectMapperProvider provider = openapi31Provider(); + + ArraySchema arraySchema = new ArraySchema(); + arraySchema.setTypes(Set.of("array")); + arraySchema.setItems(new StringSchema()); + + ArraySchema cloned = SpringDocUtils.cloneViaJson(arraySchema, ArraySchema.class, provider.jsonMapper()); + + // The object is cloned properly, we do not get the type cast fallback + assertNotSame(arraySchema, cloned); + assertNotNull(cloned); + assertEquals(Set.of("array"), cloned.getTypes()); + } + + @Test + void singleTypeForSchemaSubclassJsonCloningWithAStandaloneMapper() { + ArraySchema arraySchema = new ArraySchema(); + arraySchema.setTypes(Set.of("array")); + arraySchema.setItems(new StringSchema()); + + ArraySchema cloned = SpringDocUtils.cloneViaJson(arraySchema, ArraySchema.class, ObjectMapperProvider.createJson(openapi31Properties())); + + // The object is cloned properly, we do not get the type cast fallback + assertNotSame(arraySchema, cloned); + assertNotNull(cloned); + assertEquals(Set.of("array"), cloned.getTypes()); + } + + @Test + void singleTypeForSchemaSubclassJsonCloningWithASortingMapper() { + SpringDocConfigProperties properties = openapi31Properties(); + properties.setWriterWithOrderByKeys(true); + + ArraySchema arraySchema = new ArraySchema(); + arraySchema.setTypes(Set.of("array")); + arraySchema.setItems(new StringSchema()); + + ArraySchema cloned = SpringDocUtils.cloneViaJson(arraySchema, ArraySchema.class, ObjectMapperProvider.createJson(properties)); + + // The object is cloned properly, we do not get the type cast fallback + assertNotSame(arraySchema, cloned); + assertNotNull(cloned); + assertEquals(Set.of("array"), cloned.getTypes()); + } + private ObjectMapperProvider openapi31Provider() { + return new ObjectMapperProvider(openapi31Properties()); + } + + private SpringDocConfigProperties openapi31Properties() { SpringDocConfigProperties properties = new SpringDocConfigProperties(); properties.getApiDocs().setVersion(OpenApiVersion.OPENAPI_3_1); - return new ObjectMapperProvider(properties); + return properties; } }