diff --git a/pom.xml b/pom.xml index 6ef73adbc18..102f346cc4d 100644 --- a/pom.xml +++ b/pom.xml @@ -153,6 +153,7 @@ --> 4 classes + --enable-preview @@ -185,6 +186,25 @@ -parameters + + + default-testCompile + process-test-sources + + testCompile + + + + true + 14 + 14 + + -parameters + --enable-preview + + + + diff --git a/src/main/java/com/fasterxml/jackson/databind/deser/BasicDeserializerFactory.java b/src/main/java/com/fasterxml/jackson/databind/deser/BasicDeserializerFactory.java index 8a2ecc94d09..5dd42db19a8 100644 --- a/src/main/java/com/fasterxml/jackson/databind/deser/BasicDeserializerFactory.java +++ b/src/main/java/com/fasterxml/jackson/databind/deser/BasicDeserializerFactory.java @@ -973,6 +973,14 @@ protected SettableBeanProperty constructCreatorProperty(DeserializationContext c private PropertyName _findParamName(DeserializationContext ctxt, AnnotatedParameter param, AnnotationIntrospector intr) { + if (param != null) { + Class ownerClass = param.getOwner().getType().getRawClass(); + if (RecordUtil.isRecord(ownerClass)) { + String recordComponentName = RecordUtil.getRecordComponents(ownerClass)[param.getIndex()]; + return PropertyName.construct(recordComponentName); + } + } + if (param != null && intr != null) { final DeserializationConfig config = ctxt.getConfig(); PropertyName name = intr.findNameForDeserialization(config, param); diff --git a/src/main/java/com/fasterxml/jackson/databind/introspect/POJOPropertiesCollector.java b/src/main/java/com/fasterxml/jackson/databind/introspect/POJOPropertiesCollector.java index 28a805dabbb..8bd41f5eabf 100644 --- a/src/main/java/com/fasterxml/jackson/databind/introspect/POJOPropertiesCollector.java +++ b/src/main/java/com/fasterxml/jackson/databind/introspect/POJOPropertiesCollector.java @@ -13,6 +13,7 @@ import com.fasterxml.jackson.databind.cfg.MapperConfig; import com.fasterxml.jackson.databind.util.BeanUtil; import com.fasterxml.jackson.databind.util.ClassUtil; +import com.fasterxml.jackson.databind.util.RecordUtil; /** * Helper class used for aggregating information about all possible @@ -447,6 +448,24 @@ protected void _addFields(Map props) */ protected void _addCreators(Map props) { + // collect record's canonical constructor + if (RecordUtil.isRecord(_classDef.getAnnotated())) { + if (_creatorProperties == null) { + _creatorProperties = new LinkedList<>(); + } + AnnotatedConstructor constructor = RecordUtil.getCanonicalConstructor(_classDef); + if (constructor != null) { + String[] recordComponents = RecordUtil.getRecordComponents(_classDef.getAnnotated()); + for (int i = 0; i < constructor.getParameterCount(); i++) { + AnnotatedParameter parameter = constructor.getParameter(i); + POJOPropertyBuilder prop = _property(props, recordComponents[i]); + prop.addCtor(parameter, + PropertyName.construct(recordComponents[i]), false, true, false); + _creatorProperties.add(prop); + } + } + } + // can be null if annotation processing is disabled... if (!_useAnnotations) { return; diff --git a/src/main/java/com/fasterxml/jackson/databind/util/BeanUtil.java b/src/main/java/com/fasterxml/jackson/databind/util/BeanUtil.java index 250f89a376b..ea769ae13c2 100644 --- a/src/main/java/com/fasterxml/jackson/databind/util/BeanUtil.java +++ b/src/main/java/com/fasterxml/jackson/databind/util/BeanUtil.java @@ -1,5 +1,6 @@ package com.fasterxml.jackson.databind.util; +import java.util.Arrays; import java.util.Calendar; import java.util.Date; import java.util.GregorianCalendar; @@ -8,6 +9,7 @@ import com.fasterxml.jackson.databind.JavaType; import com.fasterxml.jackson.databind.introspect.AnnotatedMember; + /** * Helper class that contains functionality needed by both serialization * and deserialization side. @@ -31,11 +33,17 @@ public static String okNameForGetter(AnnotatedMember am) { public static String okNameForRegularGetter(AnnotatedMember am, String name) { + if (RecordUtil.isRecord(am.getDeclaringClass()) && + Arrays.asList(RecordUtil.getRecordComponents(am.getDeclaringClass())).contains(name)) { + // record getters are not prefixed + return name; + } + if (name.startsWith("get")) { /* 16-Feb-2009, tatu: To handle [JACKSON-53], need to block * CGLib-provided method "getCallbacks". Not sure of exact * safe criteria to get decent coverage without false matches; - * but for now let's assume there's no reason to use any + * but for now let's assume there's no reason to use any * such getter from CGLib. * But let's try this approach... */ @@ -79,7 +87,7 @@ public static String okNameForMutator(AnnotatedMember am, String prefix) /* Value defaulting helpers /********************************************************** */ - + /** * Accessor used to find out "default value" to use for comparing values to * serialize, to determine whether to exclude value from serialization with @@ -130,7 +138,7 @@ public static Object getDefaultValue(JavaType type) /** * This method was added to address the need to weed out - * CGLib-injected "getCallbacks" method. + * CGLib-injected "getCallbacks" method. * At this point caller has detected a potential getter method * with name "getCallbacks" and we need to determine if it is * indeed injectect by Cglib. We do this by verifying that the diff --git a/src/main/java/com/fasterxml/jackson/databind/util/RecordUtil.java b/src/main/java/com/fasterxml/jackson/databind/util/RecordUtil.java new file mode 100644 index 00000000000..eae84983902 --- /dev/null +++ b/src/main/java/com/fasterxml/jackson/databind/util/RecordUtil.java @@ -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. + *

+ * 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