diff --git a/asyncapi-core/src/main/java/com/asyncapi/v2/jackson/SchemaItemsDeserializer.java b/asyncapi-core/src/main/java/com/asyncapi/v2/jackson/SchemaItemsDeserializer.java new file mode 100644 index 00000000..4b90358f --- /dev/null +++ b/asyncapi-core/src/main/java/com/asyncapi/v2/jackson/SchemaItemsDeserializer.java @@ -0,0 +1,51 @@ +package com.asyncapi.v2.jackson; + +import com.asyncapi.v2.schema.Schema; +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.core.ObjectCodec; +import com.fasterxml.jackson.databind.DeserializationContext; +import com.fasterxml.jackson.databind.JsonDeserializer; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.node.ArrayNode; +import com.fasterxml.jackson.databind.node.JsonNodeType; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; + +public class SchemaItemsDeserializer extends JsonDeserializer { + + @Override + public Object deserialize(JsonParser jsonParser, DeserializationContext deserializationContext) throws IOException { + ObjectCodec objectCodec = jsonParser.getCodec(); + JsonNode node = objectCodec.readTree(jsonParser); + JsonNodeType nodeType = node.getNodeType(); + if (nodeType == JsonNodeType.OBJECT) { + return readAsSchema(node, objectCodec); + } + if (nodeType == JsonNodeType.ARRAY) { + return readAsListOfSchemas((ArrayNode) node, objectCodec); + } + return readAsObject(node, objectCodec); + } + + private List readAsListOfSchemas(ArrayNode arrayNode, ObjectCodec objectCodec) throws IOException { + List schemaList = new ArrayList<>(); + for (JsonNode childNode : arrayNode) { + schemaList.add(readAsSchema(childNode, objectCodec)); + } + return schemaList; + } + + private Schema readAsSchema(JsonNode jsonNode, ObjectCodec objectCodec) throws IOException { + try (JsonParser parser = jsonNode.traverse(objectCodec)) { + return parser.readValueAs(Schema.class); + } + } + + private Object readAsObject(JsonNode jsonNode, ObjectCodec objectCodec) throws IOException { + try (JsonParser jsonParser = jsonNode.traverse(objectCodec)) { + return jsonParser.readValueAs(Object.class); + } + } +} diff --git a/asyncapi-core/src/main/java/com/asyncapi/v2/schema/Schema.java b/asyncapi-core/src/main/java/com/asyncapi/v2/schema/Schema.java index aa99503c..c4937bc4 100644 --- a/asyncapi-core/src/main/java/com/asyncapi/v2/schema/Schema.java +++ b/asyncapi-core/src/main/java/com/asyncapi/v2/schema/Schema.java @@ -3,6 +3,7 @@ import com.asyncapi.v2.ExtendableObject; import com.asyncapi.v2._0_0.jackson.model.schema.SchemasAdditionalPropertiesDeserializer; import com.asyncapi.v2._0_0.model.ExternalDocumentation; +import com.asyncapi.v2.jackson.SchemaItemsDeserializer; import com.fasterxml.jackson.annotation.JsonProperty; import com.fasterxml.jackson.databind.annotation.JsonDeserialize; import lombok.AllArgsConstructor; @@ -362,7 +363,7 @@ Validation Keywords for Numeric Instances (number and integer) * Omitting this keyword has the same behavior as an empty schema. */ @Nullable - @JsonProperty + @JsonDeserialize(using = SchemaItemsDeserializer.class) public Object items; /** diff --git a/asyncapi-core/src/main/java/com/asyncapi/v3/jackson/SchemaItemsDeserializer.java b/asyncapi-core/src/main/java/com/asyncapi/v3/jackson/SchemaItemsDeserializer.java new file mode 100644 index 00000000..c2186164 --- /dev/null +++ b/asyncapi-core/src/main/java/com/asyncapi/v3/jackson/SchemaItemsDeserializer.java @@ -0,0 +1,51 @@ +package com.asyncapi.v3.jackson; + +import com.asyncapi.v3.schema.Schema; +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.core.ObjectCodec; +import com.fasterxml.jackson.databind.DeserializationContext; +import com.fasterxml.jackson.databind.JsonDeserializer; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.node.ArrayNode; +import com.fasterxml.jackson.databind.node.JsonNodeType; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; + +public class SchemaItemsDeserializer extends JsonDeserializer { + + @Override + public Object deserialize(JsonParser jsonParser, DeserializationContext deserializationContext) throws IOException { + ObjectCodec objectCodec = jsonParser.getCodec(); + JsonNode node = objectCodec.readTree(jsonParser); + JsonNodeType nodeType = node.getNodeType(); + if (nodeType == JsonNodeType.OBJECT) { + return readAsSchema(node, objectCodec); + } + if (nodeType == JsonNodeType.ARRAY) { + return readAsListOfSchemas((ArrayNode) node, objectCodec); + } + return readAsObject(node, objectCodec); + } + + private List readAsListOfSchemas(ArrayNode arrayNode, ObjectCodec objectCodec) throws IOException { + List schemaList = new ArrayList<>(); + for (JsonNode childNode : arrayNode) { + schemaList.add(readAsSchema(childNode, objectCodec)); + } + return schemaList; + } + + private Schema readAsSchema(JsonNode jsonNode, ObjectCodec objectCodec) throws IOException { + try (JsonParser parser = jsonNode.traverse(objectCodec)) { + return parser.readValueAs(Schema.class); + } + } + + private Object readAsObject(JsonNode jsonNode, ObjectCodec objectCodec) throws IOException { + try (JsonParser jsonParser = jsonNode.traverse(objectCodec)) { + return jsonParser.readValueAs(Object.class); + } + } +} diff --git a/asyncapi-core/src/main/java/com/asyncapi/v3/schema/Schema.java b/asyncapi-core/src/main/java/com/asyncapi/v3/schema/Schema.java index 7085d8e1..1fdfc582 100644 --- a/asyncapi-core/src/main/java/com/asyncapi/v3/schema/Schema.java +++ b/asyncapi-core/src/main/java/com/asyncapi/v3/schema/Schema.java @@ -1,5 +1,6 @@ package com.asyncapi.v3.schema; +import com.asyncapi.v3.jackson.SchemaItemsDeserializer; import com.asyncapi.v3.ExtendableObject; import com.asyncapi.v3.jackson.schema.SchemasAdditionalPropertiesDeserializer; import com.asyncapi.v3._0_0.model.ExternalDocumentation; @@ -358,7 +359,7 @@ Validation Keywords for Numeric Instances (number and integer) * Omitting this keyword has the same behavior as an empty schema. */ @Nullable - @JsonProperty + @JsonDeserialize(using = SchemaItemsDeserializer.class) public Object items; /** diff --git a/asyncapi-core/src/test/kotlin/com/asyncapi/v2/_6_0/model/channel/message/MessageWithArrayPayloadTest.kt b/asyncapi-core/src/test/kotlin/com/asyncapi/v2/_6_0/model/channel/message/MessageWithArrayPayloadTest.kt new file mode 100644 index 00000000..200e7eff --- /dev/null +++ b/asyncapi-core/src/test/kotlin/com/asyncapi/v2/_6_0/model/channel/message/MessageWithArrayPayloadTest.kt @@ -0,0 +1,30 @@ +package com.asyncapi.v2._6_0.model.channel.message + +import com.asyncapi.v2.ClasspathUtils +import com.asyncapi.v2.schema.Schema +import com.fasterxml.jackson.databind.ObjectMapper +import org.junit.jupiter.api.DisplayName +import org.junit.jupiter.api.Test +import kotlin.test.assertTrue + +class MessageWithArrayPayloadTest { + private val objectMapper = ObjectMapper() + + @Test + @DisplayName("Test array items property is parsed as a schema object") + fun testArrayItemsPropertyIsParsedAsSchemaObjectWhenItIsASingleJsonSchema() { + val model = ClasspathUtils.readAsString("/json/v2/2.6.0/model/channel/message/messageWithArrayPayloadJsonSchema.json") + val schema = objectMapper.readValue(model, Message::class.java).payload as Schema + assertTrue( + schema.items is Schema + ) + } + + @Test + @DisplayName("Test array items property is parsed as list of schemas") + fun testArrayItemsPropertyIsParsedAsArrayListOfSchemasWhenItIsAnArrayOfSchemas() { + val model = ClasspathUtils.readAsString("/json/v2/2.6.0/model/channel/message/messageWithArrayPayloadArrayOfSchemas.json") + val schema = objectMapper.readValue(model, Message::class.java).payload as Schema + assertTrue(schema.items is ArrayList<*> && (schema.items as ArrayList<*>).all { it is Schema }) + } +} \ No newline at end of file diff --git a/asyncapi-core/src/test/kotlin/com/asyncapi/v3/_0_0/model/channel/message/MessageWithArrayPayloadTest.kt b/asyncapi-core/src/test/kotlin/com/asyncapi/v3/_0_0/model/channel/message/MessageWithArrayPayloadTest.kt new file mode 100644 index 00000000..02825157 --- /dev/null +++ b/asyncapi-core/src/test/kotlin/com/asyncapi/v3/_0_0/model/channel/message/MessageWithArrayPayloadTest.kt @@ -0,0 +1,30 @@ +package com.asyncapi.v3._0_0.model.channel.message + +import com.asyncapi.v3.ClasspathUtils +import com.asyncapi.v3.schema.Schema +import com.fasterxml.jackson.databind.ObjectMapper +import org.junit.jupiter.api.DisplayName +import org.junit.jupiter.api.Test +import kotlin.test.assertTrue + +class MessageWithArrayPayloadTest { + private val objectMapper = ObjectMapper() + + @Test + @DisplayName("Test array items property is parsed as a schema object") + fun testArrayItemsPropertyIsParsedAsSchemaObjectWhenItIsASingleJsonSchema() { + val model = ClasspathUtils.readAsString("/json/v3/3.0.0/model/channel/message/messageWithArrayPayloadJsonSchema.json") + val schema = objectMapper.readValue(model, Message::class.java).payload as Schema + assertTrue( + schema.items is Schema + ) + } + + @Test + @DisplayName("Test array items property is parsed as list of schemas") + fun testArrayItemsPropertyIsParsedAsArrayListOfSchemasWhenItIsAnArrayOfSchemas() { + val model = ClasspathUtils.readAsString("/json/v3/3.0.0/model/channel/message/messageWithArrayPayloadArrayOfSchemas.json") + val schema = objectMapper.readValue(model, Message::class.java).payload as Schema + assertTrue(schema.items is ArrayList<*> && (schema.items as ArrayList<*>).all { it is Schema }) + } +} \ No newline at end of file diff --git a/asyncapi-core/src/test/resources/json/v2/2.6.0/model/channel/message/messageWithArrayPayloadArrayOfSchemas.json b/asyncapi-core/src/test/resources/json/v2/2.6.0/model/channel/message/messageWithArrayPayloadArrayOfSchemas.json new file mode 100644 index 00000000..33dc7425 --- /dev/null +++ b/asyncapi-core/src/test/resources/json/v2/2.6.0/model/channel/message/messageWithArrayPayloadArrayOfSchemas.json @@ -0,0 +1,19 @@ +{ + "bindings": { + "kafka": { + "key": { + "type": "string" + }, + "bindingVersion": "0.4.0" + } + }, + "payload": { + "type": "array", + "items": [ + { "type": "number" }, + { "type": "string" }, + { "enum": ["Street", "Avenue", "Boulevard"] }, + { "enum": ["NW", "NE", "SW", "SE"] } + ] + } +} \ No newline at end of file diff --git a/asyncapi-core/src/test/resources/json/v2/2.6.0/model/channel/message/messageWithArrayPayloadJsonSchema.json b/asyncapi-core/src/test/resources/json/v2/2.6.0/model/channel/message/messageWithArrayPayloadJsonSchema.json new file mode 100644 index 00000000..e8c084f3 --- /dev/null +++ b/asyncapi-core/src/test/resources/json/v2/2.6.0/model/channel/message/messageWithArrayPayloadJsonSchema.json @@ -0,0 +1,32 @@ +{ + "bindings": { + "kafka": { + "key": { + "type": "string" + }, + "bindingVersion": "0.4.0" + } + }, + "payload": { + "type": "array", + "items": { + "type": "object", + "required": [ + "id", + "name", + "done" + ], + "properties": { + "id": { + "type": "integer" + }, + "name": { + "type": "string" + }, + "done": { + "type": "boolean" + } + } + } + } +} \ No newline at end of file diff --git a/asyncapi-core/src/test/resources/json/v3/3.0.0/model/channel/message/messageWithArrayPayloadArrayOfSchemas.json b/asyncapi-core/src/test/resources/json/v3/3.0.0/model/channel/message/messageWithArrayPayloadArrayOfSchemas.json new file mode 100644 index 00000000..33dc7425 --- /dev/null +++ b/asyncapi-core/src/test/resources/json/v3/3.0.0/model/channel/message/messageWithArrayPayloadArrayOfSchemas.json @@ -0,0 +1,19 @@ +{ + "bindings": { + "kafka": { + "key": { + "type": "string" + }, + "bindingVersion": "0.4.0" + } + }, + "payload": { + "type": "array", + "items": [ + { "type": "number" }, + { "type": "string" }, + { "enum": ["Street", "Avenue", "Boulevard"] }, + { "enum": ["NW", "NE", "SW", "SE"] } + ] + } +} \ No newline at end of file diff --git a/asyncapi-core/src/test/resources/json/v3/3.0.0/model/channel/message/messageWithArrayPayloadJsonSchema.json b/asyncapi-core/src/test/resources/json/v3/3.0.0/model/channel/message/messageWithArrayPayloadJsonSchema.json new file mode 100644 index 00000000..e8c084f3 --- /dev/null +++ b/asyncapi-core/src/test/resources/json/v3/3.0.0/model/channel/message/messageWithArrayPayloadJsonSchema.json @@ -0,0 +1,32 @@ +{ + "bindings": { + "kafka": { + "key": { + "type": "string" + }, + "bindingVersion": "0.4.0" + } + }, + "payload": { + "type": "array", + "items": { + "type": "object", + "required": [ + "id", + "name", + "done" + ], + "properties": { + "id": { + "type": "integer" + }, + "name": { + "type": "string" + }, + "done": { + "type": "boolean" + } + } + } + } +} \ No newline at end of file