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)