Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -344,7 +344,6 @@ protected JSR310DateTimeDeserializerBase<?> _withFormatOverrides(Deserialization
return deser;
}

@SuppressWarnings("unchecked")
@Override
public T deserialize(JsonParser parser, DeserializationContext context) throws IOException
{
Expand All @@ -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);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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.
*<p>
* Since there is no guarantee that the embedded value actually is of the
* expected type -- and since generic containers (like
* {@code Map<String,Instant>}) 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> BOGUS _reportWrongToken(DeserializationContext context,
JsonToken exp, String unit) throws IOException
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -129,9 +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
return p.getEmbeddedObject();
return _fromEmbedded(p, ctxt);
} else if (p.isExpectedStartArrayToken()) {
return _deserializeFromArray(p, ctxt);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
Original file line number Diff line number Diff line change
@@ -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<String,ZoneId>}) would silently end up with wrong-typed values,
* due to type erasure.
*/
public class EmbeddedObjectDeser389Test extends ModuleTestBase
{
private final ObjectMapper MAPPER = newMapper();

static Stream<Arguments> 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");
}
}
}
}
10 changes: 10 additions & 0 deletions release-notes/CREDITS-2.x
Original file line number Diff line number Diff line change
Expand Up @@ -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)
4 changes: 4 additions & 0 deletions release-notes/VERSION-2.x
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,10 @@ Modules:
`JavaTimeFeature.ALWAYS_WRITE_SUBSECOND_DIGITS`)
(reported by @rycler)
(fix contributed by Seonwoo J)
#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)

2.22.3 (21-Sep-2026)
2.22.2 (16-Aug-2026)
Expand Down