From 263b544b5e54d6d1b8d38ddbffde49cedc89504f Mon Sep 17 00:00:00 2001 From: Tatu Saloranta Date: Fri, 21 Aug 2020 16:04:29 -0700 Subject: [PATCH] Getting close to completing #2709, now basic deserialization works; still need overrides --- .../deser/BasicDeserializerFactory.java | 43 +++++- .../databind/deser/impl/CreatorCandidate.java | 2 +- .../jackson/databind/jdk14/JDK14Util.java | 134 ++++++++++++++++++ .../jackson/databind/RecordTest.java | 27 ++-- 4 files changed, 182 insertions(+), 24 deletions(-) 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 bd572a3fdb..09337a03f0 100644 --- a/src/main/java/com/fasterxml/jackson/databind/deser/BasicDeserializerFactory.java +++ b/src/main/java/com/fasterxml/jackson/databind/deser/BasicDeserializerFactory.java @@ -7,12 +7,7 @@ import java.util.concurrent.*; import java.util.concurrent.atomic.AtomicReference; -import com.fasterxml.jackson.annotation.JacksonInject; -import com.fasterxml.jackson.annotation.JsonCreator; -import com.fasterxml.jackson.annotation.JsonIgnoreProperties; -import com.fasterxml.jackson.annotation.JsonIncludeProperties; -import com.fasterxml.jackson.annotation.JsonSetter; -import com.fasterxml.jackson.annotation.Nulls; +import com.fasterxml.jackson.annotation.*; import com.fasterxml.jackson.annotation.JsonCreator.Mode; import com.fasterxml.jackson.core.JsonParser; @@ -29,6 +24,7 @@ import com.fasterxml.jackson.databind.exc.InvalidDefinitionException; import com.fasterxml.jackson.databind.ext.OptionalHandlerFactory; import com.fasterxml.jackson.databind.introspect.*; +import com.fasterxml.jackson.databind.jdk14.JDK14Util; import com.fasterxml.jackson.databind.jsontype.NamedType; import com.fasterxml.jackson.databind.jsontype.TypeDeserializer; import com.fasterxml.jackson.databind.jsontype.TypeResolverBuilder; @@ -278,6 +274,15 @@ protected ValueInstantiator _constructDefaultValueInstantiator(DeserializationCo _addDeserializerFactoryMethods(ctxt, beanDesc, vchecker, intr, creators, creatorDefs); // constructors only usable on concrete types: if (beanDesc.getType().isConcrete()) { + // [databind#2709]: Record support + if (beanDesc.getType().isRecordType()) { + final List names = new ArrayList<>(); + AnnotatedConstructor canonical = JDK14Util.findRecordConstructor(ctxt, beanDesc, names); + if (canonical != null) { + _addRecordConstructor(ctxt, beanDesc, creators, canonical, names); + return creators.constructValueInstantiator(ctxt); + } + } _addDeserializerConstructors(ctxt, beanDesc, vchecker, intr, creators, creatorDefs); } return creators.constructValueInstantiator(ctxt); @@ -542,6 +547,30 @@ protected void _addDeserializerConstructors(DeserializationContext ctxt, } } + /** + * Helper method called when a {@code java.lang.Record} definition's "canonical" + * constructor is to be used: if so, we have implicit names to consider. + * + * @since 2.12 + */ + protected void _addRecordConstructor(DeserializationContext ctxt, + BeanDescription beanDesc, CreatorCollector creators, + AnnotatedConstructor canonical, List names) + throws JsonMappingException + { + final int argCount = canonical.getParameterCount(); + final AnnotationIntrospector intr = ctxt.getAnnotationIntrospector(); + final SettableBeanProperty[] properties = new SettableBeanProperty[argCount]; + + for (int i = 0; i < argCount; ++i) { + final AnnotatedParameter param = canonical.getParameter(i); + JacksonInject.Value injectable = intr.findInjectableValue(param); + final PropertyName name = PropertyName.construct(names.get(i)); + properties[i] = constructCreatorProperty(ctxt, beanDesc, name, i, param, injectable); + } + creators.addPropertyCreator(canonical, false, properties); + } + /** * Helper method called when there is the explicit "is-creator" with mode of "delegating" * @@ -1038,7 +1067,7 @@ protected SettableBeanProperty constructCreatorProperty(DeserializationContext c private PropertyName _findParamName(AnnotatedParameter param, AnnotationIntrospector intr) { - if (param != null && intr != null) { + if (intr != null) { PropertyName name = intr.findNameForDeserialization(param); if (name != null) { return name; diff --git a/src/main/java/com/fasterxml/jackson/databind/deser/impl/CreatorCandidate.java b/src/main/java/com/fasterxml/jackson/databind/deser/impl/CreatorCandidate.java index 64ab178b53..4cf62cd64e 100644 --- a/src/main/java/com/fasterxml/jackson/databind/deser/impl/CreatorCandidate.java +++ b/src/main/java/com/fasterxml/jackson/databind/deser/impl/CreatorCandidate.java @@ -58,7 +58,7 @@ public PropertyName explicitParamName(int i) { } return null; } - + public PropertyName findImplicitParamName(int i) { String str = _intr.findImplicitPropertyName(_params[i].annotated); if (str != null && !str.isEmpty()) { diff --git a/src/main/java/com/fasterxml/jackson/databind/jdk14/JDK14Util.java b/src/main/java/com/fasterxml/jackson/databind/jdk14/JDK14Util.java index c23d08d23e..d8454ac7bc 100644 --- a/src/main/java/com/fasterxml/jackson/databind/jdk14/JDK14Util.java +++ b/src/main/java/com/fasterxml/jackson/databind/jdk14/JDK14Util.java @@ -1,7 +1,21 @@ package com.fasterxml.jackson.databind.jdk14; import java.lang.reflect.Method; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonCreator.Mode; +import com.fasterxml.jackson.databind.AnnotationIntrospector; +import com.fasterxml.jackson.databind.BeanDescription; +import com.fasterxml.jackson.databind.DeserializationConfig; +import com.fasterxml.jackson.databind.DeserializationContext; +import com.fasterxml.jackson.databind.deser.impl.CreatorCollector; +import com.fasterxml.jackson.databind.introspect.AnnotatedConstructor; +import com.fasterxml.jackson.databind.introspect.AnnotatedWithParams; +import com.fasterxml.jackson.databind.introspect.BeanPropertyDefinition; +import com.fasterxml.jackson.databind.introspect.VisibilityChecker; import com.fasterxml.jackson.databind.util.ClassUtil; /** @@ -18,6 +32,12 @@ public static String[] getRecordFieldNames(Class recordType) { return RecordAccessor.instance().getRecordFieldNames(recordType); } + public static AnnotatedConstructor findRecordConstructor(DeserializationContext ctxt, + BeanDescription beanDesc, List names) { + return new CreatorLocator(ctxt, beanDesc) + .locate(names); + } + static class RecordAccessor { private final Method RECORD_GET_RECORD_COMPONENTS; private final Method RECORD_COMPONENT_GET_NAME; @@ -74,6 +94,32 @@ public String[] getRecordFieldNames(Class recordType) throws IllegalArgumentE return names; } + public RawTypeName[] getRecordFields(Class recordType) throws IllegalArgumentException + { + final Object[] components = recordComponents(recordType); + final RawTypeName[] results = new RawTypeName[components.length]; + for (int i = 0; i < components.length; i++) { + String name; + try { + name = (String) RECORD_COMPONENT_GET_NAME.invoke(components[i]); + } catch (Exception e) { + throw new IllegalArgumentException(String.format( +"Failed to access name of field #%d (of %d) of Record type %s", +i, components.length, ClassUtil.nameOf(recordType)), e); + } + Class type; + try { + type = (Class) RECORD_COMPONENT_GET_TYPE.invoke(components[i]); + } catch (Exception e) { + throw new IllegalArgumentException(String.format( +"Failed to access type of field #%d (of %d) of Record type %s", +i, components.length, ClassUtil.nameOf(recordType)), e); + } + results[i] = new RawTypeName(type, name); + } + return results; + } + protected Object[] recordComponents(Class recordType) throws IllegalArgumentException { try { @@ -84,4 +130,92 @@ protected Object[] recordComponents(Class recordType) throws IllegalArgumentE } } } + + static class RawTypeName { + public final Class rawType; + public final String name; + + public RawTypeName(Class rt, String n) { + rawType = rt; + name = n; + } + } + + static class CreatorLocator { + protected final BeanDescription _beanDesc; + protected final DeserializationConfig _config; + protected final AnnotationIntrospector _intr; + + protected final List _constructors; + protected final AnnotatedConstructor _primaryConstructor; + protected final RawTypeName[] _recordFields; + + CreatorLocator(DeserializationContext ctxt, BeanDescription beanDesc) + { + _beanDesc = beanDesc; + + _intr = ctxt.getAnnotationIntrospector(); + _config = ctxt.getConfig(); + + _recordFields = RecordAccessor.instance().getRecordFields(beanDesc.getBeanClass()); + final int argCount = _recordFields.length; + + // And then locate the canonical constructor; must be found, if not, fail + // altogether (so we can figure out what went wrong) + AnnotatedConstructor primary = null; + + // One special case: empty Records, empty constructor is separate case + if (argCount == 0) { + primary = beanDesc.findDefaultConstructor(); + _constructors = Collections.singletonList(primary); + } else { + _constructors = beanDesc.getConstructors(); + main_loop: + for (AnnotatedConstructor ctor : _constructors) { + if (ctor.getParameterCount() != argCount) { + continue; + } + for (int i = 0; i < argCount; ++i) { + if (!ctor.getRawParameterType(i).equals(_recordFields[i].rawType)) { + continue main_loop; + } + } + primary = ctor; + break; + } + } + if (primary == null) { + throw new IllegalArgumentException("Failed to find the canonical Record constructor of type " + +ClassUtil.getTypeDescription(_beanDesc.getType())); + } + _primaryConstructor = primary; + } + + public AnnotatedConstructor locate(List names) + { + // First things first: ensure that either there are no explicit marked constructors + // or that there is just one and it is the canonical one and it is not + // declared as "delegating" constructor + for (AnnotatedConstructor ctor : _constructors) { + JsonCreator.Mode creatorMode = _intr.findCreatorAnnotation(_config, ctor); + if ((null == creatorMode) || (Mode.DISABLED == creatorMode)) { + continue; + } + // If there's a delegating Creator let caller figure out + if (Mode.DELEGATING == creatorMode) { + return null; + } + if (ctor != _primaryConstructor) { + return null; + } + } + + // By now we have established that the canonical constructor is the one to use + // and just need to gather implicit names to return + for (RawTypeName field : _recordFields) { + names.add(field.name); + } + return _primaryConstructor; + } + } } diff --git a/src/test-jdk14/java/com/fasterxml/jackson/databind/RecordTest.java b/src/test-jdk14/java/com/fasterxml/jackson/databind/RecordTest.java index b75e56db76..c34fe3853b 100644 --- a/src/test-jdk14/java/com/fasterxml/jackson/databind/RecordTest.java +++ b/src/test-jdk14/java/com/fasterxml/jackson/databind/RecordTest.java @@ -31,9 +31,9 @@ public RecordWithAltCtor(@JsonProperty("id") int id) { } } - record JsonIgnoreRecord(int id, @JsonIgnore String name) { } + record RecordWithIgnore(int id, @JsonIgnore String name) { } - record JsonPropertyRenameRecord(int id, @JsonProperty("rename")String name) { } + record RecordWithRename(int id, @JsonProperty("rename")String name) { } record EmptyRecord() { } @@ -50,8 +50,8 @@ public void testClassUtil() { assertTrue(ClassUtil.isRecordType(SimpleRecord.class)); assertTrue(ClassUtil.isRecordType(RecordOfRecord.class)); - assertTrue(ClassUtil.isRecordType(JsonIgnoreRecord.class)); - assertTrue(ClassUtil.isRecordType(JsonPropertyRenameRecord.class)); + assertTrue(ClassUtil.isRecordType(RecordWithIgnore.class)); + assertTrue(ClassUtil.isRecordType(RecordWithRename.class)); } public void testRecordJavaType() { @@ -59,8 +59,8 @@ public void testRecordJavaType() { assertTrue(MAPPER.constructType(SimpleRecord.class).isRecordType()); assertTrue(MAPPER.constructType(RecordOfRecord.class).isRecordType()); - assertTrue(MAPPER.constructType(JsonIgnoreRecord.class).isRecordType()); - assertTrue(MAPPER.constructType(JsonPropertyRenameRecord.class).isRecordType()); + assertTrue(MAPPER.constructType(RecordWithIgnore.class).isRecordType()); + assertTrue(MAPPER.constructType(RecordWithRename.class).isRecordType()); } /* @@ -130,10 +130,7 @@ public void testDeserializeSimpleRecord_DisableAnnotationIntrospector() throws E */ public void testSerializeJsonIgnoreRecord() throws Exception { - JsonIgnoreRecord record = new JsonIgnoreRecord(123, "Bob"); - - String json = MAPPER.writeValueAsString(record); - + String json = MAPPER.writeValueAsString(new RecordWithIgnore(123, "Bob")); assertEquals("{\"id\":123}", json); } @@ -152,17 +149,15 @@ public void testDeserializeWithAltCtor() throws Exception { } public void testSerializeJsonRenameRecord() throws Exception { - JsonPropertyRenameRecord record = new JsonPropertyRenameRecord(123, "Bob"); - - String json = MAPPER.writeValueAsString(record); + String json = MAPPER.writeValueAsString(new RecordWithRename(123, "Bob")); final Object EXP = map("id", Integer.valueOf(123), "rename", "Bob"); assertEquals(EXP, MAPPER.readValue(json, Object.class)); } public void testDeserializeJsonRenameRecord() throws Exception { - JsonPropertyRenameRecord value = MAPPER.readValue("{\"id\":123,\"rename\":\"Bob\"}", - JsonPropertyRenameRecord.class); - assertEquals(new JsonPropertyRenameRecord(123, "Bob"), value); + RecordWithRename value = MAPPER.readValue("{\"id\":123,\"rename\":\"Bob\"}", + RecordWithRename.class); + assertEquals(new RecordWithRename(123, "Bob"), value); } private Map map(String key1, Object value1,