diff --git a/jr-objects/src/main/java/com/fasterxml/jackson/jr/ob/JSON.java b/jr-objects/src/main/java/com/fasterxml/jackson/jr/ob/JSON.java
index 49f84c90..267fb6f7 100644
--- a/jr-objects/src/main/java/com/fasterxml/jackson/jr/ob/JSON.java
+++ b/jr-objects/src/main/java/com/fasterxml/jackson/jr/ob/JSON.java
@@ -261,6 +261,17 @@ public enum Feature
*/
USE_IS_GETTERS(true, true),
+ /**
+ * Feature that provides serialization support for Groovy & Java 17 records, by allowing
+ * reading of "non-get-getters" in a class, (like for a field named amount
+ * the getter would be amount()
).
+ *
+ * @implNote
Feature is disabled by default for backward compatibility.
+ *
+ * @since 2.17
+ */
+ USE_FIELD_MATCHING_GETTERS(false,true),
+
/**
* Feature that enables use of public fields instead of setters and getters,
* in cases where no setter/getter is available.
diff --git a/jr-objects/src/main/java/com/fasterxml/jackson/jr/ob/impl/BeanPropertyIntrospector.java b/jr-objects/src/main/java/com/fasterxml/jackson/jr/ob/impl/BeanPropertyIntrospector.java
index 59548f8d..4a46b302 100644
--- a/jr-objects/src/main/java/com/fasterxml/jackson/jr/ob/impl/BeanPropertyIntrospector.java
+++ b/jr-objects/src/main/java/com/fasterxml/jackson/jr/ob/impl/BeanPropertyIntrospector.java
@@ -1,9 +1,7 @@
package com.fasterxml.jackson.jr.ob.impl;
-import java.lang.reflect.Constructor;
-import java.lang.reflect.Field;
-import java.lang.reflect.Method;
-import java.lang.reflect.Modifier;
+import java.lang.reflect.*;
+import java.util.HashMap;
import java.util.Map;
import java.util.TreeMap;
@@ -98,17 +96,22 @@ private static void _introspect(Class> currType, Map prop
_introspect(currType.getSuperclass(), props, features);
final boolean noStatics = JSON.Feature.INCLUDE_STATIC_FIELDS.isDisabled(features);
+ final boolean isFieldNameGettersEnabled = JSON.Feature.USE_FIELD_MATCHING_GETTERS.isEnabled(features);
+
+ final Map fieldNameMap = isFieldNameGettersEnabled ? new HashMap<>() : null;
+
// then public fields (since 2.8); may or may not be ultimately included
// but at this point still possible
for (Field f : currType.getDeclaredFields()) {
- if (!Modifier.isPublic(f.getModifiers())
- || f.isEnumConstant() || f.isSynthetic()) {
+ if (fieldNameMap != null) {
+ fieldNameMap.put(f.getName(), f);
+ }
+ if (!Modifier.isPublic(f.getModifiers()) || f.isEnumConstant() || f.isSynthetic()) {
continue;
}
// Only include static members if (a) inclusion feature enabled and
// (b) not final (cannot deserialize final fields)
- if (Modifier.isStatic(f.getModifiers())
- && (noStatics || Modifier.isFinal(f.getModifiers()))) {
+ if (Modifier.isStatic(f.getModifiers()) && (noStatics || Modifier.isFinal(f.getModifiers()))) {
continue;
}
_propFrom(props, f.getName()).withField(f);
@@ -145,6 +148,17 @@ private static void _introspect(Class> currType, Map prop
name = decap(name.substring(2));
_propFrom(props, name).withIsGetter(m);
}
+ } else if (isFieldNameGettersEnabled) {
+ // 10-Mar-2024: [jackson-jr#94]:
+ // This will allow getters with field name as their getters,
+ // like the ones generated by Groovy (or JDK 17 for Records).
+ // If method name matches with field name, & method return
+ // type matches the field type only then it can be considered a getter.
+ Field field = fieldNameMap.get(name);
+ if (field != null && Modifier.isPublic(m.getModifiers()) && m.getReturnType().equals(field.getType())) {
+ // NOTE: do NOT decap, field name should be used as-is
+ _propFrom(props, name).withGetter(m);
+ }
}
} else if (argTypes.length == 1) { // setter?
// Non-public setters are fine if we can force access, don't yet check
diff --git a/jr-objects/src/test/java/com/fasterxml/jackson/jr/ob/ReadRecordLikeTest.java b/jr-objects/src/test/java/com/fasterxml/jackson/jr/ob/ReadRecordLikeTest.java
new file mode 100644
index 00000000..f14c0b8b
--- /dev/null
+++ b/jr-objects/src/test/java/com/fasterxml/jackson/jr/ob/ReadRecordLikeTest.java
@@ -0,0 +1,37 @@
+package com.fasterxml.jackson.jr.ob;
+
+// For [jackson-jr#94]: support for Serializing JDK 17/Groovy records
+// (minimal one; full test in separate test package)
+//
+// @since 2.17
+public class ReadRecordLikeTest extends TestBase
+{
+ static class RecordLike94 {
+ int count = 3;
+ int STATUS = 500;
+ int foobar;
+
+ // should be discovered:
+ public int count() { return count; }
+ // likewise:
+ public int STATUS() { return STATUS; }
+
+ // should NOT be discovered (takes argument(s))
+ public int foobar(int value) {
+ foobar = value;
+ return value;
+ }
+
+ // also not to be discovered
+ public int mismatched() { return 42; }
+ }
+
+ public void testRecordLikePOJO() throws Exception
+ {
+ // By default, do not auto-detect "record-style" accessors
+ assertEquals("{}", JSON.std.asString(new RecordLike94()));
+
+ assertEquals(a2q("{'STATUS':500,'count':3}"), JSON.std.with(JSON.Feature.USE_FIELD_MATCHING_GETTERS)
+ .asString(new RecordLike94()));
+ }
+}
diff --git a/jr-test-module/src/test/groovy/GroovyObjectSupportTest.groovy b/jr-test-module/src/test/groovy/GroovyObjectSupportTest.groovy
index b19e70b7..7bac1e08 100644
--- a/jr-test-module/src/test/groovy/GroovyObjectSupportTest.groovy
+++ b/jr-test-module/src/test/groovy/GroovyObjectSupportTest.groovy
@@ -2,6 +2,11 @@ import com.fasterxml.jackson.jr.ob.JSON
import org.junit.Assert
import org.junit.Test
+/**
+ * A minor note on running/debugging this test on local, if you are using intellij, please
+ * change `pom` to `bundle`. this is causing
+ * some issue with the IDE.
+ */
class GroovyObjectSupportTest {
@Test
void testSimpleGroovyObject() throws Exception {
diff --git a/jr-test-module/src/test/groovy/GroovyRecordsTest.groovy b/jr-test-module/src/test/groovy/GroovyRecordsTest.groovy
new file mode 100644
index 00000000..dbf0fa49
--- /dev/null
+++ b/jr-test-module/src/test/groovy/GroovyRecordsTest.groovy
@@ -0,0 +1,67 @@
+import com.fasterxml.jackson.jr.ob.JSON
+import org.junit.Assert
+import org.junit.Test
+
+/**
+ * A minor note on running/debugging this test on local, if you are using intellij, please
+ * change `pom` to `bundle`. this is causing
+ * some issue with the IDE.
+*/
+class GroovyRecordsTest {
+
+ @Test
+ void testRecord() throws Exception {
+ /* We need to use this since build (8, ubuntu-20.04), will fail Map.of() was added in Java 9*/
+ def map = new HashMap()
+ map.put("foo", "bar")
+
+ def json = JSON.builder().enable(JSON.Feature.USE_FIELD_MATCHING_GETTERS).build().asString(new Cow("foo", map))
+ def expected = """{"message":"foo","object":{"foo":"bar"}}"""
+ Assert.assertEquals(expected, json)
+ }
+
+ @Test
+ void testRecordEquivalentObjects() throws Exception {
+ def expected = """{"message":"foo","object":{"foo":"bar"}}"""
+
+ /* We need to use this since build (8, ubuntu-20.04), will fail Map.of() was added in Java 9*/
+ def map = new HashMap()
+ map.put("foo", "bar")
+
+ def json = JSON.builder().enable(JSON.Feature.USE_FIELD_MATCHING_GETTERS).build().asString(new SimpleGroovyObject("foo", map))
+ Assert.assertEquals(expected, json)
+
+ def json2 = JSON.builder().enable(JSON.Feature.USE_FIELD_MATCHING_GETTERS).build().asString(new GroovyObjectWithNamedGetters("foo", map))
+ Assert.assertEquals(expected, json2)
+ }
+}
+
+class SimpleGroovyObject {
+ public final String message
+ public final Map object
+
+ SimpleGroovyObject(String message, Map object) {
+ this.message = message
+ this.object = object
+ }
+}
+
+class GroovyObjectWithNamedGetters {
+ private final String message
+ private final Map object
+
+ GroovyObjectWithNamedGetters(String message, Map object) {
+ this.message = message
+ this.object = object
+ }
+
+ String message() {
+ return message
+ }
+
+ Map object() {
+ return object
+ }
+}
+
+record Cow(String message, Map object) {}
\ No newline at end of file
diff --git a/jr-test-module/src/test/java/Java17RecordTest.java b/jr-test-module/src/test/java/Java17RecordTest.java
new file mode 100644
index 00000000..a3006279
--- /dev/null
+++ b/jr-test-module/src/test/java/Java17RecordTest.java
@@ -0,0 +1,22 @@
+import com.fasterxml.jackson.jr.ob.JSON;
+import org.junit.Assert;
+import org.junit.Test;
+
+import java.io.IOException;
+import java.util.Map;
+
+/**
+ * This test is in test module since the JDK version to be tested is higher than other, and hence supports Records.
+ */
+public class Java17RecordTest {
+
+ @Test
+ public void testJava14RecordSupport() throws IOException {
+ var expectedString = "{\"message\":\"MOO\",\"object\":{\"Foo\":\"Bar\"}}";
+ var json = JSON.builder().enable(JSON.Feature.USE_FIELD_MATCHING_GETTERS).build().asString(new Cow("MOO", Map.of("Foo", "Bar")));
+ Assert.assertEquals(expectedString, json);
+ }
+
+ record Cow(String message, Map object) {
+ }
+}
diff --git a/release-notes/CREDITS-2.x b/release-notes/CREDITS-2.x
index fa9294e0..4c8907d1 100644
--- a/release-notes/CREDITS-2.x
+++ b/release-notes/CREDITS-2.x
@@ -57,5 +57,7 @@ Julian Honnen (@jhonnen)
* Contributed fix for #93: Skip serialization of `groovy.lang.MetaClass` values
to avoid `StackOverflowError`
(2.17.0)
+* Constributed implementation of #94: Support for serializing Java Records
+ (2.17.0)
* Contributed impl for #100: Add support for `java.time` (Java 8 date/time) types
(2.17.0)
diff --git a/release-notes/VERSION-2.x b/release-notes/VERSION-2.x
index 1dea6504..b87e52cf 100644
--- a/release-notes/VERSION-2.x
+++ b/release-notes/VERSION-2.x
@@ -17,6 +17,8 @@ Not yet released
(contributed by @Shounaks)
#51: Duplicate key detection does not work for (simple) Trees
(contributed by @Shounaks)
+#94: Support for serializing Java Records
+ (implementation contributed by @Shounaks)
#131: Add mechanism for `JacksonJrExtension`s to access state of `JSON.Feature`s
2.17.0-rc1 (26-Feb-2024)