+ * See JEP 359 + */ +public final class RecordUtil { + + private static final Method IS_RECORD; + private static final Method GET_RECORD_COMPONENTS; + private static final Method GET_NAME; + private static final Method GET_TYPE; + + static { + Method isRecord; + Method getRecordComponents; + Method getName; + Method getType; + try { + isRecord = Class.class.getDeclaredMethod("isRecord"); + getRecordComponents = Class.class.getMethod("getRecordComponents"); + Class c = Class.forName("java.lang.reflect.RecordComponent"); + getName = c.getMethod("getName"); + getType = c.getMethod("getType"); + } catch (ClassNotFoundException| NoSuchMethodException e) { + // pre-Java-14 + isRecord = null; + getRecordComponents = null; + getName = null; + getType = null; + } + IS_RECORD = isRecord; + GET_RECORD_COMPONENTS = getRecordComponents; + GET_NAME = getName; + GET_TYPE = getType; + } + + public static boolean isRecord(Class> aClass) { + try { + return IS_RECORD == null ? false : (boolean) IS_RECORD.invoke(aClass); + } catch (IllegalAccessException | InvocationTargetException e) { + throw new AssertionError(); + } + } + + /** + * @return Record component's names, ordering is preserved. + */ + public static String[] getRecordComponents(Class> aRecord) { + if (!isRecord(aRecord)) { + return new String[0]; + } + + try { + Object[] components = (Object[]) GET_RECORD_COMPONENTS.invoke(aRecord); + String[] names = new String[components.length]; + for (int i = 0; i < components.length; i++) { + Object component = components[i]; + names[i] = (String) GET_NAME.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 { + Object[] components = (Object[]) GET_RECORD_COMPONENTS.invoke(aRecord); + Class>[] types = new Class[components.length]; + for (int i = 0; i < components.length; i++) { + Object component = components[i]; + types[i] = (Class>) GET_TYPE.invoke(component); + } + return types; + } catch (Throwable e) { + return new Class[0]; + } + } +} + diff --git a/src/test-jdk14/java/com/fasterxml/jackson/databind/RecordTest.java b/src/test-jdk14/java/com/fasterxml/jackson/databind/RecordTest.java new file mode 100644 index 0000000000..ce440cee36 --- /dev/null +++ b/src/test-jdk14/java/com/fasterxml/jackson/databind/RecordTest.java @@ -0,0 +1,105 @@ +package com.fasterxml.jackson.databind; + +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.fasterxml.jackson.annotation.JsonProperty; +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(); + } + + public 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); + } + + public 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); + } + + public 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); + } + + public 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); + } + + public record JsonPropertyRenameRecord(int id, @JsonProperty("rename")String name) { + } + + public void testSerializeJsonRenameRecord() throws JsonProcessingException { + JsonPropertyRenameRecord record = new JsonPropertyRenameRecord(123, "Bob"); + + String json = jsonMapper.writeValueAsString(record); + + assertEquals("{\"id\":123,\"rename\":\"Bob\"}", json); + } + + public void testDeserializeJsonRenameRecord() throws IOException { + JsonPropertyRenameRecord value = jsonMapper.readValue("{\"id\":123,\"rename\":\"Bob\"}", JsonPropertyRenameRecord.class); + + assertEquals(new JsonPropertyRenameRecord(123, "Bob"), value); + } +} diff --git a/src/test-jdk14/java/com/fasterxml/jackson/databind/util/RecordUtilTest.java b/src/test-jdk14/java/com/fasterxml/jackson/databind/util/RecordUtilTest.java new file mode 100644 index 0000000000..d9cbe0580f --- /dev/null +++ b/src/test-jdk14/java/com/fasterxml/jackson/databind/util/RecordUtilTest.java @@ -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)); + } + + public 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 + ))); + } + +}