spring-cloud / spring-cloud-function

Apache License 2.0
1.04k stars 615 forks source link

JsonMapper.isJsonStringRepresentsCollection may incorrectly identify a collection based on the Iterable interface #1065

Open k-seth opened 1 year ago

k-seth commented 1 year ago

Describe the bug Using:

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

from an invocation of StreamBridge such as:

ObjectNode node = JsonNodeFactory.instance.objectNode();
node.put("id", "1234ab");
node.put("foo", "bar");

streamBridge.send("example-out-0", node);

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.

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

rogeriolino commented 11 months ago

hi @olegz, is there any plan to release this fix in v4.0? thanks

sobychacko commented 10 months ago

@olegz I don't think we have back-ported this to 4.0.x yet. This issue in Spring Cloud Stream is due to this fix missing in 4.0.x.