Skip to content

Commit

Permalink
Adding support for Java 17 & Groovy Records (#130)
Browse files Browse the repository at this point in the history
  • Loading branch information
Shounaks authored Mar 11, 2024
1 parent cec7bac commit 226c0bc
Show file tree
Hide file tree
Showing 8 changed files with 168 additions and 8 deletions.
11 changes: 11 additions & 0 deletions jr-objects/src/main/java/com/fasterxml/jackson/jr/ob/JSON.java
Original file line number Diff line number Diff line change
Expand Up @@ -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 <code>amount</code>
* the getter would be <code>amount()</code>).
*
* @implNote <p>Feature is disabled by default for backward compatibility.</p>
*
* @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.
Expand Down
Original file line number Diff line number Diff line change
@@ -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;

Expand Down Expand Up @@ -98,17 +96,22 @@ private static void _introspect(Class<?> currType, Map<String, PropBuilder> 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<String, Field> 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);
Expand Down Expand Up @@ -145,6 +148,17 @@ private static void _introspect(Class<?> currType, Map<String, PropBuilder> 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
Expand Down
Original file line number Diff line number Diff line change
@@ -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()));
}
}
5 changes: 5 additions & 0 deletions jr-test-module/src/test/groovy/GroovyObjectSupportTest.groovy
Original file line number Diff line number Diff line change
Expand Up @@ -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 `<packaging>pom</packaging>` to `<packaging>bundle</packaging>`. this is causing
* some issue with the IDE.
*/
class GroovyObjectSupportTest {
@Test
void testSimpleGroovyObject() throws Exception {
Expand Down
67 changes: 67 additions & 0 deletions jr-test-module/src/test/groovy/GroovyRecordsTest.groovy
Original file line number Diff line number Diff line change
@@ -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 `<packaging>pom</packaging>` to `<packaging>bundle</packaging>`. 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<String, String>()
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<String, String>()
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<String, String> object

SimpleGroovyObject(String message, Map<String, String> object) {
this.message = message
this.object = object
}
}

class GroovyObjectWithNamedGetters {
private final String message
private final Map<String, String> object

GroovyObjectWithNamedGetters(String message, Map<String, String> object) {
this.message = message
this.object = object
}

String message() {
return message
}

Map<String, String> object() {
return object
}
}

record Cow(String message, Map<String, String> object) {}
22 changes: 22 additions & 0 deletions jr-test-module/src/test/java/Java17RecordTest.java
Original file line number Diff line number Diff line change
@@ -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<String, String> object) {
}
}
2 changes: 2 additions & 0 deletions release-notes/CREDITS-2.x
Original file line number Diff line number Diff line change
Expand Up @@ -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)
2 changes: 2 additions & 0 deletions release-notes/VERSION-2.x
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down

0 comments on commit 226c0bc

Please sign in to comment.