Skip to content

Commit 4c74c8b

Browse files
committed
Fix #5006: add MapperFeature.REQUIRE_HANDLERS_FOR_JAVA8_OPTIONALS
1 parent b5e54e5 commit 4c74c8b

File tree

6 files changed

+81
-7
lines changed

6 files changed

+81
-7
lines changed

release-notes/VERSION-2.x

+2
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,8 @@ Project: jackson-databind
6767
#4963: Serializing `Map.Entry` as Bean with `@JsonFormat.shape = Shape.OBJECT`
6868
fails on JDK 17+
6969
#4997: `ObjectNode` put methods should do null check for key
70+
#5006: Add `MapperFeature.REQUIRE_HANDLERS_FOR_JAVA8_OPTIONALS` to prevent
71+
failure of `java.util.Optional` (de)serialization without Java 8 module
7072
#5014: Add `java.lang.Runnable` as unsafe base type in `DefaultBaseTypeLimitingValidator`
7173
#5020: Support new `@JsonProperty.isRequired` for overridable definition of "required-ness"
7274

src/main/java/com/fasterxml/jackson/databind/MapperFeature.java

+15-1
Original file line numberDiff line numberDiff line change
@@ -647,7 +647,21 @@ public enum MapperFeature implements ConfigFeature
647647
*
648648
* @since 2.13
649649
*/
650-
APPLY_DEFAULT_VALUES(true)
650+
APPLY_DEFAULT_VALUES(true),
651+
652+
/**
653+
* Feature that determines what happens if Java 8 {@link java.util.Optional} (and
654+
* other related optional types) are to be serialized or deserialized, but there
655+
* are no registered handlers for them.
656+
* If enabled, an exception is thrown (to indicate problem, a solution for which is
657+
* to register {@code jackson-datatype-jdk8} module); if disabled, the value is
658+
* serialized and/or deserialized using regular POJO ("Bean") (de)serialization.
659+
*<p>
660+
* Feature is enabled by default.
661+
*
662+
* @since 2.19
663+
*/
664+
REQUIRE_HANDLERS_FOR_JAVA8_OPTIONALS(true)
651665
;
652666

653667
private final boolean _defaultState;

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

+1-1
Original file line numberDiff line numberDiff line change
@@ -208,7 +208,7 @@ protected JsonDeserializer<Object> _findUnsupportedTypeDeserializer(Deserializat
208208
{
209209
// 05-May-2020, tatu: Should we check for possible Shape override to "POJO"?
210210
// (to let users force 'serialize-as-POJO'? Or not?
211-
final String errorMsg = BeanUtil.checkUnsupportedType(type);
211+
final String errorMsg = BeanUtil.checkUnsupportedType(ctxt.getConfig(), type);
212212
if (errorMsg != null) {
213213
// 30-Sep-2020, tatu: [databind#2867] Avoid checks if there is a mix-in
214214
// which likely providers a handler...

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

+1-1
Original file line numberDiff line numberDiff line change
@@ -913,7 +913,7 @@ protected JsonSerializer<?> _findUnsupportedTypeSerializer(SerializerProvider ct
913913
{
914914
// 05-May-2020, tatu: Should we check for possible Shape override to "POJO"?
915915
// (to let users force 'serialize-as-POJO'?
916-
final String errorMsg = BeanUtil.checkUnsupportedType(type);
916+
final String errorMsg = BeanUtil.checkUnsupportedType(ctxt.getConfig(), type);
917917
if (errorMsg != null) {
918918
// 30-Sep-2020, tatu: [databind#2867] Avoid checks if there is a mix-in
919919
// which likely providers a handler...

src/main/java/com/fasterxml/jackson/databind/util/BeanUtil.java

+25-3
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@
77
import com.fasterxml.jackson.annotation.JsonInclude;
88

99
import com.fasterxml.jackson.databind.JavaType;
10+
import com.fasterxml.jackson.databind.MapperFeature;
11+
import com.fasterxml.jackson.databind.cfg.MapperConfig;
1012
import com.fasterxml.jackson.databind.introspect.AnnotatedMethod;
1113

1214
/**
@@ -293,11 +295,12 @@ public static String stdManglePropertyName(final String basename, final int offs
293295
* "well-known" types for which there would be a datatype module; and if so,
294296
* return appropriate failure message to give to caller.
295297
*
296-
* @since 2.12
298+
* @since 2.19
297299
*/
298-
public static String checkUnsupportedType(JavaType type) {
300+
public static String checkUnsupportedType(MapperConfig<?> config, JavaType type) {
299301
final String className = type.getRawClass().getName();
300302
String typeName, moduleName;
303+
MapperFeature failFeature = null;
301304

302305
if (isJava8TimeClass(className)) {
303306
// [modules-java8#207]: do NOT check/block helper types in sub-packages,
@@ -315,13 +318,32 @@ public static String checkUnsupportedType(JavaType type) {
315318
typeName = "Joda date/time";
316319
moduleName = "com.fasterxml.jackson.datatype:jackson-datatype-joda";
317320
} else if (isJava8OptionalClass(className)) {
321+
failFeature = MapperFeature.REQUIRE_HANDLERS_FOR_JAVA8_OPTIONALS;
322+
final boolean fail = (config == null) || config.isEnabled(failFeature);
323+
if (!fail) {
324+
return null;
325+
}
318326
typeName = "Java 8 optional";
319327
moduleName = "com.fasterxml.jackson.datatype:jackson-datatype-jdk8";
320328
} else {
321329
return null;
322330
}
323-
return String.format("%s type %s not supported by default: add Module \"%s\" to enable handling",
331+
String str = String.format("%s type %s not supported by default: add Module \"%s\" to enable handling",
324332
typeName, ClassUtil.getTypeDescription(type), moduleName);
333+
if (failFeature != null) {
334+
str = String.format("%s (or disable `MapperFeature.%s`)",
335+
str, MapperFeature.REQUIRE_HANDLERS_FOR_JAVA8_OPTIONALS.name());
336+
}
337+
return str;
338+
}
339+
340+
/**
341+
* @since 2.12
342+
* @deprecated since 2.19
343+
*/
344+
@Deprecated // since 2.19
345+
public static String checkUnsupportedType(JavaType type) {
346+
return checkUnsupportedType(null, type);
325347
}
326348

327349
/**

src/test/java/com/fasterxml/jackson/databind/interop/OptionalJava8Fallbacks4082Test.java

+37-1
Original file line numberDiff line numberDiff line change
@@ -10,20 +10,28 @@
1010
import com.fasterxml.jackson.core.*;
1111
import com.fasterxml.jackson.databind.*;
1212
import com.fasterxml.jackson.databind.exc.InvalidDefinitionException;
13+
import com.fasterxml.jackson.databind.json.JsonMapper;
1314
import com.fasterxml.jackson.databind.testutil.DatabindTestUtil;
1415
import com.fasterxml.jackson.databind.util.TokenBuffer;
1516

17+
import static org.assertj.core.api.Assertions.assertThat;
1618
import static org.junit.jupiter.api.Assertions.*;
1719

1820
// [databind#4082]: add fallback handling for Java 8 Optional types, to
1921
// prevent accidental serialization as POJOs, as well as give more information
2022
// on deserialization attempts
23+
// [databind#5006]: add `MapperFeature.REQUIRE_HANDLERS_FOR_JAVA8_OPTIONALS`
2124
//
22-
// @since 2.16
25+
// @since 2.16 (changed in 2.19)
2326
public class OptionalJava8Fallbacks4082Test extends DatabindTestUtil
2427
{
2528
private final ObjectMapper MAPPER = newJsonMapper();
2629

30+
ObjectMapper LENIENT_MAPPER = JsonMapper.builder()
31+
.disable(MapperFeature.REQUIRE_HANDLERS_FOR_JAVA8_OPTIONALS)
32+
.disable(MapperFeature.CAN_OVERRIDE_ACCESS_MODIFIERS)
33+
.build();
34+
2735
// Test to prevent serialization as POJO, without Java 8 date/time module:
2836
@Test
2937
public void testPreventSerialization() throws Exception {
@@ -42,6 +50,7 @@ private void _testPreventSerialization(Object value) throws Exception
4250
verifyException(e, "Java 8 optional type `"+value.getClass().getName()
4351
+"` not supported by default");
4452
verifyException(e, "add Module \"com.fasterxml.jackson.datatype:jackson-datatype-jdk8\"");
53+
verifyException(e, "(or disable `MapperFeature.REQUIRE_HANDLERS_FOR_JAVA8_OPTIONALS`");
4554
}
4655
}
4756

@@ -62,6 +71,7 @@ private void _testBetterDeserializationError(Class<?> target) throws Exception
6271
} catch (InvalidDefinitionException e) {
6372
verifyException(e, "Java 8 optional type `"+target.getName()+"` not supported by default");
6473
verifyException(e, "add Module \"com.fasterxml.jackson.datatype:jackson-datatype-jdk8\"");
74+
verifyException(e, "(or disable `MapperFeature.REQUIRE_HANDLERS_FOR_JAVA8_OPTIONALS`");
6575
}
6676
}
6777

@@ -93,4 +103,30 @@ public void testAllowAsEmbedded() throws Exception
93103
}
94104
}
95105
}
106+
107+
// [databind#5006]
108+
@Test
109+
public void testAllowDeserializationWithFeature() throws Exception
110+
{
111+
_testAllowDeserializationLenient(Optional.class);
112+
_testAllowDeserializationLenient(OptionalInt.class);
113+
_testAllowDeserializationLenient(OptionalLong.class);
114+
_testAllowDeserializationLenient(OptionalDouble.class);
115+
}
116+
117+
private void _testAllowDeserializationLenient(Class<?> target) throws Exception {
118+
JacksonException e = assertThrows(JacksonException.class, () ->
119+
LENIENT_MAPPER.readValue("{}", target));
120+
assertThat(e).hasMessageContaining("Cannot construct instance of `"+target.getName()+"`");
121+
}
122+
123+
// [databind#5006]
124+
@Test
125+
public void testAllowSerializationWithFeature() throws Exception
126+
{
127+
assertThat(LENIENT_MAPPER.writeValueAsString(Optional.empty())).contains("\"empty\":true");
128+
assertThat(LENIENT_MAPPER.writeValueAsString(OptionalInt.of(13))).contains("\"asInt\":13");
129+
assertThat(LENIENT_MAPPER.writeValueAsString(OptionalLong.of(-1L))).contains("\"asLong\":-1");
130+
assertThat(LENIENT_MAPPER.writeValueAsString(OptionalDouble.of(0.5))).contains("\"asDouble\":0.5");
131+
}
96132
}

0 commit comments

Comments
 (0)