From 6e08b939b57d88e30ab3b6706b91160fbecefac0 Mon Sep 17 00:00:00 2001 From: Shalnark <65479699+Shounaks@users.noreply.github.com> Date: Thu, 7 Mar 2024 08:03:13 +0530 Subject: [PATCH 01/10] Adding Implementation + Groovy and Java Tests --- .../com/fasterxml/jackson/jr/ob/JSON.java | 7 +++ .../jr/ob/impl/BeanPropertyIntrospector.java | 21 ++++++- .../groovy/GroovyObjectSupportTest.groovy | 5 ++ .../src/test/groovy/GroovyRecordsTest.groovy | 59 +++++++++++++++++++ .../src/test/java/Java14RecordTest.java | 22 +++++++ 5 files changed, 112 insertions(+), 2 deletions(-) create mode 100644 jr-test-module/src/test/groovy/GroovyRecordsTest.groovy create mode 100644 jr-test-module/src/test/java/Java14RecordTest.java 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 3e3bf115..e168e463 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,13 @@ public enum Feature */ USE_IS_GETTERS(true, true), + /** + * Feature that provides support for Groovy & JDK14 records, by allowing + * reading of "non-get-getters" in a class, (like for a field named amount + * the getter would be amount()) + * */ + USE_FIELD_NAME_GETTERS(true,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..cdb09895 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 @@ -4,13 +4,17 @@ import java.lang.reflect.Field; import java.lang.reflect.Method; import java.lang.reflect.Modifier; +import java.util.Arrays; import java.util.Map; import java.util.TreeMap; -import com.fasterxml.jackson.jr.ob.JSON; import com.fasterxml.jackson.jr.ob.impl.POJODefinition.Prop; import com.fasterxml.jackson.jr.ob.impl.POJODefinition.PropBuilder; + +import static com.fasterxml.jackson.jr.ob.JSON.Feature.INCLUDE_STATIC_FIELDS; +import static com.fasterxml.jackson.jr.ob.JSON.Feature.USE_FIELD_NAME_GETTERS; + /** * Helper class that jackson-jr uses by default to introspect POJO properties * (represented as {@link POJODefinition}) to build general POJO readers @@ -97,7 +101,9 @@ private static void _introspect(Class currType, Map prop // First, check base type _introspect(currType.getSuperclass(), props, features); - final boolean noStatics = JSON.Feature.INCLUDE_STATIC_FIELDS.isDisabled(features); + final boolean noStatics = INCLUDE_STATIC_FIELDS.isDisabled(features); + final boolean isFieldNameGettersEnabled = USE_FIELD_NAME_GETTERS.isEnabled(features); + // then public fields (since 2.8); may or may not be ultimately included // but at this point still possible for (Field f : currType.getDeclaredFields()) { @@ -146,6 +152,17 @@ private static void _introspect(Class currType, Map prop _propFrom(props, name).withIsGetter(m); } } + else if (isFieldNameGettersEnabled) { + // This will allow getters with field name as their getters, like the ones generated by Groovy + // If method name matches with field name, & method return type matches with field type + // only then it can be considered a direct name getter. + final String decapName = name; + Arrays.stream(currType.getDeclaredFields()) + .filter(f -> f.getName().equals(m.getName())) + .filter(f -> Modifier.isPublic(m.getModifiers()) && m.getReturnType().equals(f.getType())) + .findFirst() + .ifPresent(f -> _propFrom(props, decap(decapName)).withGetter(m)); + } } else if (argTypes.length == 1) { // setter? // Non-public setters are fine if we can force access, don't yet check // let's also not bother about return type; setters that return value are fine 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..468e31bb --- /dev/null +++ b/jr-test-module/src/test/groovy/GroovyRecordsTest.groovy @@ -0,0 +1,59 @@ +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 { + def json = JSON.std.asString(new Cow("foo", Map.of("foo", "bar"))) + def expected = """{"message":"foo","object":{"foo":"bar"}}""" + Assert.assertEquals(expected, json) + } + + @Test + void testRecordEquivalentObjects() throws Exception { + def expected = """{"message":"foo","object":{"foo":"bar"}}""" + + def json = JSON.std.asString(new SimpleGroovyObject("foo", Map.of("foo", "bar"))) + Assert.assertEquals(expected, json) + + def json2 = JSON.std.asString(new GroovyObjectWithNamedGetters("foo", Map.of("foo", "bar"))) + 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/Java14RecordTest.java b/jr-test-module/src/test/java/Java14RecordTest.java new file mode 100644 index 00000000..ad02214f --- /dev/null +++ b/jr-test-module/src/test/java/Java14RecordTest.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 Java14RecordTest { + + @Test + public void testJava14RecordSupport() throws IOException { + var expectedString = "{\"message\":\"MOO\",\"object\":{\"Foo\":\"Bar\"}}"; + var json = JSON.std.asString(new Cow("MOO", Map.of("Foo", "Bar"))); + Assert.assertEquals(expectedString, json); + } + + record Cow(String message, Map object) { + } +} From e71890e8631eb598cc8b3ceade217b6fb6e4996e Mon Sep 17 00:00:00 2001 From: Shalnark <65479699+Shounaks@users.noreply.github.com> Date: Thu, 7 Mar 2024 19:03:15 +0530 Subject: [PATCH 02/10] Disabling Setter as default behaviour. --- .../src/main/java/com/fasterxml/jackson/jr/ob/JSON.java | 2 +- jr-test-module/src/test/groovy/GroovyRecordsTest.groovy | 6 +++--- jr-test-module/src/test/java/Java14RecordTest.java | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) 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 e168e463..aa2ff274 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 @@ -266,7 +266,7 @@ public enum Feature * reading of "non-get-getters" in a class, (like for a field named amount * the getter would be amount()) * */ - USE_FIELD_NAME_GETTERS(true,true), + USE_FIELD_NAME_GETTERS(false,true), /** * Feature that enables use of public fields instead of setters and getters, diff --git a/jr-test-module/src/test/groovy/GroovyRecordsTest.groovy b/jr-test-module/src/test/groovy/GroovyRecordsTest.groovy index 468e31bb..0712cad2 100644 --- a/jr-test-module/src/test/groovy/GroovyRecordsTest.groovy +++ b/jr-test-module/src/test/groovy/GroovyRecordsTest.groovy @@ -11,7 +11,7 @@ class GroovyRecordsTest { @Test void testRecord() throws Exception { - def json = JSON.std.asString(new Cow("foo", Map.of("foo", "bar"))) + def json = JSON.builder().enable(JSON.Feature.USE_FIELD_NAME_GETTERS).build().asString(new Cow("foo", Map.of("foo", "bar"))) def expected = """{"message":"foo","object":{"foo":"bar"}}""" Assert.assertEquals(expected, json) } @@ -20,10 +20,10 @@ class GroovyRecordsTest { void testRecordEquivalentObjects() throws Exception { def expected = """{"message":"foo","object":{"foo":"bar"}}""" - def json = JSON.std.asString(new SimpleGroovyObject("foo", Map.of("foo", "bar"))) + def json = JSON.builder().enable(JSON.Feature.USE_FIELD_NAME_GETTERS).build().asString(new SimpleGroovyObject("foo", Map.of("foo", "bar"))) Assert.assertEquals(expected, json) - def json2 = JSON.std.asString(new GroovyObjectWithNamedGetters("foo", Map.of("foo", "bar"))) + def json2 = JSON.builder().enable(JSON.Feature.USE_FIELD_NAME_GETTERS).build().asString(new GroovyObjectWithNamedGetters("foo", Map.of("foo", "bar"))) Assert.assertEquals(expected, json2) } } diff --git a/jr-test-module/src/test/java/Java14RecordTest.java b/jr-test-module/src/test/java/Java14RecordTest.java index ad02214f..5500b2ac 100644 --- a/jr-test-module/src/test/java/Java14RecordTest.java +++ b/jr-test-module/src/test/java/Java14RecordTest.java @@ -13,7 +13,7 @@ public class Java14RecordTest { @Test public void testJava14RecordSupport() throws IOException { var expectedString = "{\"message\":\"MOO\",\"object\":{\"Foo\":\"Bar\"}}"; - var json = JSON.std.asString(new Cow("MOO", Map.of("Foo", "Bar"))); + var json = JSON.builder().enable(JSON.Feature.USE_FIELD_NAME_GETTERS).build().asString(new Cow("MOO", Map.of("Foo", "Bar"))); Assert.assertEquals(expectedString, json); } From 5263e9ae45c48e331f8a064711d7290b74fedfa8 Mon Sep 17 00:00:00 2001 From: Shalnark <65479699+Shounaks@users.noreply.github.com> Date: Thu, 7 Mar 2024 19:11:18 +0530 Subject: [PATCH 03/10] Fix testcase for Java8 --- .../src/test/groovy/GroovyRecordsTest.groovy | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/jr-test-module/src/test/groovy/GroovyRecordsTest.groovy b/jr-test-module/src/test/groovy/GroovyRecordsTest.groovy index 0712cad2..4d56bb8f 100644 --- a/jr-test-module/src/test/groovy/GroovyRecordsTest.groovy +++ b/jr-test-module/src/test/groovy/GroovyRecordsTest.groovy @@ -11,7 +11,11 @@ class GroovyRecordsTest { @Test void testRecord() throws Exception { - def json = JSON.builder().enable(JSON.Feature.USE_FIELD_NAME_GETTERS).build().asString(new Cow("foo", Map.of("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_NAME_GETTERS).build().asString(new Cow("foo", map)) def expected = """{"message":"foo","object":{"foo":"bar"}}""" Assert.assertEquals(expected, json) } @@ -20,10 +24,14 @@ class GroovyRecordsTest { void testRecordEquivalentObjects() throws Exception { def expected = """{"message":"foo","object":{"foo":"bar"}}""" - def json = JSON.builder().enable(JSON.Feature.USE_FIELD_NAME_GETTERS).build().asString(new SimpleGroovyObject("foo", Map.of("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_NAME_GETTERS).build().asString(new SimpleGroovyObject("foo", map)) Assert.assertEquals(expected, json) - def json2 = JSON.builder().enable(JSON.Feature.USE_FIELD_NAME_GETTERS).build().asString(new GroovyObjectWithNamedGetters("foo", Map.of("foo", "bar"))) + def json2 = JSON.builder().enable(JSON.Feature.USE_FIELD_NAME_GETTERS).build().asString(new GroovyObjectWithNamedGetters("foo", map)) Assert.assertEquals(expected, json2) } } From 7500259110c5730879d0bebee59028c22704d536 Mon Sep 17 00:00:00 2001 From: Shalnark <65479699+Shounaks@users.noreply.github.com> Date: Fri, 8 Mar 2024 11:26:46 +0530 Subject: [PATCH 04/10] Update Documentation --- .../src/main/java/com/fasterxml/jackson/jr/ob/JSON.java | 6 ++++-- .../jackson/jr/ob/impl/BeanPropertyIntrospector.java | 4 ++-- jr-test-module/src/test/groovy/GroovyRecordsTest.groovy | 6 +++--- .../java/{Java14RecordTest.java => Java17RecordTest.java} | 4 ++-- 4 files changed, 11 insertions(+), 9 deletions(-) rename jr-test-module/src/test/java/{Java14RecordTest.java => Java17RecordTest.java} (85%) 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 aa2ff274..e172690e 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 @@ -262,11 +262,13 @@ public enum Feature USE_IS_GETTERS(true, true), /** - * Feature that provides support for Groovy & JDK14 records, by allowing + * Feature that provides serialization support for Groovy & JDK14 records, by allowing * reading of "non-get-getters" in a class, (like for a field named amount * the getter would be amount()) + * @since 2.17 + * @implNote

Default state is false due to backward compatibility.

* */ - USE_FIELD_NAME_GETTERS(false,true), + USE_FIELD_MATCHING_GETTERS(false,true), /** * Feature that enables use of public fields instead of setters and getters, 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 cdb09895..9b20bec6 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 @@ -13,7 +13,7 @@ import static com.fasterxml.jackson.jr.ob.JSON.Feature.INCLUDE_STATIC_FIELDS; -import static com.fasterxml.jackson.jr.ob.JSON.Feature.USE_FIELD_NAME_GETTERS; +import static com.fasterxml.jackson.jr.ob.JSON.Feature.USE_FIELD_MATCHING_GETTERS; /** * Helper class that jackson-jr uses by default to introspect POJO properties @@ -102,7 +102,7 @@ private static void _introspect(Class currType, Map prop _introspect(currType.getSuperclass(), props, features); final boolean noStatics = INCLUDE_STATIC_FIELDS.isDisabled(features); - final boolean isFieldNameGettersEnabled = USE_FIELD_NAME_GETTERS.isEnabled(features); + final boolean isFieldNameGettersEnabled = USE_FIELD_MATCHING_GETTERS.isEnabled(features); // then public fields (since 2.8); may or may not be ultimately included // but at this point still possible diff --git a/jr-test-module/src/test/groovy/GroovyRecordsTest.groovy b/jr-test-module/src/test/groovy/GroovyRecordsTest.groovy index 4d56bb8f..dbf0fa49 100644 --- a/jr-test-module/src/test/groovy/GroovyRecordsTest.groovy +++ b/jr-test-module/src/test/groovy/GroovyRecordsTest.groovy @@ -15,7 +15,7 @@ class GroovyRecordsTest { def map = new HashMap() map.put("foo", "bar") - def json = JSON.builder().enable(JSON.Feature.USE_FIELD_NAME_GETTERS).build().asString(new Cow("foo", map)) + 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) } @@ -28,10 +28,10 @@ class GroovyRecordsTest { def map = new HashMap() map.put("foo", "bar") - def json = JSON.builder().enable(JSON.Feature.USE_FIELD_NAME_GETTERS).build().asString(new SimpleGroovyObject("foo", map)) + 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_NAME_GETTERS).build().asString(new GroovyObjectWithNamedGetters("foo", map)) + def json2 = JSON.builder().enable(JSON.Feature.USE_FIELD_MATCHING_GETTERS).build().asString(new GroovyObjectWithNamedGetters("foo", map)) Assert.assertEquals(expected, json2) } } diff --git a/jr-test-module/src/test/java/Java14RecordTest.java b/jr-test-module/src/test/java/Java17RecordTest.java similarity index 85% rename from jr-test-module/src/test/java/Java14RecordTest.java rename to jr-test-module/src/test/java/Java17RecordTest.java index 5500b2ac..a3006279 100644 --- a/jr-test-module/src/test/java/Java14RecordTest.java +++ b/jr-test-module/src/test/java/Java17RecordTest.java @@ -8,12 +8,12 @@ /** * This test is in test module since the JDK version to be tested is higher than other, and hence supports Records. */ -public class Java14RecordTest { +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_NAME_GETTERS).build().asString(new Cow("MOO", Map.of("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); } From 95652ffb634532f00866b00d348657ba2a5c3cb3 Mon Sep 17 00:00:00 2001 From: Shalnark <65479699+Shounaks@users.noreply.github.com> Date: Fri, 8 Mar 2024 15:21:50 +0530 Subject: [PATCH 05/10] Review Comment Fix --- .../jr/ob/impl/BeanPropertyIntrospector.java | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) 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 9b20bec6..c09730a6 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 @@ -4,16 +4,17 @@ import java.lang.reflect.Field; import java.lang.reflect.Method; import java.lang.reflect.Modifier; -import java.util.Arrays; import java.util.Map; import java.util.TreeMap; import com.fasterxml.jackson.jr.ob.impl.POJODefinition.Prop; import com.fasterxml.jackson.jr.ob.impl.POJODefinition.PropBuilder; - import static com.fasterxml.jackson.jr.ob.JSON.Feature.INCLUDE_STATIC_FIELDS; import static com.fasterxml.jackson.jr.ob.JSON.Feature.USE_FIELD_MATCHING_GETTERS; +import static java.util.Arrays.stream; +import static java.util.function.Function.identity; +import static java.util.stream.Collectors.toMap; /** * Helper class that jackson-jr uses by default to introspect POJO properties @@ -156,12 +157,14 @@ else if (isFieldNameGettersEnabled) { // This will allow getters with field name as their getters, like the ones generated by Groovy // If method name matches with field name, & method return type matches with field type // only then it can be considered a direct name getter. - final String decapName = name; - Arrays.stream(currType.getDeclaredFields()) - .filter(f -> f.getName().equals(m.getName())) - .filter(f -> Modifier.isPublic(m.getModifiers()) && m.getReturnType().equals(f.getType())) - .findFirst() - .ifPresent(f -> _propFrom(props, decap(decapName)).withGetter(m)); + final Map fieldNameMap = + stream(currType.getDeclaredFields()).collect(toMap(Field::getName, identity())); + + final Field field = fieldNameMap.get(name); + + if(Modifier.isPublic(m.getModifiers()) && field != null && m.getReturnType().equals(field.getType())) { + _propFrom(props, decap(field.getName())).withGetter(m); + } } } else if (argTypes.length == 1) { // setter? // Non-public setters are fine if we can force access, don't yet check From 5d8d79457543bec555d404a3e749220940d7b2a4 Mon Sep 17 00:00:00 2001 From: Shalnark <65479699+Shounaks@users.noreply.github.com> Date: Sun, 10 Mar 2024 06:36:25 +0530 Subject: [PATCH 06/10] using hashmap to pre-register fields --- .../jr/ob/impl/BeanPropertyIntrospector.java | 19 +++++++------------ .../jackson/jr/ob/impl/POJODefinition.java | 4 ++++ 2 files changed, 11 insertions(+), 12 deletions(-) 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 c09730a6..8d0b5983 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 @@ -4,6 +4,7 @@ import java.lang.reflect.Field; import java.lang.reflect.Method; import java.lang.reflect.Modifier; +import java.util.HashMap; import java.util.Map; import java.util.TreeMap; @@ -12,9 +13,6 @@ import static com.fasterxml.jackson.jr.ob.JSON.Feature.INCLUDE_STATIC_FIELDS; import static com.fasterxml.jackson.jr.ob.JSON.Feature.USE_FIELD_MATCHING_GETTERS; -import static java.util.Arrays.stream; -import static java.util.function.Function.identity; -import static java.util.stream.Collectors.toMap; /** * Helper class that jackson-jr uses by default to introspect POJO properties @@ -105,17 +103,18 @@ private static void _introspect(Class currType, Map prop final boolean noStatics = INCLUDE_STATIC_FIELDS.isDisabled(features); final boolean isFieldNameGettersEnabled = USE_FIELD_MATCHING_GETTERS.isEnabled(features); + final Map fieldNameMap = new HashMap<>(); + // 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()) { + 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); @@ -157,11 +156,7 @@ else if (isFieldNameGettersEnabled) { // This will allow getters with field name as their getters, like the ones generated by Groovy // If method name matches with field name, & method return type matches with field type // only then it can be considered a direct name getter. - final Map fieldNameMap = - stream(currType.getDeclaredFields()).collect(toMap(Field::getName, identity())); - - final Field field = fieldNameMap.get(name); - + Field field = fieldNameMap.get(name); if(Modifier.isPublic(m.getModifiers()) && field != null && m.getReturnType().equals(field.getType())) { _propFrom(props, decap(field.getName())).withGetter(m); } diff --git a/jr-objects/src/main/java/com/fasterxml/jackson/jr/ob/impl/POJODefinition.java b/jr-objects/src/main/java/com/fasterxml/jackson/jr/ob/impl/POJODefinition.java index e14ee67b..8ddd5bf7 100644 --- a/jr-objects/src/main/java/com/fasterxml/jackson/jr/ob/impl/POJODefinition.java +++ b/jr-objects/src/main/java/com/fasterxml/jackson/jr/ob/impl/POJODefinition.java @@ -176,6 +176,10 @@ public PropBuilder withIsGetter(Method m) { _isGetter = m; return this; } + + public Field get_field() { + return _field; + } } } From a5f55f80e9e3f075e755194054723302ed6daea6 Mon Sep 17 00:00:00 2001 From: Shalnark <65479699+Shounaks@users.noreply.github.com> Date: Sun, 10 Mar 2024 06:54:37 +0530 Subject: [PATCH 07/10] fix: documentation --- jr-objects/src/main/java/com/fasterxml/jackson/jr/ob/JSON.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 b34a541a..565b1278 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 @@ -262,7 +262,7 @@ public enum Feature USE_IS_GETTERS(true, true), /** - * Feature that provides serialization support for Groovy & JDK14 records, by allowing + * 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()) * @since 2.17 From 7b9566988fa865d893cfa21119628703e1253472 Mon Sep 17 00:00:00 2001 From: Shalnark <65479699+Shounaks@users.noreply.github.com> Date: Sun, 10 Mar 2024 06:55:40 +0530 Subject: [PATCH 08/10] fix: remove now-redundant method --- .../java/com/fasterxml/jackson/jr/ob/impl/POJODefinition.java | 4 ---- 1 file changed, 4 deletions(-) diff --git a/jr-objects/src/main/java/com/fasterxml/jackson/jr/ob/impl/POJODefinition.java b/jr-objects/src/main/java/com/fasterxml/jackson/jr/ob/impl/POJODefinition.java index 8ddd5bf7..e14ee67b 100644 --- a/jr-objects/src/main/java/com/fasterxml/jackson/jr/ob/impl/POJODefinition.java +++ b/jr-objects/src/main/java/com/fasterxml/jackson/jr/ob/impl/POJODefinition.java @@ -176,10 +176,6 @@ public PropBuilder withIsGetter(Method m) { _isGetter = m; return this; } - - public Field get_field() { - return _field; - } } } From 3070e11794fe28e2ee09f4a65ee0ed108a1d298f Mon Sep 17 00:00:00 2001 From: Tatu Saloranta Date: Sun, 10 Mar 2024 18:40:41 -0700 Subject: [PATCH 09/10] Update release notes, tweak impl slightly --- .../com/fasterxml/jackson/jr/ob/JSON.java | 8 +++-- .../jr/ob/impl/BeanPropertyIntrospector.java | 35 +++++++++---------- release-notes/CREDITS-2.x | 2 ++ release-notes/VERSION-2.x | 2 ++ 4 files changed, 26 insertions(+), 21 deletions(-) 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 565b1278..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 @@ -264,10 +264,12 @@ public enum Feature /** * 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()) + * the getter would be amount()). + * + * @implNote

Feature is disabled by default for backward compatibility.

+ * * @since 2.17 - * @implNote

Default state is false due to backward compatibility.

- * */ + */ USE_FIELD_MATCHING_GETTERS(false,true), /** 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 8d0b5983..ef33e2b7 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,19 +1,14 @@ 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; +import com.fasterxml.jackson.jr.ob.JSON; import com.fasterxml.jackson.jr.ob.impl.POJODefinition.Prop; import com.fasterxml.jackson.jr.ob.impl.POJODefinition.PropBuilder; -import static com.fasterxml.jackson.jr.ob.JSON.Feature.INCLUDE_STATIC_FIELDS; -import static com.fasterxml.jackson.jr.ob.JSON.Feature.USE_FIELD_MATCHING_GETTERS; - /** * Helper class that jackson-jr uses by default to introspect POJO properties * (represented as {@link POJODefinition}) to build general POJO readers @@ -100,15 +95,17 @@ private static void _introspect(Class currType, Map prop // First, check base type _introspect(currType.getSuperclass(), props, features); - final boolean noStatics = INCLUDE_STATIC_FIELDS.isDisabled(features); - final boolean isFieldNameGettersEnabled = USE_FIELD_MATCHING_GETTERS.isEnabled(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 = new HashMap<>(); + 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()) { - fieldNameMap.put(f.getName(), f); + if (fieldNameMap != null) { + fieldNameMap.put(f.getName(), f); + } if (!Modifier.isPublic(f.getModifiers()) || f.isEnumConstant() || f.isSynthetic()) { continue; } @@ -151,14 +148,16 @@ private static void _introspect(Class currType, Map prop name = decap(name.substring(2)); _propFrom(props, name).withIsGetter(m); } - } - else if (isFieldNameGettersEnabled) { - // This will allow getters with field name as their getters, like the ones generated by Groovy - // If method name matches with field name, & method return type matches with field type - // only then it can be considered a direct name getter. + } 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. + // 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(Modifier.isPublic(m.getModifiers()) && field != null && m.getReturnType().equals(field.getType())) { - _propFrom(props, decap(field.getName())).withGetter(m); + if (field != null && Modifier.isPublic(m.getModifiers()) && m.getReturnType().equals(field.getType())) { + // NOTE: do NOT decap name, field name should be used as-is + _propFrom(props, name).withGetter(m); } } } else if (argTypes.length == 1) { // setter? 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) From f9143bd0f58ea7b12473a2bccbd18457d9366ec7 Mon Sep 17 00:00:00 2001 From: Tatu Saloranta Date: Sun, 10 Mar 2024 18:50:51 -0700 Subject: [PATCH 10/10] Minor tweaks --- .../jr/ob/impl/BeanPropertyIntrospector.java | 4 +- .../jackson/jr/ob/ReadRecordLikeTest.java | 37 +++++++++++++++++++ 2 files changed, 39 insertions(+), 2 deletions(-) create mode 100644 jr-objects/src/test/java/com/fasterxml/jackson/jr/ob/ReadRecordLikeTest.java 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 ef33e2b7..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 @@ -151,12 +151,12 @@ private static void _introspect(Class currType, Map prop } 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. + // 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 name, field name should be used as-is + // NOTE: do NOT decap, field name should be used as-is _propFrom(props, name).withGetter(m); } } 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())); + } +}