Skip to content

Commit d0b0bb1

Browse files
authored
feat: add TypeReference support for generic type deserialization across all JSON APIs (#22442)
Add TypeReference<T> parameter overloads to enable proper deserialization of generic collections like List<Bean> and Map<String, Bean> across all Flow JSON APIs. This eliminates the need for unsafe casting when working with generic return types from JavaScript execution, property access, and event data handling. Key changes: - Add JacksonCodec.decodeAs(JsonNode, TypeReference<T>) for core deserialization with full generic type information preservation - Add PendingJavaScriptResult.then/toCompletableFuture TypeReference overloads for executeJs() return value handling - Add Element.getProperty(String, TypeReference<T>) for typed property access with generic collections - Add DomEvent.getEventData(TypeReference<T>) for typed event data extraction - Add DomListenerRegistration.addEventData(Class/TypeReference) overloads that automatically introspect bean/record structures and register all properties - Add BeanUtil.getBeanPropertyPaths() utility for extracting property paths from beans/records using Java Bean introspection - Update @eventdata javadoc to document existing bean/record support
1 parent 121d791 commit d0b0bb1

File tree

11 files changed

+853
-4
lines changed

11 files changed

+853
-4
lines changed

flow-server/src/main/java/com/vaadin/flow/component/EventData.java

Lines changed: 31 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -37,10 +37,37 @@
3737
* expression is passed back to the server and injected into the annotated
3838
* {@link ComponentEvent} constructor parameter.
3939
* <p>
40-
* Supported parameter types are {@link String},
41-
* {@link elemental.json.JsonValue}, {@link tools.jackson.databind.JsonNode},
42-
* {@link Integer}, {@link Double}, {@link Boolean} and their respective
43-
* primitive types.
40+
* Supported parameter types include:
41+
* <ul>
42+
* <li>Primitives and their wrappers: {@link Integer}, {@link Double},
43+
* {@link Boolean}, int, double, boolean, etc.</li>
44+
* <li>String values: {@link String}</li>
45+
* <li>JSON types: {@link tools.jackson.databind.JsonNode},
46+
* {@link elemental.json.JsonValue}</li>
47+
* <li>Bean/DTO types: Any Java bean or record that can be deserialized from
48+
* JSON using Jackson</li>
49+
* <li>Collections: {@link java.util.List}, {@link java.util.Map}, etc. (when
50+
* using generic types with proper bean definitions)</li>
51+
* </ul>
52+
* <p>
53+
* Example with a bean type:
54+
*
55+
* <pre>
56+
* public class MouseDetails {
57+
* private int clientX;
58+
* private int clientY;
59+
* // getters and setters
60+
* }
61+
*
62+
* &#64;DomEvent("custom-click")
63+
* public class CustomClickEvent extends ComponentEvent&lt;Component&gt; {
64+
* public CustomClickEvent(Component source, boolean fromClient,
65+
* &#64;EventData("event.detail") MouseDetails details) {
66+
* super(source, fromClient);
67+
* // details is automatically deserialized from the event data
68+
* }
69+
* }
70+
* </pre>
4471
*
4572
* @see DomEvent
4673
* @see DomListenerRegistration#addEventData(String)

flow-server/src/main/java/com/vaadin/flow/component/page/PendingJavaScriptResult.java

Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
import java.io.Serializable;
1919
import java.util.concurrent.CompletableFuture;
2020

21+
import tools.jackson.core.type.TypeReference;
2122
import tools.jackson.databind.JsonNode;
2223

2324
import com.vaadin.flow.component.internal.DeadlockDetectingCompletableFuture;
@@ -195,6 +196,123 @@ default <T> CompletableFuture<T> toCompletableFuture(Class<T> targetType) {
195196
return completableFuture;
196197
}
197198

199+
/**
200+
* Adds a typed handler that will be run for a successful execution and a
201+
* handler that will be run for a failed execution. One of the handlers will
202+
* be invoked asynchronously when the result of the execution is sent back
203+
* to the server.
204+
* <p>
205+
* The JavaScript return value will be automatically converted to the type
206+
* specified by the {@link TypeReference}. This method supports generic
207+
* types such as {@code List<MyBean>} and {@code Map<String, MyBean>}.
208+
* <p>
209+
* Example usage:
210+
*
211+
* <pre>
212+
* element.executeJs("return [{name: 'Alice', age: 30}]")
213+
* .then(new TypeReference&lt;List&lt;Person&gt;&gt;() {
214+
* }, list -&gt; System.out.println(list.get(0).name));
215+
* </pre>
216+
* <p>
217+
* Handlers can only be added before the execution has been sent to the
218+
* browser.
219+
*
220+
* @param <T>
221+
* the type to convert the result to
222+
* @param typeReference
223+
* the type reference describing the target type, not
224+
* <code>null</code>
225+
* @param resultHandler
226+
* a handler for the return value from a successful execution,
227+
* not <code>null</code>
228+
* @param errorHandler
229+
* a handler for an error message in case the execution failed,
230+
* or <code>null</code> to ignore errors
231+
*/
232+
default <T> void then(TypeReference<T> typeReference,
233+
SerializableConsumer<T> resultHandler,
234+
SerializableConsumer<String> errorHandler) {
235+
if (typeReference == null) {
236+
throw new IllegalArgumentException("Type reference cannot be null");
237+
}
238+
if (resultHandler == null) {
239+
throw new IllegalArgumentException("Result handler cannot be null");
240+
}
241+
242+
SerializableConsumer<JsonNode> convertingResultHandler = value -> resultHandler
243+
.accept(JacksonCodec.decodeAs(value, typeReference));
244+
245+
then(convertingResultHandler, errorHandler);
246+
}
247+
248+
/**
249+
* Adds a typed handler that will be run for a successful execution. The
250+
* handler will be invoked asynchronously if the execution was successful.
251+
* In case of a failure, no handler will be run.
252+
* <p>
253+
* The JavaScript return value will be automatically converted to the type
254+
* specified by the {@link TypeReference}. This method supports generic
255+
* types such as {@code List<MyBean>} and {@code Map<String, MyBean>}.
256+
* <p>
257+
* A handler can only be added before the execution has been sent to the
258+
* browser.
259+
*
260+
* @param <T>
261+
* the type to convert the result to
262+
* @param typeReference
263+
* the type reference describing the target type, not
264+
* <code>null</code>
265+
* @param resultHandler
266+
* a handler for the return value from a successful execution,
267+
* not <code>null</code>
268+
*/
269+
default <T> void then(TypeReference<T> typeReference,
270+
SerializableConsumer<T> resultHandler) {
271+
then(typeReference, resultHandler, null);
272+
}
273+
274+
/**
275+
* Creates a typed completable future that will be completed with the result
276+
* of the execution. It will be completed asynchronously when the result of
277+
* the execution is sent back to the server.
278+
* <p>
279+
* The JavaScript return value will be automatically converted to the type
280+
* specified by the {@link TypeReference}. This method supports generic
281+
* types such as {@code List<MyBean>} and {@code Map<String, MyBean>}.
282+
* <p>
283+
* A completable future can only be created before the execution has been
284+
* sent to the browser.
285+
*
286+
* @param <T>
287+
* the type to convert the result to
288+
* @param typeReference
289+
* the type reference describing the target type, not
290+
* <code>null</code>
291+
* @return a completable future that will be completed based on the
292+
* execution results, not <code>null</code>
293+
*/
294+
default <T> CompletableFuture<T> toCompletableFuture(
295+
TypeReference<T> typeReference) {
296+
if (typeReference == null) {
297+
throw new IllegalArgumentException("Type reference cannot be null");
298+
}
299+
300+
VaadinSession session = VaadinSession.getCurrent();
301+
302+
CompletableFuture<T> completableFuture = new DeadlockDetectingCompletableFuture<>(
303+
session);
304+
305+
then(value -> {
306+
T convertedValue = JacksonCodec.decodeAs(value, typeReference);
307+
completableFuture.complete(convertedValue);
308+
}, errorValue -> {
309+
JavaScriptException exception = new JavaScriptException(errorValue);
310+
completableFuture.completeExceptionally(exception);
311+
});
312+
313+
return completableFuture;
314+
}
315+
198316
/**
199317
* Adds an untyped handler that will be run for a successful execution and a
200318
* handler that will be run for a failed execution. One of the handlers will

flow-server/src/main/java/com/vaadin/flow/dom/DomEvent.java

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,8 +21,10 @@
2121
import java.util.concurrent.atomic.AtomicReference;
2222
import java.util.function.Consumer;
2323

24+
import tools.jackson.core.type.TypeReference;
2425
import tools.jackson.databind.JsonNode;
2526

27+
import com.vaadin.flow.internal.JacksonCodec;
2628
import com.vaadin.flow.internal.NodeOwner;
2729
import com.vaadin.flow.internal.StateNode;
2830
import com.vaadin.flow.internal.StateTree;
@@ -157,6 +159,54 @@ public JsonNode getEventData() {
157159
return eventData;
158160
}
159161

162+
/**
163+
* Gets the event data deserialized as the given type. This method supports
164+
* arbitrary bean types through Jackson deserialization.
165+
* <p>
166+
* Example usage:
167+
*
168+
* <pre>
169+
* MyDto dto = domEvent.getEventData(MyDto.class);
170+
* </pre>
171+
*
172+
* @param <T>
173+
* the type to deserialize to
174+
* @param type
175+
* the class to deserialize the event data to, not
176+
* <code>null</code>
177+
* @return the event data deserialized as the given type
178+
* @see DomListenerRegistration#addEventData(String)
179+
*/
180+
public <T> T getEventData(Class<T> type) {
181+
return JacksonCodec.decodeAs(eventData, type);
182+
}
183+
184+
/**
185+
* Gets the event data deserialized as the type specified by the
186+
* {@link TypeReference}. This method supports generic types such as
187+
* {@code List<MyBean>} and {@code Map<String, MyBean>} through Jackson's
188+
* TypeReference mechanism.
189+
* <p>
190+
* Example usage:
191+
*
192+
* <pre>
193+
* TypeReference&lt;List&lt;MyDto&gt;&gt; typeRef = new TypeReference&lt;List&lt;MyDto&gt;&gt;() {
194+
* };
195+
* List&lt;MyDto&gt; dtos = domEvent.getEventData(typeRef);
196+
* </pre>
197+
*
198+
* @param <T>
199+
* the type to deserialize to
200+
* @param typeReference
201+
* the type reference describing the target type, not
202+
* <code>null</code>
203+
* @return the event data deserialized as the given type
204+
* @see DomListenerRegistration#addEventData(String)
205+
*/
206+
public <T> T getEventData(TypeReference<T> typeReference) {
207+
return JacksonCodec.decodeAs(eventData, typeReference);
208+
}
209+
160210
/**
161211
* Gets the debounce phase for which this event is fired. This is used
162212
* internally to only deliver the event to the appropriate listener in cases

flow-server/src/main/java/com/vaadin/flow/dom/DomListenerRegistration.java

Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,10 +15,15 @@
1515
*/
1616
package com.vaadin.flow.dom;
1717

18+
import java.beans.IntrospectionException;
19+
import java.util.List;
1820
import java.util.Objects;
1921
import java.util.Set;
2022

23+
import tools.jackson.core.type.TypeReference;
24+
2125
import com.vaadin.flow.function.SerializableRunnable;
26+
import com.vaadin.flow.internal.BeanUtil;
2227
import com.vaadin.flow.shared.JsonConstants;
2328
import com.vaadin.flow.shared.Registration;
2429

@@ -399,6 +404,103 @@ default public DomListenerRegistration preventDefault() {
399404
return this;
400405
}
401406

407+
/**
408+
* Automatically adds event data expressions for all properties of the given
409+
* class. This method uses Java Bean introspection to discover the
410+
* properties of the class and adds event data expressions for each field
411+
* path.
412+
* <p>
413+
* This is particularly useful with Java records that mirror the JavaScript
414+
* event structure. For example:
415+
*
416+
* <pre>
417+
* record MouseEventData(EventDetails event) {
418+
* record EventDetails(int button, int clientX, int clientY) {
419+
* }
420+
* }
421+
*
422+
* element.addEventListener("click", e -&gt; {
423+
* MouseEventData data = e.getEventData(MouseEventData.class);
424+
* System.out.println("Button: " + data.event().button());
425+
* }).addEventData(MouseEventData.class);
426+
* </pre>
427+
*
428+
* The above will automatically add event data expressions for
429+
* {@code event.button}, {@code event.clientX}, {@code event.clientY}, and
430+
* {@code type}.
431+
*
432+
* @param type
433+
* the class whose properties should be captured from the event
434+
* data, not <code>null</code>
435+
* @return this registration, for chaining
436+
* @see #addEventData(String)
437+
*/
438+
default DomListenerRegistration addEventData(Class<?> type) {
439+
if (type == null) {
440+
throw new IllegalArgumentException("Type cannot be null");
441+
}
442+
try {
443+
List<String> propertyPaths = BeanUtil.getBeanPropertyPaths(type);
444+
for (String path : propertyPaths) {
445+
addEventData(path);
446+
}
447+
} catch (IntrospectionException e) {
448+
throw new IllegalArgumentException(
449+
"Failed to introspect type: " + type.getName(), e);
450+
}
451+
return this;
452+
}
453+
454+
/**
455+
* Automatically adds event data expressions for all properties of the type
456+
* specified by the given type reference. This method uses Java Bean
457+
* introspection to discover the properties and adds event data expressions
458+
* for each field path.
459+
* <p>
460+
* For generic types like {@code List<MyBean>}, this method will attempt to
461+
* extract the raw class type for introspection.
462+
*
463+
* @param typeReference
464+
* the type reference whose properties should be captured from
465+
* the event data, not <code>null</code>
466+
* @return this registration, for chaining
467+
* @see #addEventData(String)
468+
*/
469+
default DomListenerRegistration addEventData(
470+
TypeReference<?> typeReference) {
471+
if (typeReference == null) {
472+
throw new IllegalArgumentException("Type reference cannot be null");
473+
}
474+
// Extract raw class from TypeReference
475+
Class<?> rawType = extractRawType(typeReference);
476+
477+
if (rawType != null) {
478+
addEventData(rawType);
479+
}
480+
return this;
481+
}
482+
483+
/**
484+
* Helper method to extract the raw class from a TypeReference.
485+
*
486+
* @param typeReference
487+
* the type reference to extract from
488+
* @return the raw class, or null if it cannot be extracted
489+
*/
490+
private static Class<?> extractRawType(TypeReference<?> typeReference) {
491+
if (typeReference.getType() instanceof Class) {
492+
return (Class<?>) typeReference.getType();
493+
} else if (typeReference
494+
.getType() instanceof java.lang.reflect.ParameterizedType) {
495+
java.lang.reflect.ParameterizedType paramType = (java.lang.reflect.ParameterizedType) typeReference
496+
.getType();
497+
if (paramType.getRawType() instanceof Class) {
498+
return (Class<?>) paramType.getRawType();
499+
}
500+
}
501+
return null;
502+
}
503+
402504
/**
403505
* Configures the event listener to bypass the server side security checks
404506
* for modality. Handle with care! Can be ok when transferring data from

0 commit comments

Comments
 (0)