Skip to content

Fix #364: make one-based Month deserializer accept "12" #365

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 2 commits into from
Mar 30, 2025
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
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@

import java.io.IOException;
import java.time.Month;
import java.util.regex.Pattern;

import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.core.JsonToken;
Expand All @@ -17,31 +16,57 @@
public class OneBasedMonthDeserializer extends DelegatingDeserializer {
private static final long serialVersionUID = 1L;

private static final Pattern HAS_ONE_OR_TWO_DIGITS = Pattern.compile("^\\d{1,2}$");

public OneBasedMonthDeserializer(JsonDeserializer<?> defaultDeserializer) {
super(defaultDeserializer);
}

@Override
public Object deserialize(JsonParser parser, DeserializationContext context) throws IOException {
JsonToken token = parser.currentToken();
Month zeroBaseMonth = (Month) getDelegatee().deserialize(parser, context);
if (!_isNumericValue(parser.getText(), token)) {
return zeroBaseMonth;
}
if (zeroBaseMonth == Month.JANUARY) {
throw new InvalidFormatException(parser, "Month.JANUARY value not allowed for 1-based Month.", zeroBaseMonth, Month.class);
switch (token) {
case VALUE_NUMBER_INT:
return _decodeMonth(parser.getIntValue(), parser);
case VALUE_STRING:
String monthSpec = parser.getText();
int oneBasedMonthNumber = _decodeNumber(monthSpec);
if (oneBasedMonthNumber >= 0) {
return _decodeMonth(oneBasedMonthNumber, parser);
}
// Otherwise fall through to default handling
break;
}
return zeroBaseMonth.minus(1);
return getDelegatee().deserialize(parser, context);
}

private boolean _isNumericValue(String text, JsonToken token) {
return token == JsonToken.VALUE_NUMBER_INT || _isNumberAsString(text, token);
/**
* @return Numeric value of input text that represents a 1-digit or 2-digit number.
* Negative value in other cases (empty string, not a number, 3 or more digits).
*/
private int _decodeNumber(String text) {
int numValue;
switch (text.length()) {
case 1:
char c = text.charAt(0);
boolean cValid = ('0' <= c && c <= '9');
numValue = cValid ? (c - '0') : -1;
break;
case 2:
char c1 = text.charAt(0);
char c2 = text.charAt(1);
boolean c12valid = ('0' <= c1 && c1 <= '9' && '0' <= c2 && c2 <= '9');
numValue = c12valid ? (10 * (c1 - '0') + (c2 - '0')) : -1;
break;
default:
numValue = -1;
}
return numValue;
}

private boolean _isNumberAsString(String text, JsonToken token) {
return token == JsonToken.VALUE_STRING && HAS_ONE_OR_TWO_DIGITS.matcher(text).matches();
private Month _decodeMonth(int oneBasedMonthNumber, JsonParser parser) throws InvalidFormatException {
if (Month.JANUARY.getValue() <= oneBasedMonthNumber && oneBasedMonthNumber <= Month.DECEMBER.getValue()) {
return Month.of(oneBasedMonthNumber);
}
throw new InvalidFormatException(parser, "Month number " + oneBasedMonthNumber + " not allowed for 1-based Month.", oneBasedMonthNumber, Integer.class);
}

@Override
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,9 @@
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
import com.fasterxml.jackson.datatype.jsr310.MockObjectConfiguration;
import com.fasterxml.jackson.datatype.jsr310.ModuleTestBase;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.CsvSource;
import org.junit.jupiter.params.provider.EnumSource;

import static org.junit.jupiter.api.Assertions.*;

Expand All @@ -29,37 +32,51 @@ static class Wrapper {
public Wrapper() { }
}

@Test
public void testDeserializationAsString01_oneBased() throws Exception
@ParameterizedTest
@EnumSource(Month.class)
public void testDeserializationAsString01_oneBased(Month expectedMonth) throws Exception
{
assertEquals(Month.JANUARY, readerForOneBased().readValue("\"1\""));
int monthNum = expectedMonth.getValue();
assertEquals(expectedMonth, readerForOneBased().readValue("\"" + monthNum + '"'));
}

@Test
public void testDeserializationAsString01_zeroBased() throws Exception
@ParameterizedTest
@EnumSource(Month.class)
public void testDeserializationAsString01_zeroBased(Month expectedMonth) throws Exception
{
assertEquals(Month.FEBRUARY, readerForZeroBased().readValue("\"1\""));
int monthNum = expectedMonth.ordinal();
assertEquals(expectedMonth, readerForZeroBased().readValue("\"" + monthNum + '"'));
}


@Test
public void testDeserializationAsString02_oneBased() throws Exception
@ParameterizedTest
@EnumSource(Month.class)
public void testDeserializationAsString02_oneBased(Month month) throws Exception
{
assertEquals(Month.JANUARY, readerForOneBased().readValue("\"JANUARY\""));
assertEquals(month, readerForOneBased().readValue("\"" + month.name() + '"'));
}

@Test
public void testDeserializationAsString02_zeroBased() throws Exception
@ParameterizedTest
@EnumSource(Month.class)
public void testDeserializationAsString02_zeroBased(Month month) throws Exception
{
assertEquals(Month.JANUARY, readerForZeroBased().readValue("\"JANUARY\""));
}

@Test
public void testBadDeserializationAsString01_oneBased() {
assertEquals(month, readerForOneBased().readValue("\"" + month.name() + '"'));
}

@ParameterizedTest
@CsvSource({
"notamonth , 'Cannot deserialize value of type `java.time.Month` from String \"notamonth\": not one of the values accepted for Enum class: [OCTOBER, SEPTEMBER, JUNE, MARCH, MAY, APRIL, JULY, JANUARY, FEBRUARY, DECEMBER, AUGUST, NOVEMBER]'",
"JANUAR , 'Cannot deserialize value of type `java.time.Month` from String \"JANUAR\": not one of the values accepted for Enum class: [OCTOBER, SEPTEMBER, JUNE, MARCH, MAY, APRIL, JULY, JANUARY, FEBRUARY, DECEMBER, AUGUST, NOVEMBER]'",
"march , 'Cannot deserialize value of type `java.time.Month` from String \"march\": not one of the values accepted for Enum class: [OCTOBER, SEPTEMBER, JUNE, MARCH, MAY, APRIL, JULY, JANUARY, FEBRUARY, DECEMBER, AUGUST, NOVEMBER]'",
"0 , 'Month number 0 not allowed for 1-based Month.'",
"13 , 'Month number 13 not allowed for 1-based Month.'",
})
public void testBadDeserializationAsString01_oneBased(String monthSpec, String expectedMessage) {
String value = "\"" + monthSpec + '"';
assertError(
() -> readerForOneBased().readValue("\"notamonth\""),
() -> readerForOneBased().readValue(value),
InvalidFormatException.class,
"Cannot deserialize value of type `java.time.Month` from String \"notamonth\": not one of the values accepted for Enum class: [OCTOBER, SEPTEMBER, JUNE, MARCH, MAY, APRIL, JULY, JANUARY, FEBRUARY, DECEMBER, AUGUST, NOVEMBER]"
expectedMessage
);
}

Expand Down