From 1cec560199fa3acea87886a21ed23da89c4e9f79 Mon Sep 17 00:00:00 2001 From: lux-liang <249971141+lux-liang@users.noreply.github.com> Date: Mon, 21 Sep 2026 11:25:32 +0800 Subject: [PATCH 1/2] Fix #389: validate embedded JSR-310 values --- .../JSR310StringParsableDeserializer.java | 6 +- .../JSR310StringParsableDeserializerTest.java | 75 +++++++++++++++++++ release-notes/VERSION-2.x | 3 + 3 files changed, 83 insertions(+), 1 deletion(-) create mode 100644 datetime/src/test/java/com/fasterxml/jackson/datatype/jsr310/deser/JSR310StringParsableDeserializerTest.java diff --git a/datetime/src/main/java/com/fasterxml/jackson/datatype/jsr310/deser/JSR310StringParsableDeserializer.java b/datetime/src/main/java/com/fasterxml/jackson/datatype/jsr310/deser/JSR310StringParsableDeserializer.java index 96596a34..1e0d0a4e 100644 --- a/datetime/src/main/java/com/fasterxml/jackson/datatype/jsr310/deser/JSR310StringParsableDeserializer.java +++ b/datetime/src/main/java/com/fasterxml/jackson/datatype/jsr310/deser/JSR310StringParsableDeserializer.java @@ -131,7 +131,11 @@ public Object deserialize(JsonParser p, DeserializationContext ctxt) throws IOEx } else if (p.hasToken(JsonToken.VALUE_EMBEDDED_OBJECT)) { // 20-Apr-2016, tatu: Related to [databind#1208], can try supporting embedded // values quite easily - return p.getEmbeddedObject(); + Object value = p.getEmbeddedObject(); + if ((value == null) || _valueClass.isInstance(value)) { + return value; + } + return ctxt.handleUnexpectedToken(_valueClass, p); } else if (p.isExpectedStartArrayToken()) { return _deserializeFromArray(p, ctxt); } diff --git a/datetime/src/test/java/com/fasterxml/jackson/datatype/jsr310/deser/JSR310StringParsableDeserializerTest.java b/datetime/src/test/java/com/fasterxml/jackson/datatype/jsr310/deser/JSR310StringParsableDeserializerTest.java new file mode 100644 index 00000000..2826de32 --- /dev/null +++ b/datetime/src/test/java/com/fasterxml/jackson/datatype/jsr310/deser/JSR310StringParsableDeserializerTest.java @@ -0,0 +1,75 @@ +package com.fasterxml.jackson.datatype.jsr310.deser; + +import java.io.IOException; +import java.time.Period; +import java.time.ZoneId; +import java.time.ZoneOffset; +import java.util.Map; +import java.util.stream.Stream; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; + +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.databind.JavaType; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.exc.MismatchedInputException; +import com.fasterxml.jackson.databind.util.TokenBuffer; +import com.fasterxml.jackson.datatype.jsr310.ModuleTestBase; + +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertSame; +import static org.junit.jupiter.api.Assertions.assertThrows; + +public class JSR310StringParsableDeserializerTest extends ModuleTestBase +{ + private final ObjectMapper MAPPER = newMapper(); + + static Stream supportedTypes() + { + return Stream.of( + Arguments.of(Period.class, Period.ofDays(1)), + Arguments.of(ZoneId.class, ZoneId.of("Europe/Helsinki")), + Arguments.of(ZoneOffset.class, ZoneOffset.ofHours(2)) + ); + } + + // [modules-java8#389] + @ParameterizedTest + @MethodSource("supportedTypes") + public void testRejectsIncompatibleEmbeddedObject(Class targetType, + Object validValue) throws Exception + { + assertSame(validValue, readEmbeddedValue(targetType, validValue)); + + MismatchedInputException e = assertThrows(MismatchedInputException.class, + () -> readEmbeddedValue(targetType, new byte[] { 1, 2, 3 })); + verifyException(e, targetType.getName()); + } + + @Test + public void testAllowsNullEmbeddedObject() throws Exception + { + assertNull(readEmbeddedValue(ZoneId.class, null)); + } + + private Object readEmbeddedValue(Class targetType, Object embeddedValue) + throws IOException + { + try (TokenBuffer buffer = new TokenBuffer(MAPPER, false)) { + buffer.writeStartObject(); + buffer.writeFieldName("value"); + buffer.writeEmbeddedObject(embeddedValue); + buffer.writeEndObject(); + + JavaType mapType = MAPPER.getTypeFactory() + .constructMapType(Map.class, String.class, targetType); + try (JsonParser parser = buffer.asParser()) { + Map result = MAPPER.readValue(parser, mapType); + return result.get("value"); + } + } + } +} diff --git a/release-notes/VERSION-2.x b/release-notes/VERSION-2.x index 24110a35..0fb69c52 100644 --- a/release-notes/VERSION-2.x +++ b/release-notes/VERSION-2.x @@ -14,6 +14,9 @@ Modules: `JavaTimeFeature.ALWAYS_WRITE_SUBSECOND_DIGITS`) (reported by @rycler) (fix contributed by Seonwoo J) +#389: Validate embedded objects for `ZoneId`, `Period` and `ZoneOffset` + (reported by @manqingzhou) + (fix contributed by @lux-liang) 2.22.2 (16-Aug-2026) 2.22.1 (07-Jul-2026) From 25da016b55dd2c5e88f6efcd9b43f7e3fea1514f Mon Sep 17 00:00:00 2001 From: Tatu Saloranta Date: Mon, 21 Sep 2026 18:52:31 -0700 Subject: [PATCH 2/2] Extend the fix a bit --- .../jsr310/deser/DurationDeserializer.java | 4 +- .../jsr310/deser/InstantDeserializer.java | 5 +- .../jsr310/deser/JSR310DeserializerBase.java | 33 +++++ .../JSR310StringParsableDeserializer.java | 8 +- .../jsr310/deser/LocalDateDeserializer.java | 2 +- .../deser/LocalDateTimeDeserializer.java | 2 +- .../jsr310/deser/LocalTimeDeserializer.java | 2 +- .../jsr310/deser/MonthDayDeserializer.java | 2 +- .../jsr310/deser/OffsetTimeDeserializer.java | 2 +- .../jsr310/deser/YearDeserializer.java | 2 +- .../jsr310/deser/YearMonthDeserializer.java | 2 +- .../deser/EmbeddedObjectDeser389Test.java | 131 ++++++++++++++++++ .../JSR310StringParsableDeserializerTest.java | 75 ---------- release-notes/CREDITS-2.x | 10 ++ release-notes/VERSION-2.x | 3 +- 15 files changed, 186 insertions(+), 97 deletions(-) create mode 100644 datetime/src/test/java/com/fasterxml/jackson/datatype/jsr310/deser/EmbeddedObjectDeser389Test.java delete mode 100644 datetime/src/test/java/com/fasterxml/jackson/datatype/jsr310/deser/JSR310StringParsableDeserializerTest.java diff --git a/datetime/src/main/java/com/fasterxml/jackson/datatype/jsr310/deser/DurationDeserializer.java b/datetime/src/main/java/com/fasterxml/jackson/datatype/jsr310/deser/DurationDeserializer.java index 79a1fc25..c5b7cba6 100644 --- a/datetime/src/main/java/com/fasterxml/jackson/datatype/jsr310/deser/DurationDeserializer.java +++ b/datetime/src/main/java/com/fasterxml/jackson/datatype/jsr310/deser/DurationDeserializer.java @@ -163,9 +163,7 @@ public Duration deserialize(JsonParser parser, DeserializationContext context) t case JsonTokenId.ID_STRING: return _fromString(parser, context, parser.getText()); case JsonTokenId.ID_EMBEDDED_OBJECT: - // 20-Apr-2016, tatu: Related to [databind#1208], can try supporting embedded - // values quite easily - return (Duration) parser.getEmbeddedObject(); + return _fromEmbedded(parser, context); case JsonTokenId.ID_START_ARRAY: return _deserializeFromArray(parser, context); // 30-Sep-2020, tatu: New! "Scalar from Object" (mostly for XML) diff --git a/datetime/src/main/java/com/fasterxml/jackson/datatype/jsr310/deser/InstantDeserializer.java b/datetime/src/main/java/com/fasterxml/jackson/datatype/jsr310/deser/InstantDeserializer.java index ee50ac63..c48fa038 100644 --- a/datetime/src/main/java/com/fasterxml/jackson/datatype/jsr310/deser/InstantDeserializer.java +++ b/datetime/src/main/java/com/fasterxml/jackson/datatype/jsr310/deser/InstantDeserializer.java @@ -344,7 +344,6 @@ protected JSR310DateTimeDeserializerBase _withFormatOverrides(Deserialization return deser; } - @SuppressWarnings("unchecked") @Override public T deserialize(JsonParser parser, DeserializationContext context) throws IOException { @@ -359,9 +358,7 @@ public T deserialize(JsonParser parser, DeserializationContext context) throws I case JsonTokenId.ID_STRING: return _fromString(parser, context, parser.getText()); case JsonTokenId.ID_EMBEDDED_OBJECT: - // 20-Apr-2016, tatu: Related to [databind#1208], can try supporting embedded - // values quite easily - return (T) parser.getEmbeddedObject(); + return _fromEmbedded(parser, context); case JsonTokenId.ID_START_ARRAY: return _deserializeFromArray(parser, context); diff --git a/datetime/src/main/java/com/fasterxml/jackson/datatype/jsr310/deser/JSR310DeserializerBase.java b/datetime/src/main/java/com/fasterxml/jackson/datatype/jsr310/deser/JSR310DeserializerBase.java index b48b9225..80aafe8e 100644 --- a/datetime/src/main/java/com/fasterxml/jackson/datatype/jsr310/deser/JSR310DeserializerBase.java +++ b/datetime/src/main/java/com/fasterxml/jackson/datatype/jsr310/deser/JSR310DeserializerBase.java @@ -149,6 +149,39 @@ protected boolean _isValidTimestampString(String str) { return _isIntNumber(str) && NumberInput.inLongRange(str, (str.charAt(0) == '-')); } + /** + * Helper method called to deserialize value from an "embedded object" + * (see {@link JsonToken#VALUE_EMBEDDED_OBJECT}): something binary formats + * (CBOR, Smile, Ion) and token buffering may expose. + *

+ * Since there is no guarantee that the embedded value actually is of the + * expected type -- and since generic containers (like + * {@code Map}) perform no runtime check of their own, due + * to type erasure -- value is verified to be compatible with the type this + * deserializer handles; incompatible value is passed to + * {@link DeserializationContext#handleUnexpectedToken} for possible recovery, + * or, failing that, for reporting as + * {@link com.fasterxml.jackson.databind.exc.MismatchedInputException}. + * + * @since 2.23 + */ + @SuppressWarnings("unchecked") + protected T _fromEmbedded(JsonParser p, DeserializationContext ctxt) + throws IOException + { + // 20-Apr-2016, tatu: Related to [databind#1208], can try supporting embedded + // values quite easily + Object value = p.getEmbeddedObject(); + if (value == null) { + return getNullValue(ctxt); + } + // 21-Sep-2026, tatu: [modules-java8#389] Must verify type compatibility + if (_valueClass.isInstance(value)) { + return (T) value; + } + return (T) ctxt.handleUnexpectedToken(_valueClass, p); + } + protected BOGUS _reportWrongToken(DeserializationContext context, JsonToken exp, String unit) throws IOException { diff --git a/datetime/src/main/java/com/fasterxml/jackson/datatype/jsr310/deser/JSR310StringParsableDeserializer.java b/datetime/src/main/java/com/fasterxml/jackson/datatype/jsr310/deser/JSR310StringParsableDeserializer.java index 1e0d0a4e..a3187454 100644 --- a/datetime/src/main/java/com/fasterxml/jackson/datatype/jsr310/deser/JSR310StringParsableDeserializer.java +++ b/datetime/src/main/java/com/fasterxml/jackson/datatype/jsr310/deser/JSR310StringParsableDeserializer.java @@ -129,13 +129,7 @@ public Object deserialize(JsonParser p, DeserializationContext ctxt) throws IOEx } // fall through } else if (p.hasToken(JsonToken.VALUE_EMBEDDED_OBJECT)) { - // 20-Apr-2016, tatu: Related to [databind#1208], can try supporting embedded - // values quite easily - Object value = p.getEmbeddedObject(); - if ((value == null) || _valueClass.isInstance(value)) { - return value; - } - return ctxt.handleUnexpectedToken(_valueClass, p); + return _fromEmbedded(p, ctxt); } else if (p.isExpectedStartArrayToken()) { return _deserializeFromArray(p, ctxt); } diff --git a/datetime/src/main/java/com/fasterxml/jackson/datatype/jsr310/deser/LocalDateDeserializer.java b/datetime/src/main/java/com/fasterxml/jackson/datatype/jsr310/deser/LocalDateDeserializer.java index 5775a1d4..9b4da1d0 100644 --- a/datetime/src/main/java/com/fasterxml/jackson/datatype/jsr310/deser/LocalDateDeserializer.java +++ b/datetime/src/main/java/com/fasterxml/jackson/datatype/jsr310/deser/LocalDateDeserializer.java @@ -168,7 +168,7 @@ public LocalDate deserialize(JsonParser p, DeserializationContext ctxt) throws I t); } if (p.hasToken(JsonToken.VALUE_EMBEDDED_OBJECT)) { - return (LocalDate) p.getEmbeddedObject(); + return _fromEmbedded(p, ctxt); } // 06-Jan-2018, tatu: Is this actually safe? Do users expect such coercion? if (p.hasToken(JsonToken.VALUE_NUMBER_INT)) { diff --git a/datetime/src/main/java/com/fasterxml/jackson/datatype/jsr310/deser/LocalDateTimeDeserializer.java b/datetime/src/main/java/com/fasterxml/jackson/datatype/jsr310/deser/LocalDateTimeDeserializer.java index c76fdb0d..de0a1c69 100644 --- a/datetime/src/main/java/com/fasterxml/jackson/datatype/jsr310/deser/LocalDateTimeDeserializer.java +++ b/datetime/src/main/java/com/fasterxml/jackson/datatype/jsr310/deser/LocalDateTimeDeserializer.java @@ -210,7 +210,7 @@ public LocalDateTime deserialize(JsonParser p, DeserializationContext ctxt) thro t); } if (p.hasToken(JsonToken.VALUE_EMBEDDED_OBJECT)) { - return (LocalDateTime) p.getEmbeddedObject(); + return _fromEmbedded(p, ctxt); } if (p.hasToken(JsonToken.VALUE_NUMBER_INT)) { _throwNoNumericTimestampNeedTimeZone(p, ctxt); diff --git a/datetime/src/main/java/com/fasterxml/jackson/datatype/jsr310/deser/LocalTimeDeserializer.java b/datetime/src/main/java/com/fasterxml/jackson/datatype/jsr310/deser/LocalTimeDeserializer.java index f023be9a..3aa697da 100644 --- a/datetime/src/main/java/com/fasterxml/jackson/datatype/jsr310/deser/LocalTimeDeserializer.java +++ b/datetime/src/main/java/com/fasterxml/jackson/datatype/jsr310/deser/LocalTimeDeserializer.java @@ -164,7 +164,7 @@ public LocalTime deserialize(JsonParser p, DeserializationContext ctxt) throws I t); } if (p.hasToken(JsonToken.VALUE_EMBEDDED_OBJECT)) { - return (LocalTime) p.getEmbeddedObject(); + return _fromEmbedded(p, ctxt); } if (p.hasToken(JsonToken.VALUE_NUMBER_INT)) { _throwNoNumericTimestampNeedTimeZone(p, ctxt); diff --git a/datetime/src/main/java/com/fasterxml/jackson/datatype/jsr310/deser/MonthDayDeserializer.java b/datetime/src/main/java/com/fasterxml/jackson/datatype/jsr310/deser/MonthDayDeserializer.java index 88d71697..7ace4c0b 100644 --- a/datetime/src/main/java/com/fasterxml/jackson/datatype/jsr310/deser/MonthDayDeserializer.java +++ b/datetime/src/main/java/com/fasterxml/jackson/datatype/jsr310/deser/MonthDayDeserializer.java @@ -105,7 +105,7 @@ public MonthDay deserialize(JsonParser p, DeserializationContext ctxt) throws IO } return MonthDay.of(month, day); } else if (p.hasToken(JsonToken.VALUE_EMBEDDED_OBJECT)) { - return (MonthDay) p.getEmbeddedObject(); + return _fromEmbedded(p, ctxt); } return _handleUnexpectedToken(ctxt, p, JsonToken.VALUE_STRING, JsonToken.START_ARRAY); diff --git a/datetime/src/main/java/com/fasterxml/jackson/datatype/jsr310/deser/OffsetTimeDeserializer.java b/datetime/src/main/java/com/fasterxml/jackson/datatype/jsr310/deser/OffsetTimeDeserializer.java index 0a23ac39..d214ed6b 100644 --- a/datetime/src/main/java/com/fasterxml/jackson/datatype/jsr310/deser/OffsetTimeDeserializer.java +++ b/datetime/src/main/java/com/fasterxml/jackson/datatype/jsr310/deser/OffsetTimeDeserializer.java @@ -115,7 +115,7 @@ public OffsetTime deserialize(JsonParser p, DeserializationContext ctxt) throws } if (!p.isExpectedStartArrayToken()) { if (p.hasToken(JsonToken.VALUE_EMBEDDED_OBJECT)) { - return (OffsetTime) p.getEmbeddedObject(); + return _fromEmbedded(p, ctxt); } if (p.hasToken(JsonToken.VALUE_NUMBER_INT)) { _throwNoNumericTimestampNeedTimeZone(p, ctxt); diff --git a/datetime/src/main/java/com/fasterxml/jackson/datatype/jsr310/deser/YearDeserializer.java b/datetime/src/main/java/com/fasterxml/jackson/datatype/jsr310/deser/YearDeserializer.java index 7e6c3603..787a8379 100644 --- a/datetime/src/main/java/com/fasterxml/jackson/datatype/jsr310/deser/YearDeserializer.java +++ b/datetime/src/main/java/com/fasterxml/jackson/datatype/jsr310/deser/YearDeserializer.java @@ -96,7 +96,7 @@ public Year deserialize(JsonParser p, DeserializationContext ctxt) throws IOExce } else if (t == JsonToken.VALUE_NUMBER_INT) { return _fromNumber(ctxt, p.getIntValue()); } else if (t == JsonToken.VALUE_EMBEDDED_OBJECT) { - return (Year) p.getEmbeddedObject(); + return _fromEmbedded(p, ctxt); } else if (p.isExpectedStartArrayToken()){ return _deserializeFromArray(p, ctxt); } diff --git a/datetime/src/main/java/com/fasterxml/jackson/datatype/jsr310/deser/YearMonthDeserializer.java b/datetime/src/main/java/com/fasterxml/jackson/datatype/jsr310/deser/YearMonthDeserializer.java index 4114343e..0a35ea51 100644 --- a/datetime/src/main/java/com/fasterxml/jackson/datatype/jsr310/deser/YearMonthDeserializer.java +++ b/datetime/src/main/java/com/fasterxml/jackson/datatype/jsr310/deser/YearMonthDeserializer.java @@ -124,7 +124,7 @@ public YearMonth deserialize(JsonParser p, DeserializationContext ctxt) throws I } return YearMonth.of(year, month); } else if (p.hasToken(JsonToken.VALUE_EMBEDDED_OBJECT)) { - return (YearMonth) p.getEmbeddedObject(); + return _fromEmbedded(p, ctxt); } return _handleUnexpectedToken(ctxt, p, JsonToken.VALUE_STRING, JsonToken.START_ARRAY); diff --git a/datetime/src/test/java/com/fasterxml/jackson/datatype/jsr310/deser/EmbeddedObjectDeser389Test.java b/datetime/src/test/java/com/fasterxml/jackson/datatype/jsr310/deser/EmbeddedObjectDeser389Test.java new file mode 100644 index 00000000..19bec7e4 --- /dev/null +++ b/datetime/src/test/java/com/fasterxml/jackson/datatype/jsr310/deser/EmbeddedObjectDeser389Test.java @@ -0,0 +1,131 @@ +package com.fasterxml.jackson.datatype.jsr310.deser; + +import java.io.IOException; +import java.time.Duration; +import java.time.Instant; +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.LocalTime; +import java.time.MonthDay; +import java.time.OffsetDateTime; +import java.time.OffsetTime; +import java.time.Period; +import java.time.Year; +import java.time.YearMonth; +import java.time.ZoneId; +import java.time.ZoneOffset; +import java.time.ZonedDateTime; +import java.util.Map; +import java.util.stream.Stream; + +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; + +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.databind.JavaType; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.exc.MismatchedInputException; +import com.fasterxml.jackson.databind.util.TokenBuffer; +import com.fasterxml.jackson.datatype.jsr310.ModuleTestBase; + +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertSame; +import static org.junit.jupiter.api.Assertions.assertThrows; + +/** + * Tests for [modules-java8#389]: "embedded object" values (as exposed by binary + * formats like CBOR/Smile, and by token buffering) must be verified to be + * compatible with the requested type; otherwise generic containers (like + * {@code Map}) would silently end up with wrong-typed values, + * due to type erasure. + */ +public class EmbeddedObjectDeser389Test extends ModuleTestBase +{ + private final ObjectMapper MAPPER = newMapper(); + + static Stream supportedTypes() + { + return Stream.of( + // Types handled by `JSR310StringParsableDeserializer` + Arguments.of(Period.class, Period.ofDays(1), Duration.ofSeconds(1)), + Arguments.of(ZoneId.class, ZoneId.of("Europe/Helsinki"), Period.ofDays(1)), + // NOTE: `ZoneOffset` IS-A `ZoneId`, but not vice versa + Arguments.of(ZoneOffset.class, ZoneOffset.ofHours(2), ZoneId.of("Europe/Helsinki")), + + // Types handled by `InstantDeserializer`: note that all are `Temporal`s + // so an unchecked cast would NOT catch these + Arguments.of(Instant.class, Instant.ofEpochSecond(1), LocalDate.of(2020, 1, 1)), + Arguments.of(OffsetDateTime.class, + OffsetDateTime.parse("2020-01-01T00:00:00+02:00"), Instant.ofEpochSecond(1)), + Arguments.of(ZonedDateTime.class, + ZonedDateTime.parse("2020-01-01T00:00:00+02:00"), Instant.ofEpochSecond(1)), + + // ... and the rest of `java.time` types with embedded-object support + Arguments.of(Duration.class, Duration.ofSeconds(1), Period.ofDays(1)), + Arguments.of(LocalDate.class, LocalDate.of(2020, 1, 1), Instant.ofEpochSecond(1)), + Arguments.of(LocalDateTime.class, + LocalDateTime.of(2020, 1, 1, 12, 0), LocalDate.of(2020, 1, 1)), + Arguments.of(LocalTime.class, LocalTime.of(12, 0), LocalDate.of(2020, 1, 1)), + Arguments.of(OffsetTime.class, + OffsetTime.parse("12:00:00+02:00"), LocalTime.of(12, 0)), + Arguments.of(Year.class, Year.of(2020), YearMonth.of(2020, 1)), + Arguments.of(YearMonth.class, YearMonth.of(2020, 1), Year.of(2020)), + Arguments.of(MonthDay.class, MonthDay.of(1, 1), YearMonth.of(2020, 1)) + ); + } + + // [modules-java8#389] + @ParameterizedTest + @MethodSource("supportedTypes") + public void testAcceptsCompatibleEmbeddedObject(Class targetType, + Object validValue, Object incompatibleValue) throws Exception + { + assertSame(validValue, readEmbeddedValue(targetType, validValue)); + } + + // [modules-java8#389] + @ParameterizedTest + @MethodSource("supportedTypes") + public void testRejectsIncompatibleEmbeddedObject(Class targetType, + Object validValue, Object incompatibleValue) throws Exception + { + // First: a value of completely unrelated type (as per #389, a CBOR byte string) + MismatchedInputException e = assertThrows(MismatchedInputException.class, + () -> readEmbeddedValue(targetType, new byte[] { 1, 2, 3 })); + verifyException(e, targetType.getName()); + + // Second: a "near miss", that is, another `java.time` value; these would + // NOT be caught by an unchecked/erased cast + e = assertThrows(MismatchedInputException.class, + () -> readEmbeddedValue(targetType, incompatibleValue)); + verifyException(e, targetType.getName()); + } + + // [modules-java8#389] + @ParameterizedTest + @MethodSource("supportedTypes") + public void testAllowsNullEmbeddedObject(Class targetType, + Object validValue, Object incompatibleValue) throws Exception + { + assertNull(readEmbeddedValue(targetType, null)); + } + + private Object readEmbeddedValue(Class targetType, Object embeddedValue) + throws IOException + { + try (TokenBuffer buffer = new TokenBuffer(MAPPER, false)) { + buffer.writeStartObject(); + buffer.writeFieldName("value"); + buffer.writeEmbeddedObject(embeddedValue); + buffer.writeEndObject(); + + JavaType mapType = MAPPER.getTypeFactory() + .constructMapType(Map.class, String.class, targetType); + try (JsonParser parser = buffer.asParser()) { + Map result = MAPPER.readValue(parser, mapType); + return result.get("value"); + } + } + } +} diff --git a/datetime/src/test/java/com/fasterxml/jackson/datatype/jsr310/deser/JSR310StringParsableDeserializerTest.java b/datetime/src/test/java/com/fasterxml/jackson/datatype/jsr310/deser/JSR310StringParsableDeserializerTest.java deleted file mode 100644 index 2826de32..00000000 --- a/datetime/src/test/java/com/fasterxml/jackson/datatype/jsr310/deser/JSR310StringParsableDeserializerTest.java +++ /dev/null @@ -1,75 +0,0 @@ -package com.fasterxml.jackson.datatype.jsr310.deser; - -import java.io.IOException; -import java.time.Period; -import java.time.ZoneId; -import java.time.ZoneOffset; -import java.util.Map; -import java.util.stream.Stream; - -import org.junit.jupiter.api.Test; -import org.junit.jupiter.params.ParameterizedTest; -import org.junit.jupiter.params.provider.Arguments; -import org.junit.jupiter.params.provider.MethodSource; - -import com.fasterxml.jackson.core.JsonParser; -import com.fasterxml.jackson.databind.JavaType; -import com.fasterxml.jackson.databind.ObjectMapper; -import com.fasterxml.jackson.databind.exc.MismatchedInputException; -import com.fasterxml.jackson.databind.util.TokenBuffer; -import com.fasterxml.jackson.datatype.jsr310.ModuleTestBase; - -import static org.junit.jupiter.api.Assertions.assertNull; -import static org.junit.jupiter.api.Assertions.assertSame; -import static org.junit.jupiter.api.Assertions.assertThrows; - -public class JSR310StringParsableDeserializerTest extends ModuleTestBase -{ - private final ObjectMapper MAPPER = newMapper(); - - static Stream supportedTypes() - { - return Stream.of( - Arguments.of(Period.class, Period.ofDays(1)), - Arguments.of(ZoneId.class, ZoneId.of("Europe/Helsinki")), - Arguments.of(ZoneOffset.class, ZoneOffset.ofHours(2)) - ); - } - - // [modules-java8#389] - @ParameterizedTest - @MethodSource("supportedTypes") - public void testRejectsIncompatibleEmbeddedObject(Class targetType, - Object validValue) throws Exception - { - assertSame(validValue, readEmbeddedValue(targetType, validValue)); - - MismatchedInputException e = assertThrows(MismatchedInputException.class, - () -> readEmbeddedValue(targetType, new byte[] { 1, 2, 3 })); - verifyException(e, targetType.getName()); - } - - @Test - public void testAllowsNullEmbeddedObject() throws Exception - { - assertNull(readEmbeddedValue(ZoneId.class, null)); - } - - private Object readEmbeddedValue(Class targetType, Object embeddedValue) - throws IOException - { - try (TokenBuffer buffer = new TokenBuffer(MAPPER, false)) { - buffer.writeStartObject(); - buffer.writeFieldName("value"); - buffer.writeEmbeddedObject(embeddedValue); - buffer.writeEndObject(); - - JavaType mapType = MAPPER.getTypeFactory() - .constructMapType(Map.class, String.class, targetType); - try (JsonParser parser = buffer.asParser()) { - Map result = MAPPER.readValue(parser, mapType); - return result.get("value"); - } - } - } -} diff --git a/release-notes/CREDITS-2.x b/release-notes/CREDITS-2.x index c8693d31..7742a8c1 100644 --- a/release-notes/CREDITS-2.x +++ b/release-notes/CREDITS-2.x @@ -243,3 +243,13 @@ Seonwoo Jung (@seonwooj0810) * Contributed fix for #76: Missing milliseconds, when serializing Java 8 date-time, if they are zeros (2.23.0) + +Manqing Zhou (@manqingzhou) + * Reported #389: Validate embedded objects (`JsonToken.VALUE_EMBEDDED_OBJECT`) + against expected `java.time` type, instead of returning them as-is + (2.23.0) + +Lux Liang (@lux-liang) + * Contributed fix for #389: Validate embedded objects (`JsonToken.VALUE_EMBEDDED_OBJECT`) + against expected `java.time` type, instead of returning them as-is + (2.23.0) diff --git a/release-notes/VERSION-2.x b/release-notes/VERSION-2.x index 3b6eec96..29948ce1 100644 --- a/release-notes/VERSION-2.x +++ b/release-notes/VERSION-2.x @@ -14,7 +14,8 @@ Modules: `JavaTimeFeature.ALWAYS_WRITE_SUBSECOND_DIGITS`) (reported by @rycler) (fix contributed by Seonwoo J) -#389: Validate embedded objects for `ZoneId`, `Period` and `ZoneOffset` +#389: Validate embedded objects (`JsonToken.VALUE_EMBEDDED_OBJECT`) against + expected `java.time` type, instead of returning them as-is (reported by @manqingzhou) (fix contributed by @lux-liang)