vaadin / hilla

Build better business applications, faster. No more juggling REST endpoints or deciphering GraphQL queries. Hilla seamlessly connects Spring Boot and React to accelerate application development.
https://hilla.dev
Apache License 2.0
923 stars 57 forks source link

ListSignal with Java 8 date/time types fail with Jackson ObjectMapper #2891

Open rbrki07 opened 2 weeks ago

rbrki07 commented 2 weeks ago

Describe the bug

I want to use the new ListSignal with a Java Record that contains a field of type LocalDateTime. This is currently not possible, as a request from the frontend to the backend containing such a field cannot be deserialized due to a Jackson data binding error.

Expected-behavior

I'm able to use the new ListSignal with a Java Record that contains a field of type LocalDateTime, LocalDate or Instant.

Reproduction

import java.time.LocalDateTime;

import com.vaadin.flow.server.auth.AnonymousAllowed; import com.vaadin.hilla.BrowserCallable; import com.vaadin.hilla.signals.ListSignal;

@BrowserCallable @AnonymousAllowed public class ChatService {

public record ChatMessage(String author, String message, LocalDateTime timestamp) { }

private ListSignal<ChatMessage> messages = new ListSignal<>(ChatMessage.class);

public ListSignal<ChatMessage> messages() {
    return messages;
}

}


* Create a `ChatView` like this:
```tsx
import { HorizontalLayout, MessageInput, MessageList, TextField } from "@vaadin/react-components"
import { ChatService } from "Frontend/generated/endpoints"
import { useSignal } from '@vaadin/hilla-react-signals';

const chatMessages = ChatService.messages()

const convertToMessageListItem = (author: string, message: string, timestamp: string) => ({
  text: message,
  time: timestamp,
  userName: author,
})

const ChatView = () => {
    const username = useSignal('')

    return (
        <>
            <MessageList items={chatMessages.value.map((m) => convertToMessageListItem(m.value.message, m.value.author, m.value.timestamp))} />
            <HorizontalLayout theme={'margin'}>
                <TextField placeholder={'Your name'} onValueChanged={(e) => username.value = e.detail.value} className={'items-center'} />
                <MessageInput onSubmit={(e) => chatMessages.insertLast({ author: username.value, message: e.detail.value, timestamp: new Date().toISOString() })} className={'flex-grow'} />
            </HorizontalLayout>
        </>
    )
}

export default ChatView

If you submit a new message to the backend, you will see the following error in the backend logs:

2024-11-02T17:40:22.957+01:00 ERROR 70019 --- [nio-8080-exec-2] com.vaadin.hilla.EndpointInvoker         : Endpoint 'SignalsHandler' method 'update' execution failure

java.lang.reflect.InvocationTargetException: null
        at java.base/jdk.internal.reflect.DirectMethodHandleAccessor.invoke(DirectMethodHandleAccessor.java:118) ~[na:na]
        at java.base/java.lang.reflect.Method.invoke(Method.java:580) ~[na:na]
        at com.vaadin.hilla.EndpointInvoker.invokeVaadinEndpointMethod(EndpointInvoker.java:454) ~[hilla-endpoint-24.6.0.alpha2.jar:na]
        at com.vaadin.hilla.EndpointInvoker.invoke(EndpointInvoker.java:203) ~[hilla-endpoint-24.6.0.alpha2.jar:na]
        at com.vaadin.hilla.EndpointController.doServeEndpoint(EndpointController.java:251) ~[hilla-endpoint-24.6.0.alpha2.jar:na]
        at com.vaadin.hilla.EndpointController.serveEndpoint(EndpointController.java:199) ~[hilla-endpoint-24.6.0.alpha2.jar:na]
        at java.base/jdk.internal.reflect.DirectMethodHandleAccessor.invoke(DirectMethodHandleAccessor.java:103) ~[na:na]
        at java.base/java.lang.reflect.Method.invoke(Method.java:580) ~[na:na]
        at org.springframework.web.method.support.InvocableHandlerMethod.doInvoke(InvocableHandlerMethod.java:255) ~[spring-web-6.1.13.jar:6.1.13]
        at org.springframework.web.method.support.InvocableHandlerMethod.invokeForRequest(InvocableHandlerMethod.java:188) ~[spring-web-6.1.13.jar:6.1.13]
        at org.springframework.web.servlet.mvc.method.annotation.ServletInvocableHandlerMethod.invokeAndHandle(ServletInvocableHandlerMethod.java:118) ~[spring-webmvc-6.1.13.jar:6.1.13]
        at org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter.invokeHandlerMethod(RequestMappingHandlerAdapter.java:926) ~[spring-webmvc-6.1.13.jar:6.1.13]
        at org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter.handleInternal(RequestMappingHandlerAdapter.java:831) ~[spring-webmvc-6.1.13.jar:6.1.13]
        at org.springframework.web.servlet.mvc.method.AbstractHandlerMethodAdapter.handle(AbstractHandlerMethodAdapter.java:87) ~[spring-webmvc-6.1.13.jar:6.1.13]
        at org.springframework.web.servlet.DispatcherServlet.doDispatch(DispatcherServlet.java:1089) ~[spring-webmvc-6.1.13.jar:6.1.13]
        at org.springframework.web.servlet.DispatcherServlet.doService(DispatcherServlet.java:979) ~[spring-webmvc-6.1.13.jar:6.1.13]
        at org.springframework.web.servlet.FrameworkServlet.processRequest(FrameworkServlet.java:1014) ~[spring-webmvc-6.1.13.jar:6.1.13]
        at org.springframework.web.servlet.FrameworkServlet.doPost(FrameworkServlet.java:914) ~[spring-webmvc-6.1.13.jar:6.1.13]
        at jakarta.servlet.http.HttpServlet.service(HttpServlet.java:590) ~[tomcat-embed-core-10.1.30.jar:6.0]
        at org.springframework.web.servlet.FrameworkServlet.service(FrameworkServlet.java:885) ~[spring-webmvc-6.1.13.jar:6.1.13]
        at jakarta.servlet.http.HttpServlet.service(HttpServlet.java:658) ~[tomcat-embed-core-10.1.30.jar:6.0]
        at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:195) ~[tomcat-embed-core-10.1.30.jar:10.1.30]
        at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:140) ~[tomcat-embed-core-10.1.30.jar:10.1.30]
        at org.apache.tomcat.websocket.server.WsFilter.doFilter(WsFilter.java:51) ~[tomcat-embed-websocket-10.1.30.jar:10.1.30]
        at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:164) ~[tomcat-embed-core-10.1.30.jar:10.1.30]
        at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:140) ~[tomcat-embed-core-10.1.30.jar:10.1.30]
        at org.springframework.web.filter.RequestContextFilter.doFilterInternal(RequestContextFilter.java:100) ~[spring-web-6.1.13.jar:6.1.13]
        at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:116) ~[spring-web-6.1.13.jar:6.1.13]
        at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:164) ~[tomcat-embed-core-10.1.30.jar:10.1.30]
        at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:140) ~[tomcat-embed-core-10.1.30.jar:10.1.30]
        at org.springframework.web.filter.FormContentFilter.doFilterInternal(FormContentFilter.java:93) ~[spring-web-6.1.13.jar:6.1.13]
        at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:116) ~[spring-web-6.1.13.jar:6.1.13]
        at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:164) ~[tomcat-embed-core-10.1.30.jar:10.1.30]
        at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:140) ~[tomcat-embed-core-10.1.30.jar:10.1.30]
        at org.springframework.web.filter.CharacterEncodingFilter.doFilterInternal(CharacterEncodingFilter.java:201) ~[spring-web-6.1.13.jar:6.1.13]
        at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:116) ~[spring-web-6.1.13.jar:6.1.13]
        at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:164) ~[tomcat-embed-core-10.1.30.jar:10.1.30]
        at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:140) ~[tomcat-embed-core-10.1.30.jar:10.1.30]
        at org.apache.catalina.core.StandardWrapperValve.invoke(StandardWrapperValve.java:167) ~[tomcat-embed-core-10.1.30.jar:10.1.30]
        at org.apache.catalina.core.StandardContextValve.invoke(StandardContextValve.java:90) ~[tomcat-embed-core-10.1.30.jar:10.1.30]
        at org.apache.catalina.authenticator.AuthenticatorBase.invoke(AuthenticatorBase.java:483) ~[tomcat-embed-core-10.1.30.jar:10.1.30]
        at org.apache.catalina.core.StandardHostValve.invoke(StandardHostValve.java:115) ~[tomcat-embed-core-10.1.30.jar:10.1.30]
        at org.apache.catalina.valves.ErrorReportValve.invoke(ErrorReportValve.java:93) ~[tomcat-embed-core-10.1.30.jar:10.1.30]
        at org.apache.catalina.core.StandardEngineValve.invoke(StandardEngineValve.java:74) ~[tomcat-embed-core-10.1.30.jar:10.1.30]
        at org.apache.catalina.connector.CoyoteAdapter.service(CoyoteAdapter.java:344) ~[tomcat-embed-core-10.1.30.jar:10.1.30]
        at org.apache.coyote.http11.Http11Processor.service(Http11Processor.java:384) ~[tomcat-embed-core-10.1.30.jar:10.1.30]
        at org.apache.coyote.AbstractProcessorLight.process(AbstractProcessorLight.java:63) ~[tomcat-embed-core-10.1.30.jar:10.1.30]
        at org.apache.coyote.AbstractProtocol$ConnectionHandler.process(AbstractProtocol.java:905) ~[tomcat-embed-core-10.1.30.jar:10.1.30]
        at org.apache.tomcat.util.net.NioEndpoint$SocketProcessor.doRun(NioEndpoint.java:1741) ~[tomcat-embed-core-10.1.30.jar:10.1.30]
        at org.apache.tomcat.util.net.SocketProcessorBase.run(SocketProcessorBase.java:52) ~[tomcat-embed-core-10.1.30.jar:10.1.30]
        at org.apache.tomcat.util.threads.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1190) ~[tomcat-embed-core-10.1.30.jar:10.1.30]
        at org.apache.tomcat.util.threads.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:659) ~[tomcat-embed-core-10.1.30.jar:10.1.30]
        at org.apache.tomcat.util.threads.TaskThread$WrappingRunnable.run(TaskThread.java:63) ~[tomcat-embed-core-10.1.30.jar:10.1.30]
        at java.base/java.lang.Thread.run(Thread.java:1583) ~[na:na]
Caused by: java.lang.IllegalArgumentException: Java 8 date/time type `java.time.LocalDateTime` not supported by default: add Module "com.fasterxml.jackson.datatype:jackson-datatype-jsr310" to enable handling
 at [Source: UNKNOWN; byte offset: #UNKNOWN] (through reference chain: com.example.application.services.ChatService$ChatMessage["timestamp"])
        at com.fasterxml.jackson.databind.ObjectMapper._convert(ObjectMapper.java:4624) ~[jackson-databind-2.17.2.jar:2.17.2]
        at com.fasterxml.jackson.databind.ObjectMapper.convertValue(ObjectMapper.java:4555) ~[jackson-databind-2.17.2.jar:2.17.2]
        at com.vaadin.hilla.signals.core.event.StateEvent.convertValue(StateEvent.java:108) ~[hilla-endpoint-24.6.0.alpha2.jar:na]
        at com.vaadin.hilla.signals.core.event.ListStateEvent.<init>(ListStateEvent.java:108) ~[hilla-endpoint-24.6.0.alpha2.jar:na]
        at com.vaadin.hilla.signals.ListSignal.processEvent(ListSignal.java:130) ~[hilla-endpoint-24.6.0.alpha2.jar:na]
        at com.vaadin.hilla.signals.Signal.submit(Signal.java:100) ~[hilla-endpoint-24.6.0.alpha2.jar:na]
        at com.vaadin.hilla.signals.ListSignal.submit(ListSignal.java:113) ~[hilla-endpoint-24.6.0.alpha2.jar:na]
        at com.vaadin.hilla.signals.handler.SignalsHandler.update(SignalsHandler.java:97) ~[hilla-endpoint-24.6.0.alpha2.jar:na]
        at java.base/jdk.internal.reflect.DirectMethodHandleAccessor.invoke(DirectMethodHandleAccessor.java:103) ~[na:na]
        ... 53 common frames omitted
Caused by: com.fasterxml.jackson.databind.exc.InvalidDefinitionException: Java 8 date/time type `java.time.LocalDateTime` not supported by default: add Module "com.fasterxml.jackson.datatype:jackson-datatype-jsr310" to enable handling
 at [Source: UNKNOWN; byte offset: #UNKNOWN] (through reference chain: com.example.application.services.ChatService$ChatMessage["timestamp"])
        at com.fasterxml.jackson.databind.exc.InvalidDefinitionException.from(InvalidDefinitionException.java:67) ~[jackson-databind-2.17.2.jar:2.17.2]
        at com.fasterxml.jackson.databind.DeserializationContext.reportBadDefinition(DeserializationContext.java:1887) ~[jackson-databind-2.17.2.jar:2.17.2]
        at com.fasterxml.jackson.databind.deser.impl.UnsupportedTypeDeserializer.deserialize(UnsupportedTypeDeserializer.java:48) ~[jackson-databind-2.17.2.jar:2.17.2]
        at com.fasterxml.jackson.databind.deser.SettableBeanProperty.deserialize(SettableBeanProperty.java:545) ~[jackson-databind-2.17.2.jar:2.17.2]
        at com.fasterxml.jackson.databind.deser.BeanDeserializer._deserializeWithErrorWrapping(BeanDeserializer.java:576) ~[jackson-databind-2.17.2.jar:2.17.2]
        at com.fasterxml.jackson.databind.deser.BeanDeserializer._deserializeUsingPropertyBased(BeanDeserializer.java:446) ~[jackson-databind-2.17.2.jar:2.17.2]
        at com.fasterxml.jackson.databind.deser.BeanDeserializerBase.deserializeFromObjectUsingNonDefault(BeanDeserializerBase.java:1493) ~[jackson-databind-2.17.2.jar:2.17.2]
        at com.fasterxml.jackson.databind.deser.BeanDeserializer.deserializeFromObject(BeanDeserializer.java:348) ~[jackson-databind-2.17.2.jar:2.17.2]
        at com.fasterxml.jackson.databind.deser.BeanDeserializer.deserialize(BeanDeserializer.java:185) ~[jackson-databind-2.17.2.jar:2.17.2]
        at com.fasterxml.jackson.databind.ObjectMapper._convert(ObjectMapper.java:4619) ~[jackson-databind-2.17.2.jar:2.17.2]
        ... 61 common frames omitted

System Info

24.6.0.alpha2

platosha commented 2 weeks ago

We need to make sure that the correct Jackson mapper is used. One issue is that it's difficult to share the mapper for the signals constructor. Related issue: #2786, one approach to fix there relies on JSON mapper.

platosha commented 2 weeks ago

Let's expose a static method in the signals library that enables setting the Jackson ObjectMapper. Before the call, the built-in mapper is null, so any interaction with it will throw.

Hilla automaticalliy initializes signals using the same mapper with regular endpoint requests.