Skip to content

Commit 016b794

Browse files
authored
feat: add Jackson serializers for Component and Node types (#22400)
- Add custom Jackson serializers to handle @v-node serialization for Components and Nodes in collections - Remove duplicate component handling from JacksonCodec.encodeWithTypeInfo() - ComponentSerializer delegates to NodeSerializer to eliminate code duplication - NodeSerializer handles all Node types (Element, ShadowRoot) with consistent @v-node logic - Prevents infinite recursion when serializing Components in collections - Maintains existing @v-node serialization format for client compatibility
1 parent 462f80d commit 016b794

File tree

2 files changed

+84
-25
lines changed

2 files changed

+84
-25
lines changed

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

Lines changed: 6 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,6 @@
2828

2929
import com.vaadin.flow.component.Component;
3030
import com.vaadin.flow.dom.Element;
31-
import com.vaadin.flow.dom.Node;
3231
import com.vaadin.flow.internal.nodefeature.ReturnChannelRegistration;
3332

3433
/**
@@ -63,8 +62,9 @@ private JacksonCodec() {
6362
* JSON. Such types are encoded as an JSON array starting with an id
6463
* defining the actual type and followed by the actual data. Supported value
6564
* types are any native JSON type supported by
66-
* {@link #encodeWithoutTypeInfo(Object)}, {@link Element} and
67-
* {@link Component} (encoded as its root element).
65+
* {@link #encodeWithoutTypeInfo(Object)}, and all other types are handled
66+
* by Jackson serialization with custom serializers for {@link Component}
67+
* and Node types.
6868
*
6969
* @param value
7070
* the value to encode
@@ -74,18 +74,15 @@ public static JsonNode encodeWithTypeInfo(Object value) {
7474

7575
if (value == null) {
7676
return encodeWithoutTypeInfo(value);
77-
} else if (value instanceof Component) {
78-
return encodeNode(((Component) value).getElement());
79-
} else if (value instanceof Node<?>) {
80-
return encodeNode((Node<?>) value);
8177
} else if (value instanceof ReturnChannelRegistration) {
8278
return encodeReturnChannel((ReturnChannelRegistration) value);
8379
} else if (canEncodeWithoutTypeInfo(value.getClass())) {
8480
// Native JSON types - no wrapping needed
8581
return encodeWithoutTypeInfo(value);
8682
} else {
87-
// All other types (including arrays and beans) use standard Jackson
88-
// serialization
83+
// All other types (including Components, Nodes, arrays and beans)
84+
// use standard Jackson
85+
// serialization with custom serializers for Components and Nodes
8986
return JacksonUtils.getMapper().valueToTree(value);
9087
}
9188
}
@@ -101,18 +98,6 @@ private static JsonNode encodeReturnChannel(
10198
return obj;
10299
}
103100

104-
private static JsonNode encodeNode(Node<?> node) {
105-
StateNode stateNode = node.getNode();
106-
if (stateNode.isAttached()) {
107-
ObjectMapper mapper = JacksonUtils.getMapper();
108-
ObjectNode obj = mapper.createObjectNode();
109-
obj.put("@v-node", stateNode.getId());
110-
return obj;
111-
} else {
112-
return JacksonUtils.getMapper().nullNode();
113-
}
114-
}
115-
116101
/**
117102
* Helper for checking whether the type is supported by
118103
* {@link #encodeWithoutTypeInfo(Object)}. Supported value types are

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

Lines changed: 78 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@
3333
import java.util.stream.Stream;
3434

3535
import tools.jackson.core.JacksonException;
36+
import tools.jackson.core.JsonGenerator;
3637
import tools.jackson.core.json.JsonReadFeature;
3738
import tools.jackson.core.type.TypeReference;
3839
import tools.jackson.core.util.DefaultPrettyPrinter;
@@ -41,14 +42,21 @@
4142
import tools.jackson.databind.DeserializationFeature;
4243
import tools.jackson.databind.JsonNode;
4344
import tools.jackson.databind.ObjectMapper;
45+
import tools.jackson.databind.SerializationContext;
46+
import tools.jackson.databind.ValueSerializer;
4447
import tools.jackson.databind.json.JsonMapper;
48+
import tools.jackson.databind.module.SimpleModule;
4549
import tools.jackson.databind.node.ArrayNode;
4650
import tools.jackson.databind.node.BaseJsonNode;
4751
import tools.jackson.databind.node.DoubleNode;
4852
import tools.jackson.databind.node.JsonNodeType;
4953
import tools.jackson.databind.node.ObjectNode;
5054
import tools.jackson.databind.node.ValueNode;
5155

56+
import com.vaadin.flow.component.Component;
57+
import com.vaadin.flow.dom.Element;
58+
import com.vaadin.flow.dom.Node;
59+
5260
import elemental.json.Json;
5361
import elemental.json.JsonArray;
5462
import elemental.json.JsonBoolean;
@@ -71,10 +79,17 @@ public final class JacksonUtils {
7179

7280
private static final String CANNOT_CONVERT_NULL_TO_OBJECT = "Cannot convert null to Java object";
7381

74-
private static final ObjectMapper objectMapper = JsonMapper.builder()
75-
.enable(JsonReadFeature.ALLOW_SINGLE_QUOTES)
76-
.disable(DeserializationFeature.FAIL_ON_NULL_FOR_PRIMITIVES)
77-
.build();
82+
private static final ObjectMapper objectMapper = createConfiguredMapper();
83+
84+
private static ObjectMapper createConfiguredMapper() {
85+
SimpleModule module = new SimpleModule();
86+
module.addSerializer(Component.class, new ComponentSerializer());
87+
module.addSerializer(Node.class, new NodeSerializer());
88+
89+
return JsonMapper.builder().enable(JsonReadFeature.ALLOW_SINGLE_QUOTES)
90+
.disable(DeserializationFeature.FAIL_ON_NULL_FOR_PRIMITIVES)
91+
.addModule(module).build();
92+
}
7893

7994
public static ObjectMapper getMapper() {
8095
return objectMapper;
@@ -632,4 +647,63 @@ public static String toFileJson(JsonNode node) throws JacksonException {
632647
.withObjectNameValueSpacing(Spacing.AFTER));
633648
return objectMapper.writer().with(filePrinter).writeValueAsString(node);
634649
}
650+
651+
/**
652+
* Custom Jackson serializer for Component that delegates to NodeSerializer.
653+
*/
654+
public static class ComponentSerializer extends ValueSerializer<Component> {
655+
private final NodeSerializer nodeSerializer = new NodeSerializer();
656+
657+
@Override
658+
public void serialize(Component component, JsonGenerator gen,
659+
SerializationContext serializers) {
660+
try {
661+
if (component != null) {
662+
// Delegate to NodeSerializer using the component's element
663+
nodeSerializer.serialize(component.getElement(), gen,
664+
serializers);
665+
} else {
666+
gen.writeNull();
667+
}
668+
} catch (Exception e) {
669+
throw new RuntimeException("Failed to serialize Component", e);
670+
}
671+
}
672+
}
673+
674+
/**
675+
* Custom Jackson serializer for Node types (Element, ShadowRoot) that
676+
* serializes attached nodes as @v-node references for client-side DOM
677+
* manipulation.
678+
*/
679+
@SuppressWarnings("rawtypes")
680+
public static class NodeSerializer extends ValueSerializer<Node> {
681+
@Override
682+
@SuppressWarnings("unchecked")
683+
public void serialize(Node node, JsonGenerator gen,
684+
SerializationContext serializers) {
685+
try {
686+
if (node != null) {
687+
// Serialize attached nodes as @v-node references containing
688+
// the StateNode ID
689+
// This allows the client to identify and manipulate the
690+
// corresponding DOM element
691+
if (node.getNode().isAttached()) {
692+
gen.writeStartObject();
693+
gen.writeNumberProperty("@v-node",
694+
node.getNode().getId());
695+
gen.writeEndObject();
696+
} else {
697+
// Detached nodes cannot be referenced on the client, so
698+
// serialize as null
699+
gen.writeNull();
700+
}
701+
} else {
702+
gen.writeNull();
703+
}
704+
} catch (Exception e) {
705+
throw new RuntimeException("Failed to serialize Node", e);
706+
}
707+
}
708+
}
635709
}

0 commit comments

Comments
 (0)