JsonMapper.isJsonStringRepresentsCollection may return true for non-collection objects if the object implements Iterable. As a concrete example, ObjectNode implements Iterable through its inheritance tree, however, it may not actually represent a collection. This can produce an exception similar to:
java.lang.IllegalStateException: Failed to convert. Possible bug as the conversion probably shouldn't have been attempted here
at org.springframework.cloud.function.json.JacksonMapper.doFromJson(JacksonMapper.java:65) ~[classes/:na]
at org.springframework.cloud.function.json.JsonMapper.fromJson(JsonMapper.java:69) ~[classes/:na]
at org.springframework.cloud.function.context.catalog.SimpleFunctionRegistry$FunctionInvocationWrapper.fluxifyInputIfNecessary(SimpleFunctionRegistry.java:837) ~[classes/:na]
at org.springframework.cloud.function.context.catalog.SimpleFunctionRegistry$FunctionInvocationWrapper.doApply(SimpleFunctionRegistry.java:723) ~[classes/:na]
at org.springframework.cloud.function.context.catalog.SimpleFunctionRegistry$FunctionInvocationWrapper.apply(SimpleFunctionRegistry.java:580) ~[classes/:na]
at org.springframework.cloud.stream.function.StreamBridge.send(StreamBridge.java:177) ~[spring-cloud-stream-4.1.0-20230818.194640-201.jar:4.1.0-SNAPSHOT]
at org.springframework.cloud.stream.function.StreamBridge.send(StreamBridge.java:146) ~[spring-cloud-stream-4.1.0-20230818.194640-201.jar:4.1.0-SNAPSHOT]
at org.springframework.cloud.stream.function.StreamBridge.send(StreamBridge.java:141) ~[spring-cloud-stream-4.1.0-20230818.194640-201.jar:4.1.0-SNAPSHOT]
at io.spring.cloudevent.DemoApplication.hire(DemoApplication.java:33) ~[classes/:na]
at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method) ~[na:na]
at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:77) ~[na:na]
at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) ~[na:na]
at java.base/java.lang.reflect.Method.invoke(Method.java:568) ~[na:na]
at org.springframework.context.event.ApplicationListenerMethodAdapter.doInvoke(ApplicationListenerMethodAdapter.java:348) ~[spring-context-6.1.0-M4.jar:6.1.0-M4]
at org.springframework.context.event.ApplicationListenerMethodAdapter.processEvent(ApplicationListenerMethodAdapter.java:233) ~[spring-context-6.1.0-M4.jar:6.1.0-M4]
at org.springframework.context.event.ApplicationListenerMethodAdapter.onApplicationEvent(ApplicationListenerMethodAdapter.java:165) ~[spring-context-6.1.0-M4.jar:6.1.0-M4]
at org.springframework.context.event.SimpleApplicationEventMulticaster.doInvokeListener(SimpleApplicationEventMulticaster.java:178) ~[spring-context-6.1.0-M4.jar:6.1.0-M4]
at org.springframework.context.event.SimpleApplicationEventMulticaster.invokeListener(SimpleApplicationEventMulticaster.java:171) ~[spring-context-6.1.0-M4.jar:6.1.0-M4]
at org.springframework.context.event.SimpleApplicationEventMulticaster.multicastEvent(SimpleApplicationEventMulticaster.java:149) ~[spring-context-6.1.0-M4.jar:6.1.0-M4]
at org.springframework.context.support.AbstractApplicationContext.publishEvent(AbstractApplicationContext.java:437) ~[spring-context-6.1.0-M4.jar:6.1.0-M4]
at org.springframework.context.support.AbstractApplicationContext.publishEvent(AbstractApplicationContext.java:370) ~[spring-context-6.1.0-M4.jar:6.1.0-M4]
at org.springframework.boot.context.event.EventPublishingRunListener.ready(EventPublishingRunListener.java:109) ~[spring-boot-3.2.0-20230818.132208-219.jar:3.2.0-SNAPSHOT]
at org.springframework.boot.SpringApplicationRunListeners.lambda$ready$6(SpringApplicationRunListeners.java:80) ~[spring-boot-3.2.0-20230818.132208-219.jar:3.2.0-SNAPSHOT]
at java.base/java.lang.Iterable.forEach(Iterable.java:75) ~[na:na]
at org.springframework.boot.SpringApplicationRunListeners.doWithListeners(SpringApplicationRunListeners.java:118) ~[spring-boot-3.2.0-20230818.132208-219.jar:3.2.0-SNAPSHOT]
at org.springframework.boot.SpringApplicationRunListeners.doWithListeners(SpringApplicationRunListeners.java:112) ~[spring-boot-3.2.0-20230818.132208-219.jar:3.2.0-SNAPSHOT]
at org.springframework.boot.SpringApplicationRunListeners.ready(SpringApplicationRunListeners.java:80) ~[spring-boot-3.2.0-20230818.132208-219.jar:3.2.0-SNAPSHOT]
at org.springframework.boot.SpringApplication.run(SpringApplication.java:331) ~[spring-boot-3.2.0-20230818.132208-219.jar:3.2.0-SNAPSHOT]
at org.springframework.boot.SpringApplication.run(SpringApplication.java:1306) ~[spring-boot-3.2.0-20230818.132208-219.jar:3.2.0-SNAPSHOT]
at org.springframework.boot.SpringApplication.run(SpringApplication.java:1295) ~[spring-boot-3.2.0-20230818.132208-219.jar:3.2.0-SNAPSHOT]
at io.spring.cloudevent.DemoApplication.main(DemoApplication.java:19) ~[classes/:na]
Caused by: com.fasterxml.jackson.databind.exc.MismatchedInputException: Cannot deserialize value of type `java.util.ArrayList<java.lang.Object>` from Object value (token `JsonToken.START_OBJECT`)
at [Source: (byte[])"{"id":"1234ab","foo":"bar"}"; line: 1, column: 1]
at com.fasterxml.jackson.databind.exc.MismatchedInputException.from(MismatchedInputException.java:59) ~[jackson-databind-2.15.2.jar:2.15.2]
at com.fasterxml.jackson.databind.DeserializationContext.reportInputMismatch(DeserializationContext.java:1752) ~[jackson-databind-2.15.2.jar:2.15.2]
at com.fasterxml.jackson.databind.DeserializationContext.handleUnexpectedToken(DeserializationContext.java:1526) ~[jackson-databind-2.15.2.jar:2.15.2]
at com.fasterxml.jackson.databind.DeserializationContext.handleUnexpectedToken(DeserializationContext.java:1473) ~[jackson-databind-2.15.2.jar:2.15.2]
at com.fasterxml.jackson.databind.deser.std.CollectionDeserializer.handleNonArray(CollectionDeserializer.java:396) ~[jackson-databind-2.15.2.jar:2.15.2]
at com.fasterxml.jackson.databind.deser.std.CollectionDeserializer.deserialize(CollectionDeserializer.java:252) ~[jackson-databind-2.15.2.jar:2.15.2]
at com.fasterxml.jackson.databind.deser.std.CollectionDeserializer.deserialize(CollectionDeserializer.java:28) ~[jackson-databind-2.15.2.jar:2.15.2]
at com.fasterxml.jackson.databind.deser.DefaultDeserializationContext.readRootValue(DefaultDeserializationContext.java:323) ~[jackson-databind-2.15.2.jar:2.15.2]
at com.fasterxml.jackson.databind.ObjectMapper._readMapAndClose(ObjectMapper.java:4825) ~[jackson-databind-2.15.2.jar:2.15.2]
at com.fasterxml.jackson.databind.ObjectMapper.readValue(ObjectMapper.java:3866) ~[jackson-databind-2.15.2.jar:2.15.2]
at org.springframework.cloud.function.json.JacksonMapper.doFromJson(JacksonMapper.java:55) ~[classes/:na]
... 30 common frames omitted
This appears to relate to the commit f45131f0f040a3f4da18967e74f1ad23c952fd2c, which was made in response to the issue https://github.com/spring-cloud/spring-cloud-function/issues/1052. As part of the change made, an instanceof Iterable check was introduced, which in a case such as with ObjectNode, will result in true being returned, whereas the previous behaviour will have returned false.
As a workaround, invoking toString() and sending the stringified node as the payload will bypass the iterable check while returning the expected response.
Sample
For what it is worth, I added a small test case that invokes JsonMapper directly to demonstrate the inconsistency of the collection identification of an ObjectNode. If necessary, one of the demo applications is easy to convert to show the behaviour from a streamBridge.send call.
Describe the bug Using:
JsonMapper.isJsonStringRepresentsCollection
may return true for non-collection objects if the object implementsIterable
. As a concrete example,ObjectNode
implementsIterable
through its inheritance tree, however, it may not actually represent a collection. This can produce an exception similar to:from an invocation of
StreamBridge
such as:This appears to relate to the commit f45131f0f040a3f4da18967e74f1ad23c952fd2c, which was made in response to the issue https://github.com/spring-cloud/spring-cloud-function/issues/1052. As part of the change made, an
instanceof Iterable
check was introduced, which in a case such as withObjectNode
, will result intrue
being returned, whereas the previous behaviour will have returnedfalse
.As a workaround, invoking
toString()
and sending the stringified node as the payload will bypass the iterable check while returning the expected response.Sample For what it is worth, I added a small test case that invokes
JsonMapper
directly to demonstrate the inconsistency of the collection identification of anObjectNode
. If necessary, one of the demo applications is easy to convert to show the behaviour from astreamBridge.send
call.https://github.com/k-seth/spring-cloud-function/blob/object-node-represents-collection/spring-cloud-function-context/src/test/java/org/springframework/cloud/function/json/JsonMapperTest.java#11