diff --git a/modules/swagger-core/src/main/java/io/swagger/v3/core/jackson/ModelResolver.java b/modules/swagger-core/src/main/java/io/swagger/v3/core/jackson/ModelResolver.java index 8abdcbad4b..73437bb660 100644 --- a/modules/swagger-core/src/main/java/io/swagger/v3/core/jackson/ModelResolver.java +++ b/modules/swagger-core/src/main/java/io/swagger/v3/core/jackson/ModelResolver.java @@ -309,10 +309,20 @@ public Schema resolve(AnnotatedType annotatedType, ModelConverterContext context isPrimitive = true; } if (model == null) { - PrimitiveType primitiveType = PrimitiveType.fromType(type); - if (primitiveType != null) { - model = PrimitiveType.fromType(type).createProperty(); - isPrimitive = true; + if (resolvedSchemaAnnotation != null && StringUtils.isEmpty(resolvedSchemaAnnotation.type())) { + PrimitiveType primitiveType = PrimitiveType.fromTypeAndFormat(type, resolvedSchemaAnnotation.format()); + if (primitiveType != null) { + model = primitiveType.createProperty(); + isPrimitive = true; + } + } + + if (model == null) { + PrimitiveType primitiveType = PrimitiveType.fromType(type); + if (primitiveType != null) { + model = primitiveType.createProperty(); + isPrimitive = true; + } } } @@ -629,7 +639,8 @@ public Schema resolve(AnnotatedType annotatedType, ModelConverterContext context propType = ((AnnotatedMethod)member).getParameterType(0); } - } + } + String propSchemaName = null; io.swagger.v3.oas.annotations.media.Schema ctxSchema = AnnotationsUtils.getSchemaAnnotation(annotations); if (AnnotationsUtils.hasSchemaAnnotation(ctxSchema)) { diff --git a/modules/swagger-core/src/main/java/io/swagger/v3/core/util/PrimitiveType.java b/modules/swagger-core/src/main/java/io/swagger/v3/core/util/PrimitiveType.java index c5b8229319..df32dc2958 100644 --- a/modules/swagger-core/src/main/java/io/swagger/v3/core/util/PrimitiveType.java +++ b/modules/swagger-core/src/main/java/io/swagger/v3/core/util/PrimitiveType.java @@ -15,6 +15,8 @@ import org.apache.commons.lang3.StringUtils; import java.lang.reflect.Type; +import java.util.ArrayList; +import java.util.Collection; import java.util.Collections; import java.util.HashMap; import java.util.Map; @@ -41,13 +43,23 @@ public Schema createProperty() { }, BYTE(Byte.class, "byte") { @Override - public ByteArraySchema createProperty() { + public Schema createProperty() { + if ( + (System.getProperty(Schema.BINARY_STRING_CONVERSION_PROPERTY) != null && System.getProperty(Schema.BINARY_STRING_CONVERSION_PROPERTY).equals(Schema.BynaryStringConversion.BINARY_STRING_CONVERSION_STRING_SCHEMA.toString())) || + (System.getenv(Schema.BINARY_STRING_CONVERSION_PROPERTY) != null && System.getenv(Schema.BINARY_STRING_CONVERSION_PROPERTY).equals(Schema.BynaryStringConversion.BINARY_STRING_CONVERSION_STRING_SCHEMA.toString()))) { + return new StringSchema().format("byte"); + } return new ByteArraySchema(); } }, BINARY(Byte.class, "binary") { @Override - public BinarySchema createProperty() { + public Schema createProperty() { + if ( + (System.getProperty(Schema.BINARY_STRING_CONVERSION_PROPERTY) != null && System.getProperty(Schema.BINARY_STRING_CONVERSION_PROPERTY).equals(Schema.BynaryStringConversion.BINARY_STRING_CONVERSION_STRING_SCHEMA.toString())) || + (System.getenv(Schema.BINARY_STRING_CONVERSION_PROPERTY) != null && System.getenv(Schema.BINARY_STRING_CONVERSION_PROPERTY).equals(Schema.BynaryStringConversion.BINARY_STRING_CONVERSION_STRING_SCHEMA.toString()))) { + return new StringSchema().format("binary"); + } return new BinarySchema(); } }, @@ -149,6 +161,7 @@ public Schema createProperty() { }; private static final Map, PrimitiveType> KEY_CLASSES; + private static final Map, Collection> MULTI_KEY_CLASSES; private static final Map, PrimitiveType> BASE_CLASSES; /** * Adds support of a small number of "well-known" types, specifically for @@ -244,6 +257,11 @@ public Schema createProperty() { addKeys(keyClasses, OBJECT, Object.class); KEY_CLASSES = Collections.unmodifiableMap(keyClasses); + final Map, Collection> multiKeyClasses = new HashMap<>(); + addMultiKeys(multiKeyClasses, BYTE, byte[].class); + addMultiKeys(multiKeyClasses, BINARY, byte[].class); + MULTI_KEY_CLASSES = Collections.unmodifiableMap(multiKeyClasses); + final Map, PrimitiveType> baseClasses = new HashMap<>(); addKeys(baseClasses, DATE_TIME, java.util.Date.class, java.util.Calendar.class); BASE_CLASSES = Collections.unmodifiableMap(baseClasses); @@ -343,6 +361,20 @@ public static Set nonSystemTypePackages() { return nonSystemTypePackages; } + public static PrimitiveType fromTypeAndFormat(Type type, String format) { + final Class raw = TypeFactory.defaultInstance().constructType(type).getRawClass(); + final Collection keys = MULTI_KEY_CLASSES.get(raw); + if (keys == null || keys.isEmpty() || StringUtils.isBlank(format)) { + return fromType(type); + } else { + return keys + .stream() + .filter(t -> t.getCommonName().equalsIgnoreCase(format)) + .findAny() + .orElse(null); + } + } + public static PrimitiveType fromType(Type type) { final Class raw = TypeFactory.defaultInstance().constructType(type).getRawClass(); final PrimitiveType key = KEY_CLASSES.get(raw); @@ -351,6 +383,14 @@ public static PrimitiveType fromType(Type type) { return key; } } + + final Collection keys = MULTI_KEY_CLASSES.get(raw); + if (keys != null && !keys.isEmpty()) { + final PrimitiveType first = keys.iterator().next(); + if (!customExcludedClasses.contains(raw.getName())) { + return first; + } + } final PrimitiveType custom = customClasses.get(raw.getName()); if (custom != null) { @@ -424,6 +464,15 @@ private static void addKeys(Map map, PrimitiveType type, K } } + private static void addMultiKeys(Map> map, PrimitiveType type, K... keys) { + for (K key : keys) { + if (!map.containsKey(key)) { + map.put(key, new ArrayList<>()); + } + map.get(key).add(type); + } + } + private static class DateStub { private DateStub() { } diff --git a/modules/swagger-jaxrs2/src/test/java/io/swagger/v3/jaxrs2/BinaryParameterResourceTest.java b/modules/swagger-jaxrs2/src/test/java/io/swagger/v3/jaxrs2/BinaryParameterResourceTest.java new file mode 100644 index 0000000000..03c16c58bb --- /dev/null +++ b/modules/swagger-jaxrs2/src/test/java/io/swagger/v3/jaxrs2/BinaryParameterResourceTest.java @@ -0,0 +1,45 @@ +package io.swagger.v3.jaxrs2; + +import java.io.IOException; + +import io.swagger.v3.oas.models.media.Schema; +import org.testng.annotations.AfterTest; +import org.testng.annotations.BeforeTest; +import org.testng.annotations.Test; + +import io.swagger.v3.jaxrs2.annotations.AbstractAnnotationTest; +import io.swagger.v3.jaxrs2.resources.BinaryParameterResource; + +public class BinaryParameterResourceTest extends AbstractAnnotationTest { + + @Test(description = "check binary model serialization with base64", singleThreaded = true) // tests issue #2466 + public void shouldSerializeBinaryParameterBase64() throws IOException { + try { + System.setProperty(Schema.BINARY_STRING_CONVERSION_PROPERTY, Schema.BynaryStringConversion.BINARY_STRING_CONVERSION_BASE64.toString()); + compareAsYaml(BinaryParameterResource.class, getOpenAPIAsString("BinaryParameterResource.yaml")); + } finally { + System.clearProperty(Schema.BINARY_STRING_CONVERSION_PROPERTY); + } + } + + @BeforeTest + public void before() { + System.clearProperty(Schema.BINARY_STRING_CONVERSION_PROPERTY); + } + + @AfterTest + public void after() { + System.clearProperty(Schema.BINARY_STRING_CONVERSION_PROPERTY); + } + + + @Test(description = "check binary model serialization with StringSchema", singleThreaded = true) // tests issue #2466 + public void shouldSerializeBinaryParameterStringSchema() throws IOException { + try { + System.setProperty(Schema.BINARY_STRING_CONVERSION_PROPERTY, Schema.BynaryStringConversion.BINARY_STRING_CONVERSION_STRING_SCHEMA.toString()); + compareAsYaml(BinaryParameterResource.class, getOpenAPIAsString("BinaryParameterResource.yaml")); + } finally { + System.clearProperty(Schema.BINARY_STRING_CONVERSION_PROPERTY); + } + } +} diff --git a/modules/swagger-jaxrs2/src/test/java/io/swagger/v3/jaxrs2/resources/BinaryParameterResource.java b/modules/swagger-jaxrs2/src/test/java/io/swagger/v3/jaxrs2/resources/BinaryParameterResource.java new file mode 100644 index 0000000000..954a15e065 --- /dev/null +++ b/modules/swagger-jaxrs2/src/test/java/io/swagger/v3/jaxrs2/resources/BinaryParameterResource.java @@ -0,0 +1,44 @@ +package io.swagger.v3.jaxrs2.resources; + +import javax.ws.rs.Consumes; +import javax.ws.rs.POST; +import javax.ws.rs.Path; +import javax.ws.rs.core.Context; +import javax.ws.rs.core.MediaType; +import javax.ws.rs.core.Response; +import javax.ws.rs.core.UriInfo; + +import io.swagger.v3.jaxrs2.resources.model.Item; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.headers.Header; +import io.swagger.v3.oas.annotations.media.Content; +import io.swagger.v3.oas.annotations.media.Schema; +import io.swagger.v3.oas.annotations.responses.ApiResponse; + +@Path("/") +public class BinaryParameterResource { + @Consumes({ MediaType.APPLICATION_JSON }) + @Path("/binary") + @POST + @Operation( + summary = "Create new item", + description = "Post operation with entity in a body", + responses = { + @ApiResponse( + content = @Content( + schema = @Schema(implementation = Item.class), + mediaType = MediaType.APPLICATION_JSON + ), + headers = @Header(name = "Location"), + responseCode = "201" + ) + } + ) + public Response createItem(@Context final UriInfo uriInfo, @Parameter(required = true) final Item item) { + return Response + .created(uriInfo.getBaseUriBuilder().path(item.getName()).build()) + .entity(item).build(); + } + +} diff --git a/modules/swagger-jaxrs2/src/test/java/io/swagger/v3/jaxrs2/resources/model/Item.java b/modules/swagger-jaxrs2/src/test/java/io/swagger/v3/jaxrs2/resources/model/Item.java new file mode 100644 index 0000000000..eb3af47c96 --- /dev/null +++ b/modules/swagger-jaxrs2/src/test/java/io/swagger/v3/jaxrs2/resources/model/Item.java @@ -0,0 +1,57 @@ +package io.swagger.v3.jaxrs2.resources.model; + +import io.swagger.v3.oas.annotations.media.Schema; + +public class Item { + private String name; + private String value; + @Schema(example = "Ynl0ZQ==") + private byte[] bytes; + @Schema(format = "binary", example = "YmluYXJ5") + private byte[] binary; + + private byte[] byteNoAnnotation; + + public Item() { + } + + public void setBinary(byte[] binary) { + this.binary = binary; + } + + public byte[] getBinary() { + return binary; + } + + public void setName(String name) { + this.name = name; + } + + public String getName() { + return name; + } + + public void setValue(String value) { + this.value = value; + } + + public String getValue() { + return value; + } + + public void setBytes(byte[] bytes) { + this.bytes = bytes; + } + + public byte[] getBytes() { + return bytes; + } + + public void setByteNoAnnotation(byte[] byteNoAnnotation) { + this.byteNoAnnotation = byteNoAnnotation; + } + + public byte[] getByteNoAnnotation() { + return byteNoAnnotation; + } +} diff --git a/modules/swagger-jaxrs2/src/test/resources/BinaryParameterResource.yaml b/modules/swagger-jaxrs2/src/test/resources/BinaryParameterResource.yaml new file mode 100644 index 0000000000..ad05aac34b --- /dev/null +++ b/modules/swagger-jaxrs2/src/test/resources/BinaryParameterResource.yaml @@ -0,0 +1,42 @@ +openapi: 3.0.1 +paths: + /binary: + post: + summary: Create new item + description: Post operation with entity in a body + operationId: createItem + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/Item' + required: true + responses: + "201": + headers: + Location: + style: simple + content: + application/json: + schema: + $ref: '#/components/schemas/Item' +components: + schemas: + Item: + type: object + properties: + name: + type: string + value: + type: string + bytes: + type: string + format: byte + example: Ynl0ZQ== + binary: + type: string + format: binary + example: YmluYXJ5 + byteNoAnnotation: + type: string + format: byte diff --git a/modules/swagger-models/src/main/java/io/swagger/v3/oas/models/media/BinarySchema.java b/modules/swagger-models/src/main/java/io/swagger/v3/oas/models/media/BinarySchema.java index 627927da61..2ad6c24d2d 100644 --- a/modules/swagger-models/src/main/java/io/swagger/v3/oas/models/media/BinarySchema.java +++ b/modules/swagger-models/src/main/java/io/swagger/v3/oas/models/media/BinarySchema.java @@ -1,5 +1,6 @@ package io.swagger.v3.oas.models.media; +import java.util.Base64; import java.util.List; import java.util.Objects; @@ -36,6 +37,13 @@ protected byte[] cast(Object value) { try { if (value instanceof byte[]) { return (byte[]) value; + } else if (value instanceof String) { + if ( + (System.getProperty(BINARY_STRING_CONVERSION_PROPERTY) != null && System.getProperty(BINARY_STRING_CONVERSION_PROPERTY).equals(BynaryStringConversion.BINARY_STRING_CONVERSION_BASE64.toString())) || + (System.getenv(BINARY_STRING_CONVERSION_PROPERTY) != null && System.getenv(BINARY_STRING_CONVERSION_PROPERTY).equals(BynaryStringConversion.BINARY_STRING_CONVERSION_BASE64.toString()))) { + return Base64.getDecoder().decode((String) value); + } + return value.toString().getBytes(); } else { return value.toString().getBytes(); } diff --git a/modules/swagger-models/src/main/java/io/swagger/v3/oas/models/media/ByteArraySchema.java b/modules/swagger-models/src/main/java/io/swagger/v3/oas/models/media/ByteArraySchema.java index d27a55799f..abfc58501e 100644 --- a/modules/swagger-models/src/main/java/io/swagger/v3/oas/models/media/ByteArraySchema.java +++ b/modules/swagger-models/src/main/java/io/swagger/v3/oas/models/media/ByteArraySchema.java @@ -1,5 +1,6 @@ package io.swagger.v3.oas.models.media; +import java.util.Base64; import java.util.List; import java.util.Objects; @@ -36,6 +37,13 @@ protected byte[] cast(Object value) { try { if (value instanceof byte[]) { return (byte[]) value; + } else if (value instanceof String) { + if ( + (System.getProperty(BINARY_STRING_CONVERSION_PROPERTY) != null && System.getProperty(BINARY_STRING_CONVERSION_PROPERTY).equals(BynaryStringConversion.BINARY_STRING_CONVERSION_BASE64.toString())) || + (System.getenv(BINARY_STRING_CONVERSION_PROPERTY) != null && System.getenv(BINARY_STRING_CONVERSION_PROPERTY).equals(BynaryStringConversion.BINARY_STRING_CONVERSION_BASE64.toString()))) { + return Base64.getDecoder().decode((String) value); + } + return value.toString().getBytes(); } else { return value.toString().getBytes(); } diff --git a/modules/swagger-models/src/main/java/io/swagger/v3/oas/models/media/Schema.java b/modules/swagger-models/src/main/java/io/swagger/v3/oas/models/media/Schema.java index c367870b67..8872a74c2c 100644 --- a/modules/swagger-models/src/main/java/io/swagger/v3/oas/models/media/Schema.java +++ b/modules/swagger-models/src/main/java/io/swagger/v3/oas/models/media/Schema.java @@ -27,6 +27,22 @@ public class Schema { public static final String BIND_TYPE_AND_TYPES = "bind-type"; + public static final String BINARY_STRING_CONVERSION_PROPERTY = "binary-string-conversion"; + public enum BynaryStringConversion { + BINARY_STRING_CONVERSION_BASE64("base64"), + BINARY_STRING_CONVERSION_DEFAULT_CHARSET("default"), + BINARY_STRING_CONVERSION_STRING_SCHEMA("string-schema"); + private String value; + + BynaryStringConversion(String value) { + this.value = value; + } + + @Override + public String toString() { + return String.valueOf(value); + } + } protected T _default;