diff --git a/modules/swagger-core/src/main/java/io/swagger/v3/core/jackson/mixin/Schema31Mixin.java b/modules/swagger-core/src/main/java/io/swagger/v3/core/jackson/mixin/Schema31Mixin.java index 782ae13225..066151dd39 100644 --- a/modules/swagger-core/src/main/java/io/swagger/v3/core/jackson/mixin/Schema31Mixin.java +++ b/modules/swagger-core/src/main/java/io/swagger/v3/core/jackson/mixin/Schema31Mixin.java @@ -7,12 +7,18 @@ import com.fasterxml.jackson.annotation.JsonProperty; import com.fasterxml.jackson.annotation.JsonPropertyOrder; import com.fasterxml.jackson.core.JsonGenerator; +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 com.fasterxml.jackson.databind.JsonSerializer; import com.fasterxml.jackson.databind.SerializerProvider; +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; import com.fasterxml.jackson.databind.annotation.JsonSerialize; import java.io.IOException; import java.math.BigDecimal; +import java.util.LinkedHashSet; import java.util.Map; import java.util.Set; @@ -42,6 +48,7 @@ public abstract class Schema31Mixin { @JsonProperty("type") @JsonSerialize(using = TypeSerializer.class) + @JsonDeserialize(using = TypeDeserializer.class) public abstract Set getTypes(); @JsonAnyGetter @@ -78,4 +85,30 @@ public void serialize(Set types, JsonGenerator jsonGenerator, Serializer } } + /** + * Inverse of {@link TypeSerializer}: accepts either a scalar string + * ({@code "type":"integer"}) or an array ({@code "type":["string","null"]}). + */ + public static class TypeDeserializer extends JsonDeserializer> { + + @Override + public Set deserialize(JsonParser p, DeserializationContext ctxt) throws IOException { + JsonNode node = p.getCodec().readTree(p); + if (node == null || node.isNull()) { + return null; + } + Set types = new LinkedHashSet<>(); + if (node.isArray()) { + node.forEach(n -> { + if (n != null && !n.isNull()) { + types.add(n.asText()); + } + }); + } else { + types.add(node.asText()); + } + return types.isEmpty() ? null : types; + } + } + } diff --git a/modules/swagger-core/src/test/java/io/swagger/v3/core/deserialization/JsonSchemaTypeRoundTripTest.java b/modules/swagger-core/src/test/java/io/swagger/v3/core/deserialization/JsonSchemaTypeRoundTripTest.java new file mode 100644 index 0000000000..4bc335b576 --- /dev/null +++ b/modules/swagger-core/src/test/java/io/swagger/v3/core/deserialization/JsonSchemaTypeRoundTripTest.java @@ -0,0 +1,64 @@ +package io.swagger.v3.core.deserialization; + +import io.swagger.v3.core.util.Json31; +import io.swagger.v3.oas.models.media.JsonSchema; +import org.testng.annotations.Test; + +import java.util.Arrays; +import java.util.LinkedHashSet; +import java.util.Set; + +import static org.testng.Assert.assertEquals; +import static org.testng.Assert.assertNotNull; +import static org.testng.Assert.assertTrue; + +/** + * OpenAPI 3.1 serializes a single schema type as a JSON string + * ({@code "type":"integer"}) via {@code Schema31Mixin.TypeSerializer}, while + * the model field is {@code Set types}. Without a matching deserializer, + * round-tripping {@link JsonSchema} fails when reading that scalar back. + * + * @see #5264 + */ +public class JsonSchemaTypeRoundTripTest { + + @Test + public void singleTypeStringRoundTripsOnJsonSchema() throws Exception { + JsonSchema original = new JsonSchema(); + original.setTypes(new LinkedHashSet<>(Arrays.asList("integer"))); + + String json = Json31.mapper().writeValueAsString(original); + assertTrue(json.contains("\"type\":\"integer\"") || json.contains("\"type\": \"integer\""), + "expected scalar type in serialized form, got: " + json); + + JsonSchema roundTripped = Json31.mapper().readValue(json, JsonSchema.class); + assertNotNull(roundTripped); + assertEquals(roundTripped.getTypes(), setOf("integer")); + } + + @Test + public void multiTypeArrayRoundTripsOnJsonSchema() throws Exception { + JsonSchema original = new JsonSchema(); + original.setTypes(new LinkedHashSet<>(Arrays.asList("string", "null"))); + + String json = Json31.mapper().writeValueAsString(original); + JsonSchema roundTripped = Json31.mapper().readValue(json, JsonSchema.class); + + assertNotNull(roundTripped); + assertEquals(roundTripped.getTypes(), setOf("string", "null")); + } + + @Test + public void convertValuePreservesSingleType() { + JsonSchema original = new JsonSchema(); + original.setTypes(new LinkedHashSet<>(Arrays.asList("array"))); + + JsonSchema converted = Json31.mapper().convertValue(original, JsonSchema.class); + assertNotNull(converted); + assertEquals(converted.getTypes(), setOf("array")); + } + + private static Set setOf(String... values) { + return new LinkedHashSet<>(Arrays.asList(values)); + } +} diff --git a/modules/swagger-integration/src/main/java/io/swagger/v3/oas/integration/GenericOpenApiContext.java b/modules/swagger-integration/src/main/java/io/swagger/v3/oas/integration/GenericOpenApiContext.java index f7681cb1ac..63dea84e2b 100644 --- a/modules/swagger-integration/src/main/java/io/swagger/v3/oas/integration/GenericOpenApiContext.java +++ b/modules/swagger-integration/src/main/java/io/swagger/v3/oas/integration/GenericOpenApiContext.java @@ -9,6 +9,7 @@ import com.fasterxml.jackson.databind.MapperFeature; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.SerializationFeature; +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; import com.fasterxml.jackson.databind.annotation.JsonSerialize; import io.swagger.v3.core.converter.ModelConverter; import io.swagger.v3.core.converter.ModelConverters; @@ -914,6 +915,7 @@ static abstract class SortedSchemaMixin31 { @JsonProperty("type") @JsonSerialize(using = Schema31Mixin.TypeSerializer.class) + @JsonDeserialize(using = Schema31Mixin.TypeDeserializer.class) public abstract Set getTypes(); @JsonAnySetter diff --git a/modules/swagger-integration/src/test/java/io/swagger/v3/oas/integration/SortedSchemaMixin31TypeRoundTripTest.java b/modules/swagger-integration/src/test/java/io/swagger/v3/oas/integration/SortedSchemaMixin31TypeRoundTripTest.java new file mode 100644 index 0000000000..b6a157799e --- /dev/null +++ b/modules/swagger-integration/src/test/java/io/swagger/v3/oas/integration/SortedSchemaMixin31TypeRoundTripTest.java @@ -0,0 +1,57 @@ +package io.swagger.v3.oas.integration; + +import com.fasterxml.jackson.databind.ObjectMapper; +import io.swagger.v3.core.util.Json31; +import io.swagger.v3.oas.models.media.JsonSchema; +import io.swagger.v3.oas.models.media.Schema; +import org.testng.annotations.Test; + +import java.util.Arrays; +import java.util.LinkedHashSet; +import java.util.Set; + +import static org.testng.Assert.assertEquals; +import static org.testng.Assert.assertNotNull; +import static org.testng.Assert.assertTrue; + +/** + * sortOutput replaces {@code Schema31Mixin} with {@code SortedSchemaMixin31}. + * The sorted mixin must keep the scalar-type deserializer or OAS 3.1 + * {@code "type":"integer"} fails to read back into {@code Set}. + */ +public class SortedSchemaMixin31TypeRoundTripTest { + + @Test + public void sortedMixinRoundTripsScalarType() throws Exception { + ObjectMapper mapper = Json31.mapper().copy(); + mapper.addMixIn(Schema.class, GenericOpenApiContext.SortedSchemaMixin31.class); + + JsonSchema original = new JsonSchema(); + original.setTypes(new LinkedHashSet(Arrays.asList("integer"))); + + String json = mapper.writeValueAsString(original); + assertTrue(json.contains("\"type\":\"integer\"") || json.contains("\"type\": \"integer\""), + "expected scalar type in serialized form, got: " + json); + + JsonSchema roundTripped = mapper.readValue(json, JsonSchema.class); + assertNotNull(roundTripped); + assertEquals(roundTripped.getTypes(), setOf("integer")); + } + + @Test + public void sortedMixinRoundTripsTypeArray() throws Exception { + ObjectMapper mapper = Json31.mapper().copy(); + mapper.addMixIn(Schema.class, GenericOpenApiContext.SortedSchemaMixin31.class); + + JsonSchema original = new JsonSchema(); + original.setTypes(new LinkedHashSet(Arrays.asList("string", "null"))); + + JsonSchema roundTripped = mapper.readValue(mapper.writeValueAsString(original), JsonSchema.class); + assertNotNull(roundTripped); + assertEquals(roundTripped.getTypes(), setOf("string", "null")); + } + + private static Set setOf(String... values) { + return new LinkedHashSet(Arrays.asList(values)); + } +}