Skip to content

Commit 226c0bc

Browse files
authored
Adding support for Java 17 & Groovy Records (#130)
1 parent cec7bac commit 226c0bc

File tree

8 files changed

+168
-8
lines changed

8 files changed

+168
-8
lines changed

jr-objects/src/main/java/com/fasterxml/jackson/jr/ob/JSON.java

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -261,6 +261,17 @@ public enum Feature
261261
*/
262262
USE_IS_GETTERS(true, true),
263263

264+
/**
265+
* Feature that provides serialization support for Groovy & Java 17 records, by allowing
266+
* reading of "non-get-getters" in a class, (like for a field named <code>amount</code>
267+
* the getter would be <code>amount()</code>).
268+
*
269+
* @implNote <p>Feature is disabled by default for backward compatibility.</p>
270+
*
271+
* @since 2.17
272+
*/
273+
USE_FIELD_MATCHING_GETTERS(false,true),
274+
264275
/**
265276
* Feature that enables use of public fields instead of setters and getters,
266277
* in cases where no setter/getter is available.

jr-objects/src/main/java/com/fasterxml/jackson/jr/ob/impl/BeanPropertyIntrospector.java

Lines changed: 22 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,7 @@
11
package com.fasterxml.jackson.jr.ob.impl;
22

3-
import java.lang.reflect.Constructor;
4-
import java.lang.reflect.Field;
5-
import java.lang.reflect.Method;
6-
import java.lang.reflect.Modifier;
3+
import java.lang.reflect.*;
4+
import java.util.HashMap;
75
import java.util.Map;
86
import java.util.TreeMap;
97

@@ -98,17 +96,22 @@ private static void _introspect(Class<?> currType, Map<String, PropBuilder> prop
9896
_introspect(currType.getSuperclass(), props, features);
9997

10098
final boolean noStatics = JSON.Feature.INCLUDE_STATIC_FIELDS.isDisabled(features);
99+
final boolean isFieldNameGettersEnabled = JSON.Feature.USE_FIELD_MATCHING_GETTERS.isEnabled(features);
100+
101+
final Map<String, Field> fieldNameMap = isFieldNameGettersEnabled ? new HashMap<>() : null;
102+
101103
// then public fields (since 2.8); may or may not be ultimately included
102104
// but at this point still possible
103105
for (Field f : currType.getDeclaredFields()) {
104-
if (!Modifier.isPublic(f.getModifiers())
105-
|| f.isEnumConstant() || f.isSynthetic()) {
106+
if (fieldNameMap != null) {
107+
fieldNameMap.put(f.getName(), f);
108+
}
109+
if (!Modifier.isPublic(f.getModifiers()) || f.isEnumConstant() || f.isSynthetic()) {
106110
continue;
107111
}
108112
// Only include static members if (a) inclusion feature enabled and
109113
// (b) not final (cannot deserialize final fields)
110-
if (Modifier.isStatic(f.getModifiers())
111-
&& (noStatics || Modifier.isFinal(f.getModifiers()))) {
114+
if (Modifier.isStatic(f.getModifiers()) && (noStatics || Modifier.isFinal(f.getModifiers()))) {
112115
continue;
113116
}
114117
_propFrom(props, f.getName()).withField(f);
@@ -145,6 +148,17 @@ private static void _introspect(Class<?> currType, Map<String, PropBuilder> prop
145148
name = decap(name.substring(2));
146149
_propFrom(props, name).withIsGetter(m);
147150
}
151+
} else if (isFieldNameGettersEnabled) {
152+
// 10-Mar-2024: [jackson-jr#94]:
153+
// This will allow getters with field name as their getters,
154+
// like the ones generated by Groovy (or JDK 17 for Records).
155+
// If method name matches with field name, & method return
156+
// type matches the field type only then it can be considered a getter.
157+
Field field = fieldNameMap.get(name);
158+
if (field != null && Modifier.isPublic(m.getModifiers()) && m.getReturnType().equals(field.getType())) {
159+
// NOTE: do NOT decap, field name should be used as-is
160+
_propFrom(props, name).withGetter(m);
161+
}
148162
}
149163
} else if (argTypes.length == 1) { // setter?
150164
// Non-public setters are fine if we can force access, don't yet check
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
package com.fasterxml.jackson.jr.ob;
2+
3+
// For [jackson-jr#94]: support for Serializing JDK 17/Groovy records
4+
// (minimal one; full test in separate test package)
5+
//
6+
// @since 2.17
7+
public class ReadRecordLikeTest extends TestBase
8+
{
9+
static class RecordLike94 {
10+
int count = 3;
11+
int STATUS = 500;
12+
int foobar;
13+
14+
// should be discovered:
15+
public int count() { return count; }
16+
// likewise:
17+
public int STATUS() { return STATUS; }
18+
19+
// should NOT be discovered (takes argument(s))
20+
public int foobar(int value) {
21+
foobar = value;
22+
return value;
23+
}
24+
25+
// also not to be discovered
26+
public int mismatched() { return 42; }
27+
}
28+
29+
public void testRecordLikePOJO() throws Exception
30+
{
31+
// By default, do not auto-detect "record-style" accessors
32+
assertEquals("{}", JSON.std.asString(new RecordLike94()));
33+
34+
assertEquals(a2q("{'STATUS':500,'count':3}"), JSON.std.with(JSON.Feature.USE_FIELD_MATCHING_GETTERS)
35+
.asString(new RecordLike94()));
36+
}
37+
}

jr-test-module/src/test/groovy/GroovyObjectSupportTest.groovy

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,11 @@ import com.fasterxml.jackson.jr.ob.JSON
22
import org.junit.Assert
33
import org.junit.Test
44

5+
/**
6+
* A minor note on running/debugging this test on local, if you are using intellij, please
7+
* change `<packaging>pom</packaging>` to `<packaging>bundle</packaging>`. this is causing
8+
* some issue with the IDE.
9+
*/
510
class GroovyObjectSupportTest {
611
@Test
712
void testSimpleGroovyObject() throws Exception {
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
import com.fasterxml.jackson.jr.ob.JSON
2+
import org.junit.Assert
3+
import org.junit.Test
4+
5+
/**
6+
* A minor note on running/debugging this test on local, if you are using intellij, please
7+
* change `<packaging>pom</packaging>` to `<packaging>bundle</packaging>`. this is causing
8+
* some issue with the IDE.
9+
*/
10+
class GroovyRecordsTest {
11+
12+
@Test
13+
void testRecord() throws Exception {
14+
/* We need to use this since build (8, ubuntu-20.04), will fail Map.of() was added in Java 9*/
15+
def map = new HashMap<String, String>()
16+
map.put("foo", "bar")
17+
18+
def json = JSON.builder().enable(JSON.Feature.USE_FIELD_MATCHING_GETTERS).build().asString(new Cow("foo", map))
19+
def expected = """{"message":"foo","object":{"foo":"bar"}}"""
20+
Assert.assertEquals(expected, json)
21+
}
22+
23+
@Test
24+
void testRecordEquivalentObjects() throws Exception {
25+
def expected = """{"message":"foo","object":{"foo":"bar"}}"""
26+
27+
/* We need to use this since build (8, ubuntu-20.04), will fail Map.of() was added in Java 9*/
28+
def map = new HashMap<String, String>()
29+
map.put("foo", "bar")
30+
31+
def json = JSON.builder().enable(JSON.Feature.USE_FIELD_MATCHING_GETTERS).build().asString(new SimpleGroovyObject("foo", map))
32+
Assert.assertEquals(expected, json)
33+
34+
def json2 = JSON.builder().enable(JSON.Feature.USE_FIELD_MATCHING_GETTERS).build().asString(new GroovyObjectWithNamedGetters("foo", map))
35+
Assert.assertEquals(expected, json2)
36+
}
37+
}
38+
39+
class SimpleGroovyObject {
40+
public final String message
41+
public final Map<String, String> object
42+
43+
SimpleGroovyObject(String message, Map<String, String> object) {
44+
this.message = message
45+
this.object = object
46+
}
47+
}
48+
49+
class GroovyObjectWithNamedGetters {
50+
private final String message
51+
private final Map<String, String> object
52+
53+
GroovyObjectWithNamedGetters(String message, Map<String, String> object) {
54+
this.message = message
55+
this.object = object
56+
}
57+
58+
String message() {
59+
return message
60+
}
61+
62+
Map<String, String> object() {
63+
return object
64+
}
65+
}
66+
67+
record Cow(String message, Map<String, String> object) {}
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
import com.fasterxml.jackson.jr.ob.JSON;
2+
import org.junit.Assert;
3+
import org.junit.Test;
4+
5+
import java.io.IOException;
6+
import java.util.Map;
7+
8+
/**
9+
* This test is in test module since the JDK version to be tested is higher than other, and hence supports Records.
10+
*/
11+
public class Java17RecordTest {
12+
13+
@Test
14+
public void testJava14RecordSupport() throws IOException {
15+
var expectedString = "{\"message\":\"MOO\",\"object\":{\"Foo\":\"Bar\"}}";
16+
var json = JSON.builder().enable(JSON.Feature.USE_FIELD_MATCHING_GETTERS).build().asString(new Cow("MOO", Map.of("Foo", "Bar")));
17+
Assert.assertEquals(expectedString, json);
18+
}
19+
20+
record Cow(String message, Map<String, String> object) {
21+
}
22+
}

release-notes/CREDITS-2.x

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,5 +57,7 @@ Julian Honnen (@jhonnen)
5757
* Contributed fix for #93: Skip serialization of `groovy.lang.MetaClass` values
5858
to avoid `StackOverflowError`
5959
(2.17.0)
60+
* Constributed implementation of #94: Support for serializing Java Records
61+
(2.17.0)
6062
* Contributed impl for #100: Add support for `java.time` (Java 8 date/time) types
6163
(2.17.0)

release-notes/VERSION-2.x

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,8 @@ Not yet released
1717
(contributed by @Shounaks)
1818
#51: Duplicate key detection does not work for (simple) Trees
1919
(contributed by @Shounaks)
20+
#94: Support for serializing Java Records
21+
(implementation contributed by @Shounaks)
2022
#131: Add mechanism for `JacksonJrExtension`s to access state of `JSON.Feature`s
2123

2224
2.17.0-rc1 (26-Feb-2024)

0 commit comments

Comments
 (0)