Skip to content

Commit 2abd8b9

Browse files
authored
Allow (de)serializing records using Bean(De)SerializerModifier even when reflection is unavailable (#3417)
1 parent e073287 commit 2abd8b9

File tree

6 files changed

+128
-34
lines changed

6 files changed

+128
-34
lines changed

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

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1412,6 +1412,10 @@ protected Object deserializeFromObjectUsingNonDefault(JsonParser p,
14121412
return ctxt.handleMissingInstantiator(raw, null, p,
14131413
"non-static inner classes like this can only by instantiated using default, no-argument constructor");
14141414
}
1415+
if (NativeImageUtil.needsReflectionConfiguration(raw)) {
1416+
return ctxt.handleMissingInstantiator(raw, null, p,
1417+
"cannot deserialize from Object value (no delegate- or property-based Creator): this appears to be a native image, in which case you may need to configure reflection for the class that is to be deserialized");
1418+
}
14151419
return ctxt.handleMissingInstantiator(raw, getValueInstantiator(), p,
14161420
"cannot deserialize from Object value (no delegate- or property-based Creator)");
14171421
}

src/main/java/com/fasterxml/jackson/databind/introspect/DefaultAccessorNamingStrategy.java

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
package com.fasterxml.jackson.databind.introspect;
22

3+
import java.util.Arrays;
4+
import java.util.Collections;
35
import java.util.HashSet;
46
import java.util.Set;
57

@@ -527,10 +529,10 @@ public RecordNaming(MapperConfig<?> config, AnnotatedClass forClass) {
527529
// trickier: regular fields are ok (handled differently), but should
528530
// we also allow getter discovery? For now let's do so
529531
"get", "is", null);
530-
_fieldNames = new HashSet<>();
531-
for (String name : JDK14Util.getRecordFieldNames(forClass.getRawType())) {
532-
_fieldNames.add(name);
533-
}
532+
String[] recordFieldNames = JDK14Util.getRecordFieldNames(forClass.getRawType());
533+
_fieldNames = recordFieldNames == null ?
534+
Collections.emptySet() :
535+
new HashSet<>(Arrays.asList(recordFieldNames));
534536
}
535537

536538
@Override

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

Lines changed: 50 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
import com.fasterxml.jackson.databind.DeserializationContext;
1313
import com.fasterxml.jackson.databind.introspect.AnnotatedConstructor;
1414
import com.fasterxml.jackson.databind.util.ClassUtil;
15+
import com.fasterxml.jackson.databind.util.NativeImageUtil;
1516

1617
/**
1718
* Helper class to support some of JDK 14 (and later) features
@@ -76,6 +77,10 @@ public static RecordAccessor instance() {
7677
public String[] getRecordFieldNames(Class<?> recordType) throws IllegalArgumentException
7778
{
7879
final Object[] components = recordComponents(recordType);
80+
if (components == null) {
81+
// not a record, or no reflective access on native image
82+
return null;
83+
}
7984
final String[] names = new String[components.length];
8085
for (int i = 0; i < components.length; i++) {
8186
try {
@@ -92,6 +97,10 @@ public String[] getRecordFieldNames(Class<?> recordType) throws IllegalArgumentE
9297
public RawTypeName[] getRecordFields(Class<?> recordType) throws IllegalArgumentException
9398
{
9499
final Object[] components = recordComponents(recordType);
100+
if (components == null) {
101+
// not a record, or no reflective access on native image
102+
return null;
103+
}
95104
final RawTypeName[] results = new RawTypeName[components.length];
96105
for (int i = 0; i < components.length; i++) {
97106
String name;
@@ -120,10 +129,14 @@ protected Object[] recordComponents(Class<?> recordType) throws IllegalArgumentE
120129
try {
121130
return (Object[]) RECORD_GET_RECORD_COMPONENTS.invoke(recordType);
122131
} catch (Exception e) {
132+
if (NativeImageUtil.isUnsupportedFeatureError(e)) {
133+
return null;
134+
}
123135
throw new IllegalArgumentException("Failed to access RecordComponents of type "
124136
+ClassUtil.nameOf(recordType));
125137
}
126138
}
139+
127140
}
128141

129142
static class RawTypeName {
@@ -153,37 +166,43 @@ static class CreatorLocator {
153166
_config = ctxt.getConfig();
154167

155168
_recordFields = RecordAccessor.instance().getRecordFields(beanDesc.getBeanClass());
156-
final int argCount = _recordFields.length;
157-
158-
// And then locate the canonical constructor; must be found, if not, fail
159-
// altogether (so we can figure out what went wrong)
160-
AnnotatedConstructor primary = null;
161-
162-
// One special case: empty Records, empty constructor is separate case
163-
if (argCount == 0) {
164-
primary = beanDesc.findDefaultConstructor();
165-
_constructors = Collections.singletonList(primary);
166-
} else {
169+
if (_recordFields == null) {
170+
// not a record, or no reflective access on native image
167171
_constructors = beanDesc.getConstructors();
168-
main_loop:
169-
for (AnnotatedConstructor ctor : _constructors) {
170-
if (ctor.getParameterCount() != argCount) {
171-
continue;
172-
}
173-
for (int i = 0; i < argCount; ++i) {
174-
if (!ctor.getRawParameterType(i).equals(_recordFields[i].rawType)) {
175-
continue main_loop;
172+
_primaryConstructor = null;
173+
} else {
174+
final int argCount = _recordFields.length;
175+
176+
// And then locate the canonical constructor; must be found, if not, fail
177+
// altogether (so we can figure out what went wrong)
178+
AnnotatedConstructor primary = null;
179+
180+
// One special case: empty Records, empty constructor is separate case
181+
if (argCount == 0) {
182+
primary = beanDesc.findDefaultConstructor();
183+
_constructors = Collections.singletonList(primary);
184+
} else {
185+
_constructors = beanDesc.getConstructors();
186+
main_loop:
187+
for (AnnotatedConstructor ctor : _constructors) {
188+
if (ctor.getParameterCount() != argCount) {
189+
continue;
176190
}
191+
for (int i = 0; i < argCount; ++i) {
192+
if (!ctor.getRawParameterType(i).equals(_recordFields[i].rawType)) {
193+
continue main_loop;
194+
}
195+
}
196+
primary = ctor;
197+
break;
177198
}
178-
primary = ctor;
179-
break;
180199
}
200+
if (primary == null) {
201+
throw new IllegalArgumentException("Failed to find the canonical Record constructor of type "
202+
+ClassUtil.getTypeDescription(_beanDesc.getType()));
203+
}
204+
_primaryConstructor = primary;
181205
}
182-
if (primary == null) {
183-
throw new IllegalArgumentException("Failed to find the canonical Record constructor of type "
184-
+ClassUtil.getTypeDescription(_beanDesc.getType()));
185-
}
186-
_primaryConstructor = primary;
187206
}
188207

189208
public AnnotatedConstructor locate(List<String> names)
@@ -205,6 +224,11 @@ public AnnotatedConstructor locate(List<String> names)
205224
}
206225
}
207226

227+
if (_recordFields == null) {
228+
// not a record, or no reflective access on native image
229+
return null;
230+
}
231+
208232
// By now we have established that the canonical constructor is the one to use
209233
// and just need to gather implicit names to return
210234
for (RawTypeName field : _recordFields) {

src/main/java/com/fasterxml/jackson/databind/ser/BeanSerializerFactory.java

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@
2929
import com.fasterxml.jackson.databind.util.ClassUtil;
3030
import com.fasterxml.jackson.databind.util.Converter;
3131
import com.fasterxml.jackson.databind.util.IgnorePropertiesUtil;
32+
import com.fasterxml.jackson.databind.util.NativeImageUtil;
3233

3334
/**
3435
* Factory class that can provide serializers for any regular Java beans
@@ -476,7 +477,10 @@ protected JsonSerializer<Object> constructBeanOrAddOnSerializer(SerializerProvid
476477
}
477478
if (ser == null) { // Means that no properties were found
478479
// 21-Aug-2020, tatu: Empty Records should be fine tho
479-
if (type.isRecordType()) {
480+
// 18-Mar-2022, yawkat: Record will also appear empty when missing reflection info.
481+
// needsReflectionConfiguration will check that a constructor is present, else we fall back to the empty
482+
// bean error msg
483+
if (type.isRecordType() && !NativeImageUtil.needsReflectionConfiguration(type.getRawClass())) {
480484
return builder.createDummy();
481485
}
482486

src/main/java/com/fasterxml/jackson/databind/ser/impl/UnknownSerializer.java

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
import com.fasterxml.jackson.databind.*;
88
import com.fasterxml.jackson.databind.jsontype.TypeSerializer;
99
import com.fasterxml.jackson.databind.ser.std.ToEmptyObjectSerializer;
10+
import com.fasterxml.jackson.databind.util.NativeImageUtil;
1011

1112
@SuppressWarnings("serial")
1213
public class UnknownSerializer
@@ -43,8 +44,15 @@ public void serializeWithType(Object value, JsonGenerator gen, SerializerProvide
4344

4445
protected void failForEmpty(SerializerProvider prov, Object value)
4546
throws JsonMappingException {
46-
prov.reportBadDefinition(handledType(), String.format(
47-
"No serializer found for class %s and no properties discovered to create BeanSerializer (to avoid exception, disable SerializationFeature.FAIL_ON_EMPTY_BEANS)",
48-
value.getClass().getName()));
47+
Class<?> cl = value.getClass();
48+
if (NativeImageUtil.needsReflectionConfiguration(cl)) {
49+
prov.reportBadDefinition(handledType(), String.format(
50+
"No serializer found for class %s and no properties discovered to create BeanSerializer (to avoid exception, disable SerializationFeature.FAIL_ON_EMPTY_BEANS). This appears to be a native image, in which case you may need to configure reflection for the class that is to be serialized",
51+
cl.getName()));
52+
} else {
53+
prov.reportBadDefinition(handledType(), String.format(
54+
"No serializer found for class %s and no properties discovered to create BeanSerializer (to avoid exception, disable SerializationFeature.FAIL_ON_EMPTY_BEANS)",
55+
cl.getName()));
56+
}
4957
}
5058
}
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
package com.fasterxml.jackson.databind.util;
2+
3+
import java.lang.reflect.InvocationTargetException;
4+
5+
/**
6+
* Utilities for graal native image support.
7+
*/
8+
public class NativeImageUtil {
9+
private static final boolean RUNNING_IN_SVM;
10+
11+
static {
12+
RUNNING_IN_SVM = System.getProperty("org.graalvm.nativeimage.imagecode") != null;
13+
}
14+
15+
private NativeImageUtil() {
16+
}
17+
18+
/**
19+
* Check whether we're running in substratevm native image runtime mode. This check cannot be a constant, because
20+
* the static initializer may run early during build time
21+
*/
22+
private static boolean isRunningInNativeImage() {
23+
return RUNNING_IN_SVM && System.getProperty("org.graalvm.nativeimage.imagecode").equals("runtime");
24+
}
25+
26+
/**
27+
* Check whether the given error is a substratevm UnsupportedFeatureError
28+
*/
29+
public static boolean isUnsupportedFeatureError(Throwable e) {
30+
if (!isRunningInNativeImage()) {
31+
return false;
32+
}
33+
if (e instanceof InvocationTargetException) {
34+
e = e.getCause();
35+
}
36+
return e.getClass().getName().equals("com.oracle.svm.core.jdk.UnsupportedFeatureError");
37+
}
38+
39+
/**
40+
* Check whether the given class is likely missing reflection configuration (running in native image, and no
41+
* members visible in reflection).
42+
*/
43+
public static boolean needsReflectionConfiguration(Class<?> cl) {
44+
if (!isRunningInNativeImage()) {
45+
return false;
46+
}
47+
// records list their fields but not other members
48+
return (cl.getDeclaredFields().length == 0 || ClassUtil.isRecordType(cl)) &&
49+
cl.getDeclaredMethods().length == 0 &&
50+
cl.getDeclaredConstructors().length == 0;
51+
}
52+
}

0 commit comments

Comments
 (0)