Skip to content

Commit 263b544

Browse files
committed
Getting close to completing #2709, now basic deserialization works; still need overrides
1 parent 963f34d commit 263b544

File tree

4 files changed

+182
-24
lines changed

4 files changed

+182
-24
lines changed

src/main/java/com/fasterxml/jackson/databind/deser/BasicDeserializerFactory.java

+36-7
Original file line numberDiff line numberDiff line change
@@ -7,12 +7,7 @@
77
import java.util.concurrent.*;
88
import java.util.concurrent.atomic.AtomicReference;
99

10-
import com.fasterxml.jackson.annotation.JacksonInject;
11-
import com.fasterxml.jackson.annotation.JsonCreator;
12-
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
13-
import com.fasterxml.jackson.annotation.JsonIncludeProperties;
14-
import com.fasterxml.jackson.annotation.JsonSetter;
15-
import com.fasterxml.jackson.annotation.Nulls;
10+
import com.fasterxml.jackson.annotation.*;
1611
import com.fasterxml.jackson.annotation.JsonCreator.Mode;
1712

1813
import com.fasterxml.jackson.core.JsonParser;
@@ -29,6 +24,7 @@
2924
import com.fasterxml.jackson.databind.exc.InvalidDefinitionException;
3025
import com.fasterxml.jackson.databind.ext.OptionalHandlerFactory;
3126
import com.fasterxml.jackson.databind.introspect.*;
27+
import com.fasterxml.jackson.databind.jdk14.JDK14Util;
3228
import com.fasterxml.jackson.databind.jsontype.NamedType;
3329
import com.fasterxml.jackson.databind.jsontype.TypeDeserializer;
3430
import com.fasterxml.jackson.databind.jsontype.TypeResolverBuilder;
@@ -278,6 +274,15 @@ protected ValueInstantiator _constructDefaultValueInstantiator(DeserializationCo
278274
_addDeserializerFactoryMethods(ctxt, beanDesc, vchecker, intr, creators, creatorDefs);
279275
// constructors only usable on concrete types:
280276
if (beanDesc.getType().isConcrete()) {
277+
// [databind#2709]: Record support
278+
if (beanDesc.getType().isRecordType()) {
279+
final List<String> names = new ArrayList<>();
280+
AnnotatedConstructor canonical = JDK14Util.findRecordConstructor(ctxt, beanDesc, names);
281+
if (canonical != null) {
282+
_addRecordConstructor(ctxt, beanDesc, creators, canonical, names);
283+
return creators.constructValueInstantiator(ctxt);
284+
}
285+
}
281286
_addDeserializerConstructors(ctxt, beanDesc, vchecker, intr, creators, creatorDefs);
282287
}
283288
return creators.constructValueInstantiator(ctxt);
@@ -542,6 +547,30 @@ protected void _addDeserializerConstructors(DeserializationContext ctxt,
542547
}
543548
}
544549

550+
/**
551+
* Helper method called when a {@code java.lang.Record} definition's "canonical"
552+
* constructor is to be used: if so, we have implicit names to consider.
553+
*
554+
* @since 2.12
555+
*/
556+
protected void _addRecordConstructor(DeserializationContext ctxt,
557+
BeanDescription beanDesc, CreatorCollector creators,
558+
AnnotatedConstructor canonical, List<String> names)
559+
throws JsonMappingException
560+
{
561+
final int argCount = canonical.getParameterCount();
562+
final AnnotationIntrospector intr = ctxt.getAnnotationIntrospector();
563+
final SettableBeanProperty[] properties = new SettableBeanProperty[argCount];
564+
565+
for (int i = 0; i < argCount; ++i) {
566+
final AnnotatedParameter param = canonical.getParameter(i);
567+
JacksonInject.Value injectable = intr.findInjectableValue(param);
568+
final PropertyName name = PropertyName.construct(names.get(i));
569+
properties[i] = constructCreatorProperty(ctxt, beanDesc, name, i, param, injectable);
570+
}
571+
creators.addPropertyCreator(canonical, false, properties);
572+
}
573+
545574
/**
546575
* Helper method called when there is the explicit "is-creator" with mode of "delegating"
547576
*
@@ -1038,7 +1067,7 @@ protected SettableBeanProperty constructCreatorProperty(DeserializationContext c
10381067

10391068
private PropertyName _findParamName(AnnotatedParameter param, AnnotationIntrospector intr)
10401069
{
1041-
if (param != null && intr != null) {
1070+
if (intr != null) {
10421071
PropertyName name = intr.findNameForDeserialization(param);
10431072
if (name != null) {
10441073
return name;

src/main/java/com/fasterxml/jackson/databind/deser/impl/CreatorCandidate.java

+1-1
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,7 @@ public PropertyName explicitParamName(int i) {
5858
}
5959
return null;
6060
}
61-
61+
6262
public PropertyName findImplicitParamName(int i) {
6363
String str = _intr.findImplicitPropertyName(_params[i].annotated);
6464
if (str != null && !str.isEmpty()) {

src/main/java/com/fasterxml/jackson/databind/jdk14/JDK14Util.java

+134
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,21 @@
11
package com.fasterxml.jackson.databind.jdk14;
22

33
import java.lang.reflect.Method;
4+
import java.util.Collections;
5+
import java.util.List;
6+
import java.util.Map;
47

8+
import com.fasterxml.jackson.annotation.JsonCreator;
9+
import com.fasterxml.jackson.annotation.JsonCreator.Mode;
10+
import com.fasterxml.jackson.databind.AnnotationIntrospector;
11+
import com.fasterxml.jackson.databind.BeanDescription;
12+
import com.fasterxml.jackson.databind.DeserializationConfig;
13+
import com.fasterxml.jackson.databind.DeserializationContext;
14+
import com.fasterxml.jackson.databind.deser.impl.CreatorCollector;
15+
import com.fasterxml.jackson.databind.introspect.AnnotatedConstructor;
16+
import com.fasterxml.jackson.databind.introspect.AnnotatedWithParams;
17+
import com.fasterxml.jackson.databind.introspect.BeanPropertyDefinition;
18+
import com.fasterxml.jackson.databind.introspect.VisibilityChecker;
519
import com.fasterxml.jackson.databind.util.ClassUtil;
620

721
/**
@@ -18,6 +32,12 @@ public static String[] getRecordFieldNames(Class<?> recordType) {
1832
return RecordAccessor.instance().getRecordFieldNames(recordType);
1933
}
2034

35+
public static AnnotatedConstructor findRecordConstructor(DeserializationContext ctxt,
36+
BeanDescription beanDesc, List<String> names) {
37+
return new CreatorLocator(ctxt, beanDesc)
38+
.locate(names);
39+
}
40+
2141
static class RecordAccessor {
2242
private final Method RECORD_GET_RECORD_COMPONENTS;
2343
private final Method RECORD_COMPONENT_GET_NAME;
@@ -74,6 +94,32 @@ public String[] getRecordFieldNames(Class<?> recordType) throws IllegalArgumentE
7494
return names;
7595
}
7696

97+
public RawTypeName[] getRecordFields(Class<?> recordType) throws IllegalArgumentException
98+
{
99+
final Object[] components = recordComponents(recordType);
100+
final RawTypeName[] results = new RawTypeName[components.length];
101+
for (int i = 0; i < components.length; i++) {
102+
String name;
103+
try {
104+
name = (String) RECORD_COMPONENT_GET_NAME.invoke(components[i]);
105+
} catch (Exception e) {
106+
throw new IllegalArgumentException(String.format(
107+
"Failed to access name of field #%d (of %d) of Record type %s",
108+
i, components.length, ClassUtil.nameOf(recordType)), e);
109+
}
110+
Class<?> type;
111+
try {
112+
type = (Class<?>) RECORD_COMPONENT_GET_TYPE.invoke(components[i]);
113+
} catch (Exception e) {
114+
throw new IllegalArgumentException(String.format(
115+
"Failed to access type of field #%d (of %d) of Record type %s",
116+
i, components.length, ClassUtil.nameOf(recordType)), e);
117+
}
118+
results[i] = new RawTypeName(type, name);
119+
}
120+
return results;
121+
}
122+
77123
protected Object[] recordComponents(Class<?> recordType) throws IllegalArgumentException
78124
{
79125
try {
@@ -84,4 +130,92 @@ protected Object[] recordComponents(Class<?> recordType) throws IllegalArgumentE
84130
}
85131
}
86132
}
133+
134+
static class RawTypeName {
135+
public final Class<?> rawType;
136+
public final String name;
137+
138+
public RawTypeName(Class<?> rt, String n) {
139+
rawType = rt;
140+
name = n;
141+
}
142+
}
143+
144+
static class CreatorLocator {
145+
protected final BeanDescription _beanDesc;
146+
protected final DeserializationConfig _config;
147+
protected final AnnotationIntrospector _intr;
148+
149+
protected final List<AnnotatedConstructor> _constructors;
150+
protected final AnnotatedConstructor _primaryConstructor;
151+
protected final RawTypeName[] _recordFields;
152+
153+
CreatorLocator(DeserializationContext ctxt, BeanDescription beanDesc)
154+
{
155+
_beanDesc = beanDesc;
156+
157+
_intr = ctxt.getAnnotationIntrospector();
158+
_config = ctxt.getConfig();
159+
160+
_recordFields = RecordAccessor.instance().getRecordFields(beanDesc.getBeanClass());
161+
final int argCount = _recordFields.length;
162+
163+
// And then locate the canonical constructor; must be found, if not, fail
164+
// altogether (so we can figure out what went wrong)
165+
AnnotatedConstructor primary = null;
166+
167+
// One special case: empty Records, empty constructor is separate case
168+
if (argCount == 0) {
169+
primary = beanDesc.findDefaultConstructor();
170+
_constructors = Collections.singletonList(primary);
171+
} else {
172+
_constructors = beanDesc.getConstructors();
173+
main_loop:
174+
for (AnnotatedConstructor ctor : _constructors) {
175+
if (ctor.getParameterCount() != argCount) {
176+
continue;
177+
}
178+
for (int i = 0; i < argCount; ++i) {
179+
if (!ctor.getRawParameterType(i).equals(_recordFields[i].rawType)) {
180+
continue main_loop;
181+
}
182+
}
183+
primary = ctor;
184+
break;
185+
}
186+
}
187+
if (primary == null) {
188+
throw new IllegalArgumentException("Failed to find the canonical Record constructor of type "
189+
+ClassUtil.getTypeDescription(_beanDesc.getType()));
190+
}
191+
_primaryConstructor = primary;
192+
}
193+
194+
public AnnotatedConstructor locate(List<String> names)
195+
{
196+
// First things first: ensure that either there are no explicit marked constructors
197+
// or that there is just one and it is the canonical one and it is not
198+
// declared as "delegating" constructor
199+
for (AnnotatedConstructor ctor : _constructors) {
200+
JsonCreator.Mode creatorMode = _intr.findCreatorAnnotation(_config, ctor);
201+
if ((null == creatorMode) || (Mode.DISABLED == creatorMode)) {
202+
continue;
203+
}
204+
// If there's a delegating Creator let caller figure out
205+
if (Mode.DELEGATING == creatorMode) {
206+
return null;
207+
}
208+
if (ctor != _primaryConstructor) {
209+
return null;
210+
}
211+
}
212+
213+
// By now we have established that the canonical constructor is the one to use
214+
// and just need to gather implicit names to return
215+
for (RawTypeName field : _recordFields) {
216+
names.add(field.name);
217+
}
218+
return _primaryConstructor;
219+
}
220+
}
87221
}

src/test-jdk14/java/com/fasterxml/jackson/databind/RecordTest.java

+11-16
Original file line numberDiff line numberDiff line change
@@ -31,9 +31,9 @@ public RecordWithAltCtor(@JsonProperty("id") int id) {
3131
}
3232
}
3333

34-
record JsonIgnoreRecord(int id, @JsonIgnore String name) { }
34+
record RecordWithIgnore(int id, @JsonIgnore String name) { }
3535

36-
record JsonPropertyRenameRecord(int id, @JsonProperty("rename")String name) { }
36+
record RecordWithRename(int id, @JsonProperty("rename")String name) { }
3737

3838
record EmptyRecord() { }
3939

@@ -50,17 +50,17 @@ public void testClassUtil() {
5050

5151
assertTrue(ClassUtil.isRecordType(SimpleRecord.class));
5252
assertTrue(ClassUtil.isRecordType(RecordOfRecord.class));
53-
assertTrue(ClassUtil.isRecordType(JsonIgnoreRecord.class));
54-
assertTrue(ClassUtil.isRecordType(JsonPropertyRenameRecord.class));
53+
assertTrue(ClassUtil.isRecordType(RecordWithIgnore.class));
54+
assertTrue(ClassUtil.isRecordType(RecordWithRename.class));
5555
}
5656

5757
public void testRecordJavaType() {
5858
assertFalse(MAPPER.constructType(getClass()).isRecordType());
5959

6060
assertTrue(MAPPER.constructType(SimpleRecord.class).isRecordType());
6161
assertTrue(MAPPER.constructType(RecordOfRecord.class).isRecordType());
62-
assertTrue(MAPPER.constructType(JsonIgnoreRecord.class).isRecordType());
63-
assertTrue(MAPPER.constructType(JsonPropertyRenameRecord.class).isRecordType());
62+
assertTrue(MAPPER.constructType(RecordWithIgnore.class).isRecordType());
63+
assertTrue(MAPPER.constructType(RecordWithRename.class).isRecordType());
6464
}
6565

6666
/*
@@ -130,10 +130,7 @@ public void testDeserializeSimpleRecord_DisableAnnotationIntrospector() throws E
130130
*/
131131

132132
public void testSerializeJsonIgnoreRecord() throws Exception {
133-
JsonIgnoreRecord record = new JsonIgnoreRecord(123, "Bob");
134-
135-
String json = MAPPER.writeValueAsString(record);
136-
133+
String json = MAPPER.writeValueAsString(new RecordWithIgnore(123, "Bob"));
137134
assertEquals("{\"id\":123}", json);
138135
}
139136

@@ -152,17 +149,15 @@ public void testDeserializeWithAltCtor() throws Exception {
152149
}
153150

154151
public void testSerializeJsonRenameRecord() throws Exception {
155-
JsonPropertyRenameRecord record = new JsonPropertyRenameRecord(123, "Bob");
156-
157-
String json = MAPPER.writeValueAsString(record);
152+
String json = MAPPER.writeValueAsString(new RecordWithRename(123, "Bob"));
158153
final Object EXP = map("id", Integer.valueOf(123), "rename", "Bob");
159154
assertEquals(EXP, MAPPER.readValue(json, Object.class));
160155
}
161156

162157
public void testDeserializeJsonRenameRecord() throws Exception {
163-
JsonPropertyRenameRecord value = MAPPER.readValue("{\"id\":123,\"rename\":\"Bob\"}",
164-
JsonPropertyRenameRecord.class);
165-
assertEquals(new JsonPropertyRenameRecord(123, "Bob"), value);
158+
RecordWithRename value = MAPPER.readValue("{\"id\":123,\"rename\":\"Bob\"}",
159+
RecordWithRename.class);
160+
assertEquals(new RecordWithRename(123, "Bob"), value);
166161
}
167162

168163
private Map<String,Object> map(String key1, Object value1,

0 commit comments

Comments
 (0)