From 726bc46958fa053da50a5e139e39954175fe4ea1 Mon Sep 17 00:00:00 2001 From: Badr NASS LAHSEN Date: Sun, 6 Sep 2026 04:30:28 +0200 Subject: [PATCH] fix: read the OAS 3.1 type back for every schema subclass The type deserializer was only registered for JsonSchema, so cloning any other schema subclass still failed and logged a Json Processing Exception warning for every constrained parameter. Extend the swagger-core mixin so that it can be registered on Schema itself without dropping the serialization it declares, register it on the standalone mappers too, and carry the deserializer over to the sorting mixin that replaces it. Fixes #3314 Co-Authored-By: Claude Opus 5 --- CHANGELOG.md | 1 + .../core/mixins/SchemaTypeMixin.java | 9 ++- .../core/mixins/SortedSchemaMixin31.java | 11 ++++ .../core/providers/ObjectMapperProvider.java | 7 ++- .../core/utils/SpringDocUtilsTest.java | 55 ++++++++++++++++++- 5 files changed, 77 insertions(+), 6 deletions(-) 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; } }