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);
+    }
+
 }

Reply via email to