Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -42,6 +48,7 @@ public abstract class Schema31Mixin {

@JsonProperty("type")
@JsonSerialize(using = TypeSerializer.class)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note that the serializer is used in an additional location and that it might be of relevance to attach the deserializer there as well.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yep, added TypeDeserializer on SortedSchemaMixin31 too

@JsonDeserialize(using = TypeDeserializer.class)
public abstract Set<String> getTypes();

@JsonAnyGetter
Expand Down Expand Up @@ -78,4 +85,30 @@ public void serialize(Set<String> 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<Set<String>> {

@Override
public Set<String> deserialize(JsonParser p, DeserializationContext ctxt) throws IOException {
JsonNode node = p.getCodec().readTree(p);
if (node == null || node.isNull()) {
return null;
}
Set<String> 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;
}
}

}
Original file line number Diff line number Diff line change
@@ -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<String> types}. Without a matching deserializer,
* round-tripping {@link JsonSchema} fails when reading that scalar back.
*
* @see <a href="https://github.com/swagger-api/swagger-core/issues/5264">#5264</a>
*/
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<String> setOf(String... values) {
return new LinkedHashSet<>(Arrays.asList(values));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -914,6 +915,7 @@ static abstract class SortedSchemaMixin31 {

@JsonProperty("type")
@JsonSerialize(using = Schema31Mixin.TypeSerializer.class)
@JsonDeserialize(using = Schema31Mixin.TypeDeserializer.class)
public abstract Set<String> getTypes();

@JsonAnySetter
Expand Down
Original file line number Diff line number Diff line change
@@ -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<String>}.
*/
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<String>(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<String>(Arrays.asList("string", "null")));

JsonSchema roundTripped = mapper.readValue(mapper.writeValueAsString(original), JsonSchema.class);
assertNotNull(roundTripped);
assertEquals(roundTripped.getTypes(), setOf("string", "null"));
}

private static Set<String> setOf(String... values) {
return new LinkedHashSet<String>(Arrays.asList(values));
}
}
Loading