Skip to content

Commit 4d9632a

Browse files
Artur-claude
andcommitted
feat: Add collection/list serialization support with Component references
- Add support for sending List, Set, and other Collections from server to client - Collections are encoded using existing ARRAY_TYPE format [1, arrayContent] - Enhanced client-side array decoding to support complex types (beans, components) - Arrays can now contain mixed types: primitives, beans, and component references - Component references use {"@vaadin": "component", "nodeId": <id>} format - Added comprehensive tests for list serialization scenarios - Integration tests verify end-to-end list/collection handling Technical changes: - JacksonCodec: Added Collection handling to encode as ARRAY_TYPE - ClientJsonCodec: Enhanced jsonArrayAsJsArray() to decode complex types - Added needsTypeDecoding() helper to identify values requiring type-aware decoding - Tests added for arrays with components, beans, and mixed types 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
1 parent 70cd1ac commit 4d9632a

File tree

6 files changed

+481
-4
lines changed

6 files changed

+481
-4
lines changed

flow-client/src/main/java/com/vaadin/client/flow/util/ClientJsonCodec.java

Lines changed: 44 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -111,7 +111,7 @@ public static Object decodeWithTypeInfo(StateTree tree, JsonValue json) {
111111
int typeId = (int) array.getNumber(0);
112112
switch (typeId) {
113113
case JsonCodec.ARRAY_TYPE:
114-
return jsonArrayAsJsArray(array.getArray(1));
114+
return jsonArrayAsJsArray(tree, array.getArray(1));
115115
case JsonCodec.RETURN_CHANNEL_TYPE:
116116
return createReturnChannelCallback((int) array.getNumber(1),
117117
(int) array.getNumber(2),
@@ -228,18 +228,59 @@ public static JsonValue encodeWithoutTypeInfo(Object value) {
228228
* @return the converted JS array
229229
*/
230230
public static JsArray<Object> jsonArrayAsJsArray(JsonArray jsonArray) {
231+
return jsonArrayAsJsArray(null, jsonArray);
232+
}
233+
234+
/**
235+
* Converts a JSON array to a JS array with support for complex types.
236+
*
237+
* @param tree
238+
* the state tree for resolving complex types (can be null for
239+
* primitive arrays)
240+
* @param jsonArray
241+
* the JSON array to convert
242+
* @return the converted JS array
243+
*/
244+
public static JsArray<Object> jsonArrayAsJsArray(StateTree tree,
245+
JsonArray jsonArray) {
231246
JsArray<Object> jsArray;
232247
if (GWT.isScript()) {
233-
jsArray = WidgetUtil.crazyJsCast(jsonArray);
248+
// In browser, need to decode each element to handle complex types
249+
jsArray = JsCollections.array();
250+
for (int i = 0; i < jsonArray.length(); i++) {
251+
JsonValue item = jsonArray.get(i);
252+
if (tree != null && needsTypeDecoding(item)) {
253+
jsArray.push(decodeWithTypeInfo(tree, item));
254+
} else {
255+
jsArray.push(decodeWithoutTypeInfo(item));
256+
}
257+
}
234258
} else {
259+
// JVM test implementation
235260
jsArray = JsCollections.array();
236261
for (int i = 0; i < jsonArray.length(); i++) {
237-
jsArray.push(decodeWithoutTypeInfo(jsonArray.get(i)));
262+
JsonValue item = jsonArray.get(i);
263+
if (tree != null && needsTypeDecoding(item)) {
264+
jsArray.push(decodeWithTypeInfo(tree, item));
265+
} else {
266+
jsArray.push(decodeWithoutTypeInfo(item));
267+
}
238268
}
239269
}
240270
return jsArray;
241271
}
242272

273+
private static boolean needsTypeDecoding(JsonValue value) {
274+
// Check if this value needs type-aware decoding
275+
if (value.getType() == JsonType.ARRAY) {
276+
return true; // Could be a typed array
277+
} else if (value.getType() == JsonType.OBJECT) {
278+
JsonObject obj = (JsonObject) value;
279+
return obj.hasKey("@vaadin"); // Has type information
280+
}
281+
return false;
282+
}
283+
243284
/**
244285
* Decodes a bean object containing component references with lazy
245286
* resolution. Component lookups happen when the value is accessed, not

flow-client/src/test/java/com/vaadin/client/flow/util/ClientJsonCodecTest.java

Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -339,6 +339,104 @@ public void decodeStateNode_directComponentReference() {
339339
Assert.assertSame(node, decoded);
340340
}
341341

342+
@Test
343+
public void decodeWithTypeInfo_arrayWithComplexTypes() {
344+
StateTree tree = new StateTree(null);
345+
StateNode componentNode = new StateNode(49, tree);
346+
tree.registerNode(componentNode);
347+
348+
JsElement element = new JsElement() {
349+
};
350+
componentNode.setDomNode(element);
351+
352+
// Create an array with mixed types
353+
JsonArray itemsArray = Json.createArray();
354+
355+
// Add primitive
356+
itemsArray.set(0, Json.create("hello"));
357+
358+
// Add component reference
359+
elemental.json.JsonObject componentRef = Json.createObject();
360+
componentRef.put("@vaadin", "component");
361+
componentRef.put("nodeId", componentNode.getId());
362+
itemsArray.set(1, componentRef);
363+
364+
// Add bean
365+
elemental.json.JsonObject bean = Json.createObject();
366+
bean.put("name", "test");
367+
bean.put("value", 42);
368+
JsonArray wrappedBean = Json.createArray();
369+
wrappedBean.set(0, ClientJsonCodec.BEAN_TYPE);
370+
wrappedBean.set(1, bean);
371+
itemsArray.set(2, wrappedBean);
372+
373+
// Wrap as ARRAY_TYPE
374+
JsonArray wrappedArray = Json.createArray();
375+
wrappedArray.set(0, JsonCodec.ARRAY_TYPE);
376+
wrappedArray.set(1, itemsArray);
377+
378+
Object decoded = ClientJsonCodec.decodeWithTypeInfo(tree, wrappedArray);
379+
380+
Assert.assertNotNull("Decoded array should not be null", decoded);
381+
Assert.assertTrue("Should be a JsArray", decoded instanceof JsArray);
382+
383+
JsArray<?> decodedArray = (JsArray<?>) decoded;
384+
Assert.assertEquals("Should have 3 items", 3, decodedArray.length());
385+
Assert.assertEquals("First item should be string", "hello",
386+
decodedArray.get(0));
387+
Assert.assertSame("Second item should be the element", element,
388+
decodedArray.get(1));
389+
// Third item is a bean - in JVM tests it returns JsonValue
390+
Assert.assertNotNull("Third item should not be null",
391+
decodedArray.get(2));
392+
}
393+
394+
@Test
395+
public void decodeWithTypeInfo_arrayOfComponents() {
396+
StateTree tree = new StateTree(null);
397+
StateNode node1 = new StateNode(50, tree);
398+
StateNode node2 = new StateNode(51, tree);
399+
tree.registerNode(node1);
400+
tree.registerNode(node2);
401+
402+
JsElement element1 = new JsElement() {
403+
};
404+
JsElement element2 = new JsElement() {
405+
};
406+
node1.setDomNode(element1);
407+
node2.setDomNode(element2);
408+
409+
// Create an array of component references
410+
JsonArray itemsArray = Json.createArray();
411+
412+
elemental.json.JsonObject ref1 = Json.createObject();
413+
ref1.put("@vaadin", "component");
414+
ref1.put("nodeId", node1.getId());
415+
itemsArray.set(0, ref1);
416+
417+
elemental.json.JsonObject ref2 = Json.createObject();
418+
ref2.put("@vaadin", "component");
419+
ref2.put("nodeId", node2.getId());
420+
itemsArray.set(1, ref2);
421+
422+
// Wrap as ARRAY_TYPE
423+
JsonArray wrappedArray = Json.createArray();
424+
wrappedArray.set(0, JsonCodec.ARRAY_TYPE);
425+
wrappedArray.set(1, itemsArray);
426+
427+
Object decoded = ClientJsonCodec.decodeWithTypeInfo(tree, wrappedArray);
428+
429+
Assert.assertNotNull("Decoded array should not be null", decoded);
430+
Assert.assertTrue("Should be a JsArray", decoded instanceof JsArray);
431+
432+
JsArray<?> decodedArray = (JsArray<?>) decoded;
433+
Assert.assertEquals("Should have 2 items", 2, decodedArray.length());
434+
Assert.assertSame("First item should be element1", element1,
435+
decodedArray.get(0));
436+
Assert.assertSame("Second item should be element2", element2,
437+
decodedArray.get(1));
438+
}
439+
342440
@Test
343441
public void decodeWithTypeInfo_directComponentReferenceWithNullNodeId() {
344442
StateTree tree = new StateTree(null);

flow-server/src/main/java/com/vaadin/flow/internal/JacksonCodec.java

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -108,6 +108,13 @@ public static JsonNode encodeWithTypeInfo(Object value) {
108108
encoded = wrapComplexValue(ARRAY_TYPE, encoded);
109109
}
110110
return encoded;
111+
} else if (value instanceof java.util.Collection<?>) {
112+
// Handle collections (List, Set, etc.) as arrays
113+
ArrayNode arrayNode = JacksonUtils.createArrayNode();
114+
for (Object item : (java.util.Collection<?>) value) {
115+
arrayNode.add(encodeWithTypeInfo(item));
116+
}
117+
return wrapComplexValue(ARRAY_TYPE, arrayNode);
111118
} else {
112119
// All other types (including arrays and beans) encode as BEAN_TYPE
113120
// using Jackson's built-in serialization

flow-server/src/test/java/com/vaadin/flow/internal/JacksonCodecTest.java

Lines changed: 133 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -293,4 +293,137 @@ public void encodeWithTypeInfo_singleComponent() {
293293
Assert.assertEquals("Should have nodeId matching element",
294294
element.getNode().getId(), encoded.get("nodeId").intValue());
295295
}
296+
297+
@Test
298+
public void encodeWithTypeInfo_listOfPrimitives() {
299+
List<String> stringList = Arrays.asList("one", "two", "three");
300+
JsonNode encoded = JacksonCodec.encodeWithTypeInfo(stringList);
301+
302+
// Should be encoded as ARRAY_TYPE
303+
Assert.assertTrue("Should be an array", encoded.isArray());
304+
Assert.assertEquals("Should have type marker", JacksonCodec.ARRAY_TYPE,
305+
encoded.get(0).intValue());
306+
307+
JsonNode arrayContent = encoded.get(1);
308+
Assert.assertTrue("Content should be an array", arrayContent.isArray());
309+
Assert.assertEquals("Should have 3 elements", 3, arrayContent.size());
310+
Assert.assertEquals("First element", "one",
311+
arrayContent.get(0).asText());
312+
Assert.assertEquals("Second element", "two",
313+
arrayContent.get(1).asText());
314+
Assert.assertEquals("Third element", "three",
315+
arrayContent.get(2).asText());
316+
}
317+
318+
@Test
319+
public void encodeWithTypeInfo_listOfComponents() {
320+
UI ui = new UI();
321+
Element elem1 = ElementFactory.createDiv();
322+
Element elem2 = ElementFactory.createSpan();
323+
ui.getElement().appendChild(elem1, elem2);
324+
325+
List<Element> elements = Arrays.asList(elem1, elem2);
326+
JsonNode encoded = JacksonCodec.encodeWithTypeInfo(elements);
327+
328+
// Should be encoded as ARRAY_TYPE
329+
Assert.assertTrue("Should be an array", encoded.isArray());
330+
Assert.assertEquals("Should have type marker", JacksonCodec.ARRAY_TYPE,
331+
encoded.get(0).intValue());
332+
333+
JsonNode arrayContent = encoded.get(1);
334+
Assert.assertTrue("Content should be an array", arrayContent.isArray());
335+
Assert.assertEquals("Should have 2 elements", 2, arrayContent.size());
336+
337+
// First element should be a component reference
338+
JsonNode firstComponent = arrayContent.get(0);
339+
Assert.assertTrue("Should be an object", firstComponent.isObject());
340+
Assert.assertEquals("Should have @vaadin=component", "component",
341+
firstComponent.get("@vaadin").asText());
342+
Assert.assertEquals("Should have correct nodeId",
343+
elem1.getNode().getId(),
344+
firstComponent.get("nodeId").intValue());
345+
346+
// Second element should be a component reference
347+
JsonNode secondComponent = arrayContent.get(1);
348+
Assert.assertTrue("Should be an object", secondComponent.isObject());
349+
Assert.assertEquals("Should have @vaadin=component", "component",
350+
secondComponent.get("@vaadin").asText());
351+
Assert.assertEquals("Should have correct nodeId",
352+
elem2.getNode().getId(),
353+
secondComponent.get("nodeId").intValue());
354+
}
355+
356+
@Test
357+
public void encodeWithTypeInfo_listMixed() {
358+
UI ui = new UI();
359+
Element element = ElementFactory.createDiv();
360+
ui.getElement().appendChild(element);
361+
362+
List<Object> mixed = Arrays.asList("text", 42, element, null);
363+
JsonNode encoded = JacksonCodec.encodeWithTypeInfo(mixed);
364+
365+
// Should be encoded as ARRAY_TYPE
366+
Assert.assertTrue("Should be an array", encoded.isArray());
367+
Assert.assertEquals("Should have type marker", JacksonCodec.ARRAY_TYPE,
368+
encoded.get(0).intValue());
369+
370+
JsonNode arrayContent = encoded.get(1);
371+
Assert.assertTrue("Content should be an array", arrayContent.isArray());
372+
Assert.assertEquals("Should have 4 elements", 4, arrayContent.size());
373+
374+
// Check each element
375+
Assert.assertEquals("First element should be string", "text",
376+
arrayContent.get(0).asText());
377+
Assert.assertEquals("Second element should be number", 42,
378+
arrayContent.get(1).intValue());
379+
380+
JsonNode componentRef = arrayContent.get(2);
381+
Assert.assertTrue("Third element should be object",
382+
componentRef.isObject());
383+
Assert.assertEquals("Should have @vaadin=component", "component",
384+
componentRef.get("@vaadin").asText());
385+
386+
Assert.assertTrue("Fourth element should be null",
387+
arrayContent.get(3).isNull());
388+
}
389+
390+
public static class BeanWithList {
391+
public String name;
392+
public List<String> items;
393+
394+
public BeanWithList() {
395+
}
396+
397+
public BeanWithList(String name, List<String> items) {
398+
this.name = name;
399+
this.items = items;
400+
}
401+
}
402+
403+
@Test
404+
public void encodeWithTypeInfo_beanWithList() {
405+
List<String> items = Arrays.asList("item1", "item2", "item3");
406+
BeanWithList bean = new BeanWithList("TestBean", items);
407+
408+
JsonNode encoded = JacksonCodec.encodeWithTypeInfo(bean);
409+
410+
// Should be encoded as BEAN_TYPE
411+
Assert.assertTrue("Should be an array", encoded.isArray());
412+
Assert.assertEquals("Should have type marker", JacksonCodec.BEAN_TYPE,
413+
encoded.get(0).intValue());
414+
415+
JsonNode beanContent = encoded.get(1);
416+
Assert.assertTrue("Content should be an object",
417+
beanContent.isObject());
418+
Assert.assertEquals("Should have name field", "TestBean",
419+
beanContent.get("name").asText());
420+
421+
// The items field should be an array (not wrapped with ARRAY_TYPE
422+
// inside bean)
423+
JsonNode itemsField = beanContent.get("items");
424+
Assert.assertTrue("items field should be an array",
425+
itemsField.isArray());
426+
Assert.assertEquals("Should have 3 items", 3, itemsField.size());
427+
Assert.assertEquals("First item", "item1", itemsField.get(0).asText());
428+
}
296429
}

0 commit comments

Comments
 (0)