diff --git a/src/main/java/com/flowingcode/vaadin/jsonmigration/JsonMigration.java b/src/main/java/com/flowingcode/vaadin/jsonmigration/JsonMigration.java
index 57983ba..10cc8e2 100644
--- a/src/main/java/com/flowingcode/vaadin/jsonmigration/JsonMigration.java
+++ b/src/main/java/com/flowingcode/vaadin/jsonmigration/JsonMigration.java
@@ -19,6 +19,7 @@
*/
package com.flowingcode.vaadin.jsonmigration;
+import com.vaadin.flow.component.ClientCallable;
import com.vaadin.flow.component.page.PendingJavaScriptResult;
import com.vaadin.flow.dom.DomEvent;
import com.vaadin.flow.dom.Element;
@@ -63,6 +64,18 @@ private static Class> lookup_BaseJsonNode() {
}
}
+ /**
+ * Converts a given Java object into the return type of a {@link ClientCallable method}.
+ *
+ * In Vaadin 25, this method converts {@code JsonValue} into {@code JsonNode}.
+ *
+ * @param object the object to convert
+ * @return an {@code Object} suitable to use as the result of a {@code ClientCallable} method.
+ */
+ public static Object convertToClientCallableResult(Object object) {
+ return helper.convertToClientCallableResult(object);
+ }
+
/**
* Converts a given Java object into a {@code JsonValue}.
*
diff --git a/src/main/java/com/flowingcode/vaadin/jsonmigration/JsonMigrationHelper.java b/src/main/java/com/flowingcode/vaadin/jsonmigration/JsonMigrationHelper.java
index 9bd05fe..aca9084 100644
--- a/src/main/java/com/flowingcode/vaadin/jsonmigration/JsonMigrationHelper.java
+++ b/src/main/java/com/flowingcode/vaadin/jsonmigration/JsonMigrationHelper.java
@@ -29,6 +29,8 @@ interface JsonMigrationHelper {
JsonValue convertToJsonValue(Object object);
+ Object convertToClientCallableResult(Object object);
+
Object invoke(Method method, Object instance, Object... args);
ElementalPendingJavaScriptResult convertPendingJavaScriptResult(PendingJavaScriptResult result);
diff --git a/src/main/java/com/flowingcode/vaadin/jsonmigration/JsonMigrationHelper25.java b/src/main/java/com/flowingcode/vaadin/jsonmigration/JsonMigrationHelper25.java
index c2d6d31..591b0e9 100644
--- a/src/main/java/com/flowingcode/vaadin/jsonmigration/JsonMigrationHelper25.java
+++ b/src/main/java/com/flowingcode/vaadin/jsonmigration/JsonMigrationHelper25.java
@@ -55,6 +55,15 @@ public JsonValue convertToJsonValue(Object object) {
}
}
+ @Override
+ public Object convertToClientCallableResult(Object object) {
+ if (object instanceof JsonValue) {
+ return convertToJsonNode((JsonValue) object);
+ } else {
+ return object;
+ }
+ }
+
@Override
@SneakyThrows
public Object invoke(Method method, Object instance, Object... args) {
diff --git a/src/main/java/com/flowingcode/vaadin/jsonmigration/JsonSerializer.java b/src/main/java/com/flowingcode/vaadin/jsonmigration/JsonSerializer.java
new file mode 100644
index 0000000..fbfef15
--- /dev/null
+++ b/src/main/java/com/flowingcode/vaadin/jsonmigration/JsonSerializer.java
@@ -0,0 +1,400 @@
+/*-
+ * #%L
+ * Json Migration Helper
+ * %%
+ * Copyright (C) 2025 Flowing Code
+ * %%
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ * #L%
+ */
+package com.flowingcode.vaadin.jsonmigration;
+
+/*
+ * Copyright 2000-2025 Vaadin Ltd.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+
+import elemental.json.Json;
+import elemental.json.JsonArray;
+import elemental.json.JsonNull;
+import elemental.json.JsonObject;
+import elemental.json.JsonType;
+import elemental.json.JsonValue;
+import java.beans.BeanInfo;
+import java.beans.Introspector;
+import java.beans.PropertyDescriptor;
+import java.lang.reflect.Array;
+import java.lang.reflect.Method;
+import java.lang.reflect.ParameterizedType;
+import java.lang.reflect.RecordComponent;
+import java.lang.reflect.Type;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.LinkedHashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+import java.util.Set;
+
+/**
+ * General-purpose serializer of Java objects to {@link JsonValue} and
+ * deserializer of JsonValue to Java objects.
+ *
+ * @since 1.0
+ */
+public final class JsonSerializer {
+
+ private JsonSerializer() {
+ }
+
+ /**
+ * Converts a Java bean, String, wrapper of primitive type or enum to a {@link JsonValue}.
+ *
+ * When a bean is used, a {@link JsonObject} is returned.
+ *
+ * @param bean Java object to be converted
+ * @return the json representation of the Java object
+ */
+ public static JsonValue toJson(Object bean) {
+ if (bean == null) {
+ return Json.createNull();
+ }
+ if (bean instanceof Collection) {
+ return toJson((Collection>) bean);
+ }
+ if (bean.getClass().isArray()) {
+ return toJsonArray(bean);
+ }
+
+ Optional simpleType = tryToConvertToSimpleType(bean);
+ if (simpleType.isPresent()) {
+ return simpleType.get();
+ }
+
+ try {
+ JsonObject json = Json.createObject();
+ Class> type = bean.getClass();
+
+ if (type.isRecord()) {
+ for (RecordComponent rc : type.getRecordComponents()) {
+ json.put(rc.getName(),
+ toJson(rc.getAccessor().invoke(bean)));
+ }
+ } else {
+ BeanInfo info = Introspector.getBeanInfo(type);
+ for (PropertyDescriptor pd : info.getPropertyDescriptors()) {
+ if ("class".equals(pd.getName())) {
+ continue;
+ }
+ Method reader = pd.getReadMethod();
+ if (reader != null) {
+ json.put(pd.getName(), toJson(reader.invoke(bean)));
+ }
+ }
+ }
+
+ return json;
+ } catch (Exception e) {
+ throw new IllegalArgumentException(
+ "Could not serialize object of type " + bean.getClass()
+ + " to JsonValue",
+ e);
+ }
+ }
+
+ /**
+ * Converts a collection of object into a {@link JsonArray}, converting each
+ * item of the collection individually.
+ *
+ * @param beans
+ * the collection of objects to be converted
+ * @return the json representation of the objects in the collectios. An
+ * empty array is returned if the input collections is
+ * null
+ */
+ public static JsonArray toJson(Collection> beans) {
+ JsonArray array = Json.createArray();
+ if (beans == null) {
+ return array;
+ }
+
+ beans.stream().map(JsonSerializer::toJson)
+ .forEachOrdered(json -> array.set(array.length(), json));
+ return array;
+ }
+
+ private static JsonArray toJsonArray(Object javaArray) {
+ int length = Array.getLength(javaArray);
+ JsonArray array = Json.createArray();
+ for (int i = 0; i < length; i++) {
+ array.set(i, toJson(Array.get(javaArray, i)));
+ }
+ return array;
+ }
+
+ private static Optional tryToConvertToSimpleType(Object bean) {
+ if (bean instanceof String) {
+ return Optional.of(Json.create((String) bean));
+ }
+ if (bean instanceof Number) {
+ return Optional.of(Json.create(((Number) bean).doubleValue()));
+ }
+ if (bean instanceof Boolean) {
+ return Optional.of(Json.create((Boolean) bean));
+ }
+ if (bean instanceof Character) {
+ return Optional.of(Json.create(Character.toString((char) bean)));
+ }
+ if (bean instanceof Enum) {
+ return Optional.of(Json.create(((Enum>) bean).name()));
+ }
+ if (bean instanceof JsonValue) {
+ return Optional.of((JsonValue) bean);
+ }
+ return Optional.empty();
+ }
+
+ /**
+ * Converts a JsonValue to the corresponding Java object. The Java object can be a Java bean,
+ * String, wrapper of primitive types or an enum.
+ *
+ * @param type the type of the Java object convert the json to
+ * @param json the json representation of the object
+ * @param the resulting object type
+ *
+ * @return the deserialized object, or null if the input json is null
+ */
+ public static T toObject(Class type, JsonValue json) {
+ return toObject(type, null, json);
+ }
+
+ @SuppressWarnings("unchecked")
+ private static T toObject(Class type, Type genericType,
+ JsonValue json) {
+ if (json == null || json instanceof JsonNull) {
+ return null;
+ }
+
+ Optional> simpleType = tryToConvertFromSimpleType(type, json);
+ if (simpleType.isPresent()) {
+ return (T) simpleType.get();
+ }
+
+ if (Collection.class.isAssignableFrom(type)) {
+ return toCollection(type, genericType, json);
+ }
+
+ if (type.isRecord()) {
+ return toRecord(type, json);
+ }
+
+ T instance;
+ try {
+ instance = type.newInstance();
+ } catch (Exception e) {
+ throw new IllegalArgumentException(
+ "Could not create an instance of type " + type
+ + ". Make sure it contains a default public constructor and the class is accessible.",
+ e);
+ }
+
+ try {
+
+ JsonObject jsonObject = (JsonObject) json;
+ String[] keys = jsonObject.keys();
+ if (keys == null) {
+ return instance;
+ }
+
+ BeanInfo info = Introspector.getBeanInfo(type);
+ Map writers = new HashMap<>();
+
+ for (PropertyDescriptor pd : info.getPropertyDescriptors()) {
+ Method writer = pd.getWriteMethod();
+ if (writer != null) {
+ writers.put(pd.getName(), writer);
+ }
+ }
+ for (String key : keys) {
+ JsonValue jsonValue = jsonObject.get(key);
+
+ Method method = writers.get(key);
+ if (method != null) {
+ Class> parameterType = method.getParameterTypes()[0];
+ Type genericParameterType = method
+ .getGenericParameterTypes()[0];
+ Object value = toObject(parameterType, genericParameterType,
+ jsonValue);
+ method.invoke(instance, value);
+ }
+ }
+
+ return instance;
+ } catch (Exception e) {
+ throw new IllegalArgumentException(
+ "Could not deserialize object of type " + type
+ + " from JsonValue",
+ e);
+ }
+ }
+
+ private static T toRecord(Class type, JsonValue json) {
+ try {
+ RecordComponent[] components = type.getRecordComponents();
+ Class>[] componentTypes = new Class>[components.length];
+ Object[] values = new Object[components.length];
+
+ for (int i = 0; i < components.length; i++) {
+ componentTypes[i] = components[i].getType();
+ values[i] = toObject(componentTypes[i],
+ ((JsonObject) json).get(components[i].getName()));
+ }
+
+ return type.getDeclaredConstructor(componentTypes)
+ .newInstance(values);
+ } catch (Exception e) {
+ throw new IllegalArgumentException(
+ "Could not deserialize record of type " + type
+ + " from JsonValue",
+ e);
+ }
+ }
+
+ private static T toCollection(Class type, Type genericType,
+ JsonValue json) {
+ if (json.getType() != JsonType.ARRAY) {
+ return null;
+ }
+ if (!(genericType instanceof ParameterizedType)) {
+ throw new IllegalArgumentException(
+ "Could not infer the generic parameterized type of the collection of class: "
+ + type.getName()
+ + ". The type is no subclass of ParameterizedType: "
+ + genericType);
+ }
+ JsonArray array = (JsonArray) json;
+ Collection> collection = tryToCreateCollection(type, array.length());
+ if (array.length() > 0) {
+ ParameterizedType parameterizedType = (ParameterizedType) genericType;
+ Class> parameterizedClass = (Class>) parameterizedType
+ .getActualTypeArguments()[0];
+ collection.addAll(
+ (List) toObjects(parameterizedClass, (JsonArray) json));
+ }
+ return (T) collection;
+ }
+
+ /**
+ * Converts a JsonArray into a collection of Java objects. The Java objects can be Java beans,
+ * Strings, wrappers of primitive types or enums.
+ *
+ * @param type the type of the elements in the array
+ * @param json the json representation of the objects
+ * @param the resulting objects types
+ *
+ * @return a modifiable list of converted objects. Returns an empty list if the input array is
+ * null
+ */
+ public static List toObjects(Class type, JsonArray json) {
+ if (json == null) {
+ return new ArrayList<>(0);
+ }
+ List list = new ArrayList<>(json.length());
+ for (int i = 0; i < json.length(); i++) {
+ list.add(JsonSerializer.toObject(type, json.get(i)));
+ }
+ return list;
+ }
+
+ private static Optional> tryToConvertFromSimpleType(Class> type,
+ JsonValue json) {
+ if (type.isAssignableFrom(String.class)) {
+ return Optional.of(json.asString());
+ }
+ if (type.isAssignableFrom(int.class)
+ || type.isAssignableFrom(Integer.class)) {
+ return Optional.of((int) json.asNumber());
+ }
+ if (type.isAssignableFrom(double.class)
+ || type.isAssignableFrom(Double.class)) {
+ return Optional.of(json.asNumber());
+ }
+ if (type.isAssignableFrom(long.class)
+ || type.isAssignableFrom(Long.class)) {
+ return Optional.of((long) json.asNumber());
+ }
+ if (type.isAssignableFrom(short.class)
+ || type.isAssignableFrom(Short.class)) {
+ return Optional.of((short) json.asNumber());
+ }
+ if (type.isAssignableFrom(byte.class)
+ || type.isAssignableFrom(Byte.class)) {
+ return Optional.of((byte) json.asNumber());
+ }
+ if (type.isAssignableFrom(char.class)
+ || type.isAssignableFrom(Character.class)) {
+ return Optional.of(json.asString().charAt(0));
+ }
+ if (type.isAssignableFrom(Boolean.class)
+ || type.isAssignableFrom(boolean.class)) {
+ return Optional.of(json.asBoolean());
+ }
+ if (type.isEnum()) {
+ return Optional.of(Enum.valueOf((Class extends Enum>) type,
+ json.asString()));
+ }
+ if (JsonValue.class.isAssignableFrom(type)) {
+ return Optional.of(json);
+ }
+ return Optional.empty();
+
+ }
+
+ private static Collection> tryToCreateCollection(Class> collectionType,
+ int initialCapacity) {
+ if (collectionType.isInterface()) {
+ if (List.class.isAssignableFrom(collectionType)) {
+ return new ArrayList<>(initialCapacity);
+ }
+ if (Set.class.isAssignableFrom(collectionType)) {
+ return new LinkedHashSet<>(initialCapacity);
+ }
+ throw new IllegalArgumentException(
+ "Collection type not supported: '"
+ + collectionType.getName()
+ + "'. Use Lists, Sets or concrete classes that implement java.util.Collection.");
+ }
+ try {
+ return (Collection>) collectionType.newInstance();
+ } catch (Exception e) {
+ throw new IllegalArgumentException(
+ "Could not create an instance of the collection of type "
+ + collectionType
+ + ". Make sure it contains a default public constructor and the class is accessible.",
+ e);
+ }
+ }
+
+}
diff --git a/src/main/java/com/flowingcode/vaadin/jsonmigration/LegacyJsonMigrationHelper.java b/src/main/java/com/flowingcode/vaadin/jsonmigration/LegacyJsonMigrationHelper.java
index c4b331e..a00b9bc 100644
--- a/src/main/java/com/flowingcode/vaadin/jsonmigration/LegacyJsonMigrationHelper.java
+++ b/src/main/java/com/flowingcode/vaadin/jsonmigration/LegacyJsonMigrationHelper.java
@@ -39,6 +39,11 @@ public JsonValue convertToJsonValue(Object object) {
object.getClass().getName() + " cannot be converted to elemental.json.JsonObject");
}
}
+
+ @Override
+ public Object convertToClientCallableResult(Object object) {
+ return object;
+ }
@Override
@SneakyThrows