This is an automated email from the ASF dual-hosted git repository.
lukaszlenart pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/struts.git
The following commit(s) were added to refs/heads/main by this push:
new 60bc7b8d7 WW-5652 Add support for Java records and Optional
serialization in the JSON plugin. (#1792)
60bc7b8d7 is described below
commit 60bc7b8d7ac3aca7ee7c7c3ffbb13e1d6d1113a0
Author: Sri lakshmi kanthan P <[email protected]>
AuthorDate: Mon Jul 20 13:00:52 2026 +0530
WW-5652 Add support for Java records and Optional serialization in the JSON
plugin. (#1792)
* feat(json): support serialization of Java records in JSON processing
* feat(json): add support for serializing Optional values in JSON processing
---
.../org/apache/struts2/json/StrutsJSONWriter.java | 73 +++++++++-
.../org/apache/struts2/json/annotations/JSON.java | 2 +-
.../struts2/json/annotations/JSONFieldBridge.java | 2 +-
.../apache/struts2/json/StrutsJSONWriterTest.java | 159 +++++++++++++++++++++
4 files changed, 232 insertions(+), 4 deletions(-)
diff --git
a/plugins/json/src/main/java/org/apache/struts2/json/StrutsJSONWriter.java
b/plugins/json/src/main/java/org/apache/struts2/json/StrutsJSONWriter.java
index b7f540b0b..fd6fe14aa 100644
--- a/plugins/json/src/main/java/org/apache/struts2/json/StrutsJSONWriter.java
+++ b/plugins/json/src/main/java/org/apache/struts2/json/StrutsJSONWriter.java
@@ -35,6 +35,7 @@ import java.beans.PropertyDescriptor;
import java.lang.reflect.Array;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
+import java.lang.reflect.RecordComponent;
import java.text.CharacterIterator;
import java.text.DateFormat;
import java.text.SimpleDateFormat;
@@ -57,6 +58,7 @@ import java.util.HashMap;
import java.util.Iterator;
import java.util.Locale;
import java.util.Map;
+import java.util.Optional;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import java.util.regex.Pattern;
@@ -160,6 +162,11 @@ public class StrutsJSONWriter implements JSONWriter {
return;
}
+ if (object instanceof Optional<?> optional) {
+ this.value(optional.orElse(null), method);
+ return;
+ }
+
if (this.stack.contains(object)) {
Class<?> clazz = object.getClass();
@@ -213,6 +220,8 @@ public class StrutsJSONWriter implements JSONWriter {
this.string(object);
} else if (object instanceof Enum<?> enumValue) {
this.enumeration(enumValue);
+ } else if (object.getClass().isRecord()) {
+ this.record(object);
} else {
processCustom(object, method);
}
@@ -305,6 +314,62 @@ public class StrutsJSONWriter implements JSONWriter {
this.add("}");
}
+ /**
+ * Serialize a Java record by iterating its record components.
+ *
+ * @param object the record instance
+ * @throws JSONException in case of error during serialize
+ */
+ protected void record(Object object) throws JSONException {
+ this.add("{");
+
+ try {
+ boolean hasData = false;
+
+ for (RecordComponent component :
object.getClass().getRecordComponents()) {
+ String name = component.getName();
+ Method accessor = component.getAccessor();
+
+ if (accessor.isAnnotationPresent(JSON.class)) {
+ JSONAnnotationFinder jsonFinder = new
JSONAnnotationFinder(accessor).invoke();
+
+ if (!jsonFinder.shouldSerialize()) {
+ continue;
+ }
+
+ if (jsonFinder.getName() != null) {
+ name = jsonFinder.getName();
+ }
+ }
+
+ String expr = null;
+
+ if (this.buildExpr) {
+ expr = this.expandExpr(name);
+ if (this.shouldExcludeProperty(expr)) continue;
+ expr = this.setExprStack(expr);
+ }
+
+ Object value = accessor.invoke(object);
+
+ if (accessor.isAnnotationPresent(JSONFieldBridge.class)) {
+ value = getBridgedValue(accessor, value);
+ }
+
+ boolean propertyPrinted = this.add(name, value, accessor,
hasData);
+ hasData = hasData || propertyPrinted;
+
+ if (this.buildExpr) {
+ this.setExprStack(expr);
+ }
+ }
+ } catch (Exception e) {
+ throw new JSONException(e);
+ }
+
+ this.add("}");
+ }
+
protected BeanInfo getBeanInfoIgnoreHierarchy(final Class<?> clazz) throws
IntrospectionException {
BeanInfo beanInfo = BEAN_INFO_CACHE_IGNORE_HIERARCHY.get(clazz);
if (beanInfo != null) {
@@ -443,11 +508,15 @@ public class StrutsJSONWriter implements JSONWriter {
return false;
}
+ private static boolean isAbsent(Object value) {
+ return value == null || (value instanceof Optional<?> opt &&
opt.isEmpty());
+ }
+
/*
* Add name/value pair to buffer
*/
protected boolean add(String name, Object value, Method method, boolean
hasData) throws JSONException {
- if (excludeNullProperties && value == null) {
+ if (excludeNullProperties && isAbsent(value)) {
return false;
}
if (hasData) {
@@ -472,7 +541,7 @@ public class StrutsJSONWriter implements JSONWriter {
boolean hasData = false;
while (it.hasNext()) {
Map.Entry<?, ?> entry = (Map.Entry<?, ?>) it.next();
- if (excludeNullProperties && entry.getValue() == null) {
+ if (excludeNullProperties && isAbsent(entry.getValue())) {
continue;
}
diff --git
a/plugins/json/src/main/java/org/apache/struts2/json/annotations/JSON.java
b/plugins/json/src/main/java/org/apache/struts2/json/annotations/JSON.java
index 9b40f6f57..f9ea7d7ee 100644
--- a/plugins/json/src/main/java/org/apache/struts2/json/annotations/JSON.java
+++ b/plugins/json/src/main/java/org/apache/struts2/json/annotations/JSON.java
@@ -26,7 +26,7 @@ import java.lang.annotation.Target;
/*
* Annotation used to customize serialization
*/
-@Target(ElementType.METHOD)
+@Target({ElementType.METHOD, ElementType.RECORD_COMPONENT})
@Retention(RetentionPolicy.RUNTIME)
public @interface JSON {
String name() default "";
diff --git
a/plugins/json/src/main/java/org/apache/struts2/json/annotations/JSONFieldBridge.java
b/plugins/json/src/main/java/org/apache/struts2/json/annotations/JSONFieldBridge.java
index 799edd76b..68a83d1c0 100644
---
a/plugins/json/src/main/java/org/apache/struts2/json/annotations/JSONFieldBridge.java
+++
b/plugins/json/src/main/java/org/apache/struts2/json/annotations/JSONFieldBridge.java
@@ -26,7 +26,7 @@ import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
-@Target(ElementType.METHOD)
+@Target({ElementType.METHOD, ElementType.RECORD_COMPONENT})
@Retention(RetentionPolicy.RUNTIME)
public @interface JSONFieldBridge {
Class<? extends FieldBridge> impl() default StringBridge.class;
diff --git
a/plugins/json/src/test/java/org/apache/struts2/json/StrutsJSONWriterTest.java
b/plugins/json/src/test/java/org/apache/struts2/json/StrutsJSONWriterTest.java
index acc52dcaa..2b37f6ca4 100644
---
a/plugins/json/src/test/java/org/apache/struts2/json/StrutsJSONWriterTest.java
+++
b/plugins/json/src/test/java/org/apache/struts2/json/StrutsJSONWriterTest.java
@@ -18,6 +18,7 @@
*/
package org.apache.struts2.json;
+import org.apache.struts2.json.annotations.JSON;
import org.apache.struts2.json.annotations.JSONFieldBridge;
import org.apache.struts2.junit.util.TestUtils;
import org.junit.Test;
@@ -38,7 +39,9 @@ import java.util.Calendar;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
+import java.util.Optional;
import java.util.TimeZone;
+import java.util.regex.Pattern;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
@@ -315,4 +318,160 @@ public class StrutsJSONWriterTest {
assertTrue(json.contains("\"calendar\":\"2012-12-23T10:10:10\""));
}
+ @Test
+ public void testSerializeSimpleRecord() throws Exception {
+ record Person(String name, int age) {}
+ Person r = new Person("Alice", 30);
+ JSONWriter jsonWriter = new StrutsJSONWriter();
+ String json = jsonWriter.write(r);
+ assertEquals("{\"name\":\"Alice\",\"age\":30}", json);
+ }
+
+ @Test
+ public void testSerializeRecordWithNullField() throws Exception {
+ record RecordWithNullField(String name, String optional) {}
+ RecordWithNullField r = new RecordWithNullField("Bob", null);
+ JSONWriter jsonWriter = new StrutsJSONWriter();
+ String withNullJson = jsonWriter.write(r);
+ assertEquals("{\"name\":\"Bob\",\"optional\":null}", withNullJson);
+ String nonNullJson = jsonWriter.write(r, null, null, true);
+ assertEquals("{\"name\":\"Bob\"}", nonNullJson);
+ }
+
+
+ @Test
+ public void testSerializeNestedRecord() throws Exception {
+ record InnerRecord(String label) {}
+ record OuterRecord(String label, InnerRecord inner) {}
+ OuterRecord r = new OuterRecord("outer", new InnerRecord("inner"));
+ JSONWriter jsonWriter = new StrutsJSONWriter();
+ String json = jsonWriter.write(r);
+ assertEquals("{\"label\":\"outer\",\"inner\":{\"label\":\"inner\"}}",
json);
+ }
+
+ @Test
+ public void testSerializeRecordSkipsComponentAnnotatedWithSerializeFalse()
throws Exception {
+ record RecordWithAnnotatedComponent(@JSON(serialize = false) String
secret, String visible) {}
+ RecordWithAnnotatedComponent r = new
RecordWithAnnotatedComponent("topsecret", "hello");
+ JSONWriter jsonWriter = new StrutsJSONWriter();
+ String json = jsonWriter.write(r);
+ assertFalse("secret component should be excluded",
json.contains("secret"));
+ assertTrue(json.contains("\"visible\":\"hello\""));
+ }
+
+ @Test
+ public void testSerializeRecordUsesRenamedComponentFromJsonAnnotation()
throws Exception {
+ record RecordWithRenamedComponent(@JSON(name = "fullName") String
name, int age) {}
+ RecordWithRenamedComponent r = new RecordWithRenamedComponent("Alice",
30);
+ JSONWriter jsonWriter = new StrutsJSONWriter();
+ String json = jsonWriter.write(r);
+ assertFalse("original key 'name' should not appear",
json.contains("\"name\""));
+ assertTrue(json.contains("\"fullName\":\"Alice\""));
+ assertTrue(json.contains("\"age\":30"));
+ }
+
+ @Test
+ public void testSerializeRecordWithListField() throws Exception {
+ record RecordWithList(String title, List<String> tags) {}
+ RecordWithList r = new RecordWithList("news", List.of("java",
"records"));
+ JSONWriter jsonWriter = new StrutsJSONWriter();
+ String json = jsonWriter.write(r);
+ assertEquals("{\"title\":\"news\",\"tags\":[\"java\",\"records\"]}",
json);
+ }
+
+ @Test
+ public void testSerializeRecordIncludePropertyFilter() throws Exception {
+ record Person(String name, int age) {}
+ Person r = new Person("Alice", 30);
+ JSONWriter jsonWriter = new StrutsJSONWriter();
+ List<Pattern> include = List.of(Pattern.compile("name"));
+ String json = jsonWriter.write(r, null, include, false);
+ assertTrue(json.contains("\"name\""));
+ assertFalse(json.contains("\"age\""));
+ }
+
+ @Test
+ public void testSerializeRecordExcludePropertyFilter() throws Exception {
+ record Person(String name, int age) {}
+ Person r = new Person("Alice", 30);
+ JSONWriter jsonWriter = new StrutsJSONWriter();
+ List<Pattern> exclude = List.of(Pattern.compile("age"));
+ String json = jsonWriter.write(r, exclude, null, false);
+ assertTrue(json.contains("\"name\""));
+ assertFalse(json.contains("\"age\""));
+ }
+
+ @Test
+ public void testSerializeRecordWithFieldBridge() throws Exception {
+ record LinkRecord(@JSONFieldBridge URL homepage, String name) {}
+ URL url = URI.create("https://www.google.com").toURL();
+ LinkRecord r = new LinkRecord(url, "Struts");
+ JSONWriter jsonWriter = new StrutsJSONWriter();
+ String json = jsonWriter.write(r);
+
assertTrue(json.contains("\"homepage\":\"https:\\/\\/www.google.com\""));
+ assertTrue(json.contains("\"name\":\"Struts\""));
+ }
+
+ @Test
+ public void testSerializeOptionalPresent() throws Exception {
+ JSONWriter jsonWriter = new StrutsJSONWriter();
+ assertEquals("\"hello\"", jsonWriter.write(Optional.of("hello")));
+ }
+
+ @Test
+ public void testSerializeOptionalEmpty() throws Exception {
+ JSONWriter jsonWriter = new StrutsJSONWriter();
+ assertEquals("null", jsonWriter.write(Optional.empty()));
+ }
+
+ class BeanWithOptional {
+ private String field;
+
+ public Optional<String> getField() {
+ return Optional.ofNullable(field);
+ }
+
+ public void setField(String field) {
+ this.field = field;
+ }
+ }
+
+ @Test
+ public void testSerializeBeanWithOptionalWithNull() throws Exception {
+ BeanWithOptional bean = new BeanWithOptional();
+ bean.setField(null);
+ JSONWriter jsonWriter = new StrutsJSONWriter();
+ String json = jsonWriter.write(bean);
+ assertTrue(json.contains("\"field\":null"));
+ }
+
+ @Test
+ public void testSerializeBeanWithOptionalWithValue() throws Exception {
+ BeanWithOptional bean = new BeanWithOptional();
+ bean.setField("value");
+ JSONWriter jsonWriter = new StrutsJSONWriter();
+ String json = jsonWriter.write(bean);
+ assertTrue(json.contains("\"field\":\"value\""));
+ }
+
+ @Test
+ public void
testMapWithOptionalEmptyValueExcludedWhenExcludeNullProperties() throws
Exception {
+ Map<String, Object> map = new LinkedHashMap<>();
+ map.put("present", Optional.of("hello"));
+ map.put("absent", Optional.empty());
+ JSONWriter jsonWriter = new StrutsJSONWriter();
+ String json = jsonWriter.write(map, null, null, true);
+ assertEquals("{\"present\":\"hello\"}", json);
+ }
+
+ @Test
+ public void testMapWithOptionalEmptyValueIncludedAsNull() throws Exception
{
+ Map<String, Object> map = new LinkedHashMap<>();
+ map.put("present", Optional.of("hello"));
+ map.put("absent", Optional.empty());
+ JSONWriter jsonWriter = new StrutsJSONWriter();
+ String json = jsonWriter.write(map, null, null, false);
+ assertEquals("{\"present\":\"hello\",\"absent\":null}", json);
+ }
+
}