+ * See JEP 359 + */ +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]; + } + } +} diff --git a/src/test/java/com/fasterxml/jackson/databind/RecordTest.java b/src/test/java/com/fasterxml/jackson/databind/RecordTest.java new file mode 100644 index 00000000000..187f40304b1 --- /dev/null +++ b/src/test/java/com/fasterxml/jackson/databind/RecordTest.java @@ -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); + } +} diff --git a/src/test/java/com/fasterxml/jackson/databind/util/RecordUtilTest.java b/src/test/java/com/fasterxml/jackson/databind/util/RecordUtilTest.java new file mode 100644 index 00000000000..202c6e2e186 --- /dev/null +++ b/src/test/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)); + } + + 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 + ))); + } + +} \ No newline at end of file