forked from FasterXML/jackson-databind
-
Notifications
You must be signed in to change notification settings - Fork 1
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Support for Java 14 records (FasterXML#2709)
First attempt at supporting Java 14 records (JEP 359). Records are simple DTO/POJO objects with final fields (components) and accessors. Record's components are automatically serialized and the canonical constructor is used for deserialization. Implementation is still compatible with Java 8 and uses a bit of reflection to access record's components. However the unit tests now require a JDK 14 to run. The basic idea is to make record's components discovered as properties (similar to beans having getters) and to make the canonical constructor accessible via implicit parameter names.
- Loading branch information
1 parent
5bca846
commit 977fbb3
Showing
7 changed files
with
280 additions
and
3 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
85 changes: 85 additions & 0 deletions
85
src/main/java/com/fasterxml/jackson/databind/util/RecordUtil.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,85 @@ | ||
package com.fasterxml.jackson.databind.util; | ||
|
||
import com.fasterxml.jackson.databind.introspect.AnnotatedClass; | ||
import com.fasterxml.jackson.databind.introspect.AnnotatedConstructor; | ||
import com.fasterxml.jackson.databind.introspect.AnnotatedParameter; | ||
|
||
import java.lang.reflect.Constructor; | ||
import java.lang.reflect.Field; | ||
import java.lang.reflect.Method; | ||
import java.lang.reflect.RecordComponent; | ||
import java.util.Arrays; | ||
|
||
/** | ||
* Helper class to detect Java records without Java 14 as Jackson targets is Java 8. | ||
* <p> | ||
* See <a href="https://openjdk.java.net/jeps/359">JEP 359</a> | ||
*/ | ||
public final class RecordUtil { | ||
|
||
private static final String RECORD_CLASS_NAME = "java.lang.Record"; | ||
private static final String RECORD_GET_RECORD_COMPONENTS = "getRecordComponents"; | ||
|
||
private static final String RECORD_COMPONENT_CLASS_NAME = "java.lang.reflect.RecordComponent"; | ||
private static final String RECORD_COMPONENT_GET_NAME = "getName"; | ||
private static final String RECORD_COMPONENT_GET_TYPE = "getType"; | ||
|
||
public static boolean isRecord(Class<?> aClass) { | ||
return aClass != null | ||
&& aClass.getSuperclass() != null | ||
&& aClass.getSuperclass().getName().equals(RECORD_CLASS_NAME); | ||
} | ||
|
||
/** | ||
* @return Record component's names, ordering is preserved. | ||
*/ | ||
public static String[] getRecordComponents(Class<?> aRecord) { | ||
if (!isRecord(aRecord)) { | ||
return new String[0]; | ||
} | ||
|
||
try { | ||
Method method = Class.class.getMethod(RECORD_GET_RECORD_COMPONENTS); | ||
Object[] components = (Object[]) method.invoke(aRecord); | ||
String[] names = new String[components.length]; | ||
Method recordComponentGetName = Class.forName(RECORD_COMPONENT_CLASS_NAME).getMethod(RECORD_COMPONENT_GET_NAME); | ||
for (int i = 0; i < components.length; i++) { | ||
Object component = components[i]; | ||
names[i] = (String) recordComponentGetName.invoke(component); | ||
} | ||
return names; | ||
} catch (Throwable e) { | ||
return new String[0]; | ||
} | ||
} | ||
|
||
public static AnnotatedConstructor getCanonicalConstructor(AnnotatedClass aRecord) { | ||
if (!isRecord(aRecord.getAnnotated())) { | ||
return null; | ||
} | ||
|
||
Class<?>[] paramTypes = getRecordComponentTypes(aRecord.getAnnotated()); | ||
for (AnnotatedConstructor constructor : aRecord.getConstructors()) { | ||
if (Arrays.equals(constructor.getAnnotated().getParameterTypes(), paramTypes)) { | ||
return constructor; | ||
} | ||
} | ||
return null; | ||
} | ||
|
||
private static Class<?>[] getRecordComponentTypes(Class<?> aRecord) { | ||
try { | ||
Method method = Class.class.getMethod(RECORD_GET_RECORD_COMPONENTS); | ||
Object[] components = (Object[]) method.invoke(aRecord); | ||
Class<?>[] types = new Class[components.length]; | ||
Method recordComponentGetName = Class.forName(RECORD_COMPONENT_CLASS_NAME).getMethod(RECORD_COMPONENT_GET_TYPE); | ||
for (int i = 0; i < components.length; i++) { | ||
Object component = components[i]; | ||
types[i] = (Class<?>) recordComponentGetName.invoke(component); | ||
} | ||
return types; | ||
} catch (Throwable e) { | ||
return new Class[0]; | ||
} | ||
} | ||
} |
87 changes: 87 additions & 0 deletions
87
src/test/java/com/fasterxml/jackson/databind/RecordTest.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,87 @@ | ||
package com.fasterxml.jackson.databind; | ||
|
||
import com.fasterxml.jackson.annotation.JsonIgnore; | ||
import com.fasterxml.jackson.core.JsonProcessingException; | ||
import com.fasterxml.jackson.databind.json.JsonMapper; | ||
|
||
import java.io.IOException; | ||
|
||
public class RecordTest extends BaseMapTest { | ||
|
||
private JsonMapper jsonMapper; | ||
|
||
public void setUp() { | ||
jsonMapper = new JsonMapper(); | ||
} | ||
|
||
record SimpleRecord(int id, String name) { | ||
} | ||
|
||
public void testSerializeSimpleRecord() throws JsonProcessingException { | ||
SimpleRecord record = new SimpleRecord(123, "Bob"); | ||
|
||
String json = jsonMapper.writeValueAsString(record); | ||
|
||
assertEquals("{\"id\":123,\"name\":\"Bob\"}", json); | ||
} | ||
|
||
public void testDeserializeSimpleRecord() throws IOException { | ||
SimpleRecord value = jsonMapper.readValue("{\"id\":123,\"name\":\"Bob\"}", SimpleRecord.class); | ||
|
||
assertEquals(new SimpleRecord(123, "Bob"), value); | ||
} | ||
|
||
public void testSerializeSimpleRecord_DisableAnnotationIntrospector() throws JsonProcessingException { | ||
SimpleRecord record = new SimpleRecord(123, "Bob"); | ||
|
||
JsonMapper mapper = JsonMapper.builder() | ||
.configure(MapperFeature.USE_ANNOTATIONS, false) | ||
.build(); | ||
String json = mapper.writeValueAsString(record); | ||
|
||
assertEquals("{\"id\":123,\"name\":\"Bob\"}", json); | ||
} | ||
|
||
public void testDeserializeSimpleRecord_DisableAnnotationIntrospector() throws IOException { | ||
JsonMapper mapper = JsonMapper.builder() | ||
.configure(MapperFeature.USE_ANNOTATIONS, false) | ||
.build(); | ||
SimpleRecord value = mapper.readValue("{\"id\":123,\"name\":\"Bob\"}", SimpleRecord.class); | ||
|
||
assertEquals(new SimpleRecord(123, "Bob"), value); | ||
} | ||
|
||
record RecordOfRecord(SimpleRecord record) { | ||
} | ||
|
||
public void testSerializeRecordOfRecord() throws JsonProcessingException { | ||
RecordOfRecord record = new RecordOfRecord(new SimpleRecord(123, "Bob")); | ||
|
||
String json = jsonMapper.writeValueAsString(record); | ||
|
||
assertEquals("{\"record\":{\"id\":123,\"name\":\"Bob\"}}", json); | ||
} | ||
|
||
record JsonIgnoreRecord(int id, @JsonIgnore String name) { | ||
} | ||
|
||
public void testSerializeJsonIgnoreRecord() throws JsonProcessingException { | ||
JsonIgnoreRecord record = new JsonIgnoreRecord(123, "Bob"); | ||
|
||
String json = jsonMapper.writeValueAsString(record); | ||
|
||
assertEquals("{\"id\":123}", json); | ||
} | ||
|
||
record RecordWithConstructor(int id, String name) { | ||
public RecordWithConstructor(int id) { | ||
this(id, "name"); | ||
} | ||
} | ||
|
||
public void testDeserializeRecordWithConstructor() throws IOException { | ||
RecordWithConstructor value = jsonMapper.readValue("{\"id\":123,\"name\":\"Bob\"}", RecordWithConstructor.class); | ||
|
||
assertEquals(new RecordWithConstructor(123, "Bob"), value); | ||
} | ||
} |
50 changes: 50 additions & 0 deletions
50
src/test/java/com/fasterxml/jackson/databind/util/RecordUtilTest.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,50 @@ | ||
package com.fasterxml.jackson.databind.util; | ||
|
||
import com.fasterxml.jackson.databind.DeserializationConfig; | ||
import com.fasterxml.jackson.databind.JavaType; | ||
import com.fasterxml.jackson.databind.ObjectMapper; | ||
import com.fasterxml.jackson.databind.SerializationConfig; | ||
import com.fasterxml.jackson.databind.introspect.AnnotatedClass; | ||
import com.fasterxml.jackson.databind.introspect.AnnotatedClassResolver; | ||
import org.junit.Test; | ||
|
||
import static org.junit.Assert.*; | ||
|
||
public class RecordUtilTest { | ||
|
||
@Test | ||
public void isRecord() { | ||
assertTrue(RecordUtil.isRecord(SimpleRecord.class)); | ||
assertFalse(RecordUtil.isRecord(String.class)); | ||
} | ||
|
||
@Test | ||
public void getRecordComponents() { | ||
assertArrayEquals(new String[]{"name", "id"}, RecordUtil.getRecordComponents(SimpleRecord.class)); | ||
assertArrayEquals(new String[]{}, RecordUtil.getRecordComponents(String.class)); | ||
} | ||
|
||
record SimpleRecord(String name, int id) { | ||
public SimpleRecord(int id) { | ||
this("", id); | ||
} | ||
} | ||
|
||
@Test | ||
public void getCanonicalConstructor() { | ||
DeserializationConfig config = new ObjectMapper().deserializationConfig(); | ||
|
||
assertNotNull(null, RecordUtil.getCanonicalConstructor( | ||
AnnotatedClassResolver.resolve(config, | ||
config.constructType(SimpleRecord.class), | ||
null | ||
))); | ||
|
||
assertNull(null, RecordUtil.getCanonicalConstructor( | ||
AnnotatedClassResolver.resolve(config, | ||
config.constructType(String.class), | ||
null | ||
))); | ||
} | ||
|
||
} |