Skip to content
Merged
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
2 changes: 2 additions & 0 deletions release-notes/CREDITS
Original file line number Diff line number Diff line change
Expand Up @@ -341,6 +341,8 @@ Omkhar Arasaratnam (@omkhar)
[3.1.4]
* Reported, suggested fix for #5956: Fix problem with float-to-byte range check
[3.1.4]
* Contributed fix for #5957: Improve `java.time.Month` deserialization validation
[3.1.4]

Michael Orzechowski (@MikeBlink)
* Reported #5941: `MapperBuilder.addModule()` does not recursively register transitive
Expand Down
2 changes: 2 additions & 0 deletions release-notes/VERSION
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ No changes since 3.1
(reported, fix suggested by Omkhar A)
#5956: Fix problem with float-to-byte range check
(reported, fix suggested by Omkhar A)
#5957: Improve `java.time.Month` deserialization validation
(fix by Omkhar A)

3.1.3 (01-May-2026)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@

import tools.jackson.core.*;
import tools.jackson.databind.DeserializationContext;
import tools.jackson.databind.DeserializationFeature;
import tools.jackson.databind.cfg.DateTimeFeature;

import com.fasterxml.jackson.annotation.JsonFormat;
Expand Down Expand Up @@ -79,31 +78,9 @@ public Month deserialize(JsonParser p, DeserializationContext ctxt)
}
// fall through
} else if (p.isExpectedStartArrayToken()) {
JsonToken t = p.nextToken();
if (t == JsonToken.END_ARRAY) {
return null;
}
if ((t == JsonToken.VALUE_STRING || t == JsonToken.VALUE_EMBEDDED_OBJECT)
&& ctxt.isEnabled(DeserializationFeature.UNWRAP_SINGLE_VALUE_ARRAYS)) {
final Month parsed = deserialize(p, ctxt);
if (p.nextToken() != JsonToken.END_ARRAY) {
handleMissingEndArrayForSingle(p, ctxt);
}
return parsed;
}
if (t != JsonToken.VALUE_NUMBER_INT) {
return _reportWrongToken(ctxt, JsonToken.VALUE_NUMBER_INT, Integer.class.getName());
}
int month = p.getIntValue();
if (p.nextToken() != JsonToken.END_ARRAY) {
throw ctxt.wrongTokenException(p, handledType(), JsonToken.END_ARRAY,
"Expected array to end");
}
if (Month.JANUARY.getValue() <= month && month <= Month.DECEMBER.getValue()) {
return Month.of(month);
}
return (Month) ctxt.handleWeirdNumberValue(handledType(),
month, "month number outside 1-12 range for 1-based `Month`s");
// [databind#5957]: Delegate to standard array handling so empty arrays
// and single-element unwrapping respect coercion / UNWRAP_SINGLE_VALUE_ARRAYS.
return _deserializeFromArray(p, ctxt);
} else if (p.hasToken(JsonToken.VALUE_EMBEDDED_OBJECT)) {
return (Month) p.getEmbeddedObject();
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -91,14 +91,14 @@ public Year deserialize(JsonParser p, DeserializationContext ctxt) throws Jackso
return (Year) p.getEmbeddedObject();
}
// 30-Sep-2020, tatu: New! "Scalar from Object" (mostly for XML)
if (t == JsonToken.START_OBJECT) {
if (p.isExpectedStartObjectToken()) {
final String str = ctxt.extractScalarFromObject(p, this, handledType());
// 17-May-2025, tatu: [databind#4656] need to check for `null`
if (str != null) {
return _fromString(p, ctxt, str);
}
// fall through
} else if (p.hasToken(JsonToken.START_ARRAY)){
} else if (p.isExpectedStartArrayToken()){
return _deserializeFromArray(p, ctxt);
}
return _handleUnexpectedToken(ctxt, p, JsonToken.VALUE_STRING, JsonToken.VALUE_NUMBER_INT);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -261,78 +261,106 @@ public void testDeserializationAsIntOutOfRange_oneBased(int invalidValue) throws
@Test
public void testDeserializationAsEmptyArray() throws Exception
{
// Empty array returns null
Month result = readerForOneBased().readValue("[]");
// [databind#5957]: Empty array now requires ACCEPT_EMPTY_ARRAY_AS_NULL_OBJECT to return null
assertError(
() -> readerForOneBased().readValue("[]"),
MismatchedInputException.class,
"Cannot deserialize"
);
}

@Test
public void testDeserializationAsEmptyArray_withFeatureEnabled() throws Exception
{
ObjectMapper mapper = newMapper().rebuild()
.enable(DeserializationFeature.ACCEPT_EMPTY_ARRAY_AS_NULL_OBJECT).build();
Month result = mapper.readerFor(Month.class).readValue("[]");
assertNull(result);
}

@Test
public void testDeserializationAsArrayWithIntValue() throws Exception
{
// Array with single int value (interpreted as 1-based month)
Month result = readerForOneBased().readValue("[3]");
// [databind#5957]: Single-element int array now requires UNWRAP_SINGLE_VALUE_ARRAYS
assertError(
() -> readerForOneBased().readValue("[3]"),
MismatchedInputException.class,
"from Array value"
);
}

@Test
public void testDeserializationAsArrayWithIntValue_withFeatureEnabled() throws Exception
{
ObjectReader r = readerForOneBased()
.with(DeserializationFeature.UNWRAP_SINGLE_VALUE_ARRAYS);
Month result = r.readValue("[3]");
assertEquals(Month.MARCH, result);
}

@Test
public void testDeserializationAsArrayWithIntValue_zeroBased() throws Exception
{
// Array with single int value (0-based mode still uses Month.of for array)
Month result = readerForZeroBased().readValue("[3]");
assertEquals(Month.MARCH, result);
// [databind#5957]: Single-element int array now requires UNWRAP_SINGLE_VALUE_ARRAYS
assertError(
() -> readerForZeroBased().readValue("[3]"),
MismatchedInputException.class,
"from Array value"
);
}

@Test
public void testDeserializationAsArrayWithMoreThanOneElement() throws Exception
{
// [databind#5957]: Multi-element array (no UNWRAP) rejected as Array-token mismatch
assertError(
() -> readerForOneBased().readValue("[1, 2]"),
MismatchedInputException.class,
"Expected array to end"
"from Array value"
);
}

@Test
public void testDeserializationAsArrayWithWrongToken() throws Exception
{
// Boolean in array without UNWRAP should fail with specific error
// [databind#5957]: Boolean in array (no UNWRAP) rejected as Array-token mismatch
assertError(
() -> readerForOneBased().readValue("[true]"),
MismatchedInputException.class,
"Expected VALUE_NUMBER_INT"
"from Array value"
);
}

@Test
public void testDeserializationAsArrayWithStringUnwrapDisabled() throws Exception
{
// String in array without UNWRAP_SINGLE_VALUE_ARRAYS should fail
// [databind#5957]: String in array without UNWRAP_SINGLE_VALUE_ARRAYS should fail
assertError(
() -> readerForOneBased().readValue("[\"JANUARY\"]"),
MismatchedInputException.class,
"Expected VALUE_NUMBER_INT"
"from Array value"
);
}

@Test
public void testDeserializationAsArrayWithFloatUnwrapDisabled() throws Exception
{
// Float in array without UNWRAP should fail
// [databind#5957]: Float in array (no UNWRAP) rejected as Array-token mismatch
assertError(
() -> readerForOneBased().readValue("[1.5]"),
MismatchedInputException.class,
"Expected VALUE_NUMBER_INT"
"from Array value"
);
}

@Test
public void testDeserializationAsArrayWithObjectUnwrapDisabled() throws Exception
{
// Object in array without UNWRAP should fail
// [databind#5957]: Object in array (no UNWRAP) rejected as Array-token mismatch
assertError(
() -> readerForOneBased().readValue("[{}]"),
MismatchedInputException.class,
"Expected VALUE_NUMBER_INT"
"from Array value"
);
}

Expand Down
Loading