-
Notifications
You must be signed in to change notification settings - Fork 117
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
274: add MonthDeserializer and JavaTimeFeature option (#292)
- Loading branch information
1 parent
f7780a4
commit d8d0f5c
Showing
10 changed files
with
423 additions
and
3 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
30 changes: 30 additions & 0 deletions
30
...c/main/java/com/fasterxml/jackson/datatype/jsr310/deser/JavaTimeDeserializerModifier.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,30 @@ | ||
package com.fasterxml.jackson.datatype.jsr310.deser; | ||
|
||
import java.time.Month; | ||
|
||
import com.fasterxml.jackson.databind.BeanDescription; | ||
import com.fasterxml.jackson.databind.DeserializationConfig; | ||
import com.fasterxml.jackson.databind.JavaType; | ||
import com.fasterxml.jackson.databind.JsonDeserializer; | ||
import com.fasterxml.jackson.databind.deser.BeanDeserializerModifier; | ||
|
||
/** | ||
* @since 2.17 | ||
*/ | ||
public class JavaTimeDeserializerModifier extends BeanDeserializerModifier { | ||
private static final long serialVersionUID = 1L; | ||
|
||
private final boolean _oneBaseMonths; | ||
|
||
public JavaTimeDeserializerModifier(boolean oneBaseMonths) { | ||
_oneBaseMonths = oneBaseMonths; | ||
} | ||
|
||
@Override | ||
public JsonDeserializer<?> modifyEnumDeserializer(DeserializationConfig config, JavaType type, BeanDescription beanDesc, JsonDeserializer<?> defaultDeserializer) { | ||
if (_oneBaseMonths && type.hasRawClass(Month.class)) { | ||
return new OneBasedMonthDeserializer(defaultDeserializer); | ||
} | ||
return defaultDeserializer; | ||
} | ||
} |
51 changes: 51 additions & 0 deletions
51
.../src/main/java/com/fasterxml/jackson/datatype/jsr310/deser/OneBasedMonthDeserializer.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,51 @@ | ||
package com.fasterxml.jackson.datatype.jsr310.deser; | ||
|
||
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; | ||
import com.fasterxml.jackson.databind.DeserializationContext; | ||
import com.fasterxml.jackson.databind.JsonDeserializer; | ||
import com.fasterxml.jackson.databind.deser.std.DelegatingDeserializer; | ||
import com.fasterxml.jackson.databind.exc.InvalidFormatException; | ||
|
||
/** | ||
* @since 2.17 | ||
*/ | ||
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); | ||
} | ||
return zeroBaseMonth.minus(1); | ||
} | ||
|
||
private boolean _isNumericValue(String text, JsonToken token) { | ||
return token == JsonToken.VALUE_NUMBER_INT || _isNumberAsString(text, token); | ||
} | ||
|
||
private boolean _isNumberAsString(String text, JsonToken token) { | ||
return token == JsonToken.VALUE_STRING && HAS_ONE_OR_TWO_DIGITS.matcher(text).matches(); | ||
} | ||
|
||
@Override | ||
protected JsonDeserializer<?> newDelegatingInstance(JsonDeserializer<?> newDelegatee) { | ||
return new OneBasedMonthDeserializer(newDelegatee); | ||
} | ||
} |
30 changes: 30 additions & 0 deletions
30
...e/src/main/java/com/fasterxml/jackson/datatype/jsr310/ser/JavaTimeSerializerModifier.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,30 @@ | ||
package com.fasterxml.jackson.datatype.jsr310.ser; | ||
|
||
import java.time.Month; | ||
|
||
import com.fasterxml.jackson.databind.BeanDescription; | ||
import com.fasterxml.jackson.databind.JavaType; | ||
import com.fasterxml.jackson.databind.JsonSerializer; | ||
import com.fasterxml.jackson.databind.SerializationConfig; | ||
import com.fasterxml.jackson.databind.ser.BeanSerializerModifier; | ||
|
||
/** | ||
* @since 2.17 | ||
*/ | ||
public class JavaTimeSerializerModifier extends BeanSerializerModifier { | ||
private static final long serialVersionUID = 1L; | ||
|
||
private final boolean _oneBaseMonths; | ||
|
||
public JavaTimeSerializerModifier(boolean oneBaseMonths) { | ||
_oneBaseMonths = oneBaseMonths; | ||
} | ||
|
||
@Override | ||
public JsonSerializer<?> modifyEnumSerializer(SerializationConfig config, JavaType valueType, BeanDescription beanDesc, JsonSerializer<?> serializer) { | ||
if (_oneBaseMonths && valueType.hasRawClass(Month.class)) { | ||
return new OneBasedMonthSerializer(serializer); | ||
} | ||
return serializer; | ||
} | ||
} |
36 changes: 36 additions & 0 deletions
36
...time/src/main/java/com/fasterxml/jackson/datatype/jsr310/ser/OneBasedMonthSerializer.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,36 @@ | ||
package com.fasterxml.jackson.datatype.jsr310.ser; | ||
|
||
import java.io.IOException; | ||
import java.time.Month; | ||
|
||
import com.fasterxml.jackson.core.JsonGenerator; | ||
import com.fasterxml.jackson.databind.JsonSerializer; | ||
import com.fasterxml.jackson.databind.SerializationFeature; | ||
import com.fasterxml.jackson.databind.SerializerProvider; | ||
|
||
/** | ||
* @since 2.17 | ||
*/ | ||
public class OneBasedMonthSerializer extends JsonSerializer<Month> { | ||
private final JsonSerializer<Object> _defaultSerializer; | ||
|
||
@SuppressWarnings("unchecked") | ||
public OneBasedMonthSerializer(JsonSerializer<?> defaultSerializer) | ||
{ | ||
_defaultSerializer = (JsonSerializer<Object>) defaultSerializer; | ||
} | ||
|
||
@Override | ||
public void serialize(Month value, JsonGenerator gen, SerializerProvider ctxt) | ||
throws IOException | ||
{ | ||
// 15-Jan-2024, tatu: [modules-java8#274] This is not really sufficient | ||
// (see `jackson-databind` `EnumSerializer` for full logic), but has to | ||
// do for now. May need to add `@JsonFormat.shape` handling in future. | ||
if (ctxt.isEnabled(SerializationFeature.WRITE_ENUMS_USING_INDEX)) { | ||
gen.writeNumber(value.ordinal() + 1); | ||
return; | ||
} | ||
_defaultSerializer.serialize(value, gen, ctxt); | ||
} | ||
} |
185 changes: 185 additions & 0 deletions
185
...ime/src/test/java/com/fasterxml/jackson/datatype/jsr310/deser/OneBasedMonthDeserTest.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,185 @@ | ||
package com.fasterxml.jackson.datatype.jsr310.deser; | ||
|
||
import java.time.Month; | ||
import java.time.temporal.TemporalAccessor; | ||
|
||
import org.junit.Test; | ||
import org.junit.function.ThrowingRunnable; | ||
|
||
import com.fasterxml.jackson.databind.ObjectMapper; | ||
import com.fasterxml.jackson.databind.ObjectReader; | ||
import com.fasterxml.jackson.databind.cfg.CoercionAction; | ||
import com.fasterxml.jackson.databind.cfg.CoercionInputShape; | ||
import com.fasterxml.jackson.databind.exc.InvalidFormatException; | ||
import com.fasterxml.jackson.databind.exc.MismatchedInputException; | ||
import com.fasterxml.jackson.databind.json.JsonMapper; | ||
import com.fasterxml.jackson.datatype.jsr310.JavaTimeFeature; | ||
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; | ||
import com.fasterxml.jackson.datatype.jsr310.MockObjectConfiguration; | ||
import com.fasterxml.jackson.datatype.jsr310.ModuleTestBase; | ||
|
||
import static org.junit.Assert.*; | ||
|
||
public class OneBasedMonthDeserTest extends ModuleTestBase | ||
{ | ||
static class Wrapper { | ||
public Month value; | ||
|
||
public Wrapper(Month v) { value = v; } | ||
public Wrapper() { } | ||
} | ||
|
||
@Test | ||
public void testDeserializationAsString01_oneBased() throws Exception | ||
{ | ||
assertEquals(Month.JANUARY, readerForOneBased().readValue("\"01\"")); | ||
} | ||
|
||
@Test | ||
public void testDeserializationAsString01_zeroBased() throws Exception | ||
{ | ||
assertEquals(Month.FEBRUARY, readerForZeroBased().readValue("\"01\"")); | ||
} | ||
|
||
|
||
@Test | ||
public void testDeserializationAsString02_oneBased() throws Exception | ||
{ | ||
assertEquals(Month.JANUARY, readerForOneBased().readValue("\"JANUARY\"")); | ||
} | ||
|
||
@Test | ||
public void testDeserializationAsString02_zeroBased() throws Exception | ||
{ | ||
assertEquals(Month.JANUARY, readerForZeroBased().readValue("\"JANUARY\"")); | ||
} | ||
|
||
@Test | ||
public void testBadDeserializationAsString01_oneBased() { | ||
assertError( | ||
() -> readerForOneBased().readValue("\"notamonth\""), | ||
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]" | ||
); | ||
} | ||
|
||
static void assertError(ThrowingRunnable codeToRun, Class<? extends Throwable> expectedException, String expectedMessage) { | ||
try { | ||
codeToRun.run(); | ||
fail(String.format("Expecting %s, but nothing was thrown!", expectedException.getName())); | ||
} catch (Throwable actualException) { | ||
if (!expectedException.isInstance(actualException)) { | ||
fail(String.format("Expecting exception of type %s, but %s was thrown instead", expectedException.getName(), actualException.getClass().getName())); | ||
} | ||
if (actualException.getMessage() == null || !actualException.getMessage().contains(expectedMessage)) { | ||
fail(String.format("Expecting exception with message containing:'%s', but the actual error message was:'%s'", expectedMessage, actualException.getMessage())); | ||
} | ||
} | ||
} | ||
|
||
|
||
@Test | ||
public void testDeserialization01_zeroBased() throws Exception | ||
{ | ||
assertEquals(Month.FEBRUARY, readerForZeroBased().readValue("1")); | ||
} | ||
|
||
@Test | ||
public void testDeserialization01_oneBased() throws Exception | ||
{ | ||
assertEquals(Month.JANUARY, readerForOneBased().readValue("1")); | ||
} | ||
|
||
@Test | ||
public void testDeserialization02_zeroBased() throws Exception | ||
{ | ||
assertEquals(Month.SEPTEMBER, readerForZeroBased().readValue("\"08\"")); | ||
} | ||
|
||
@Test | ||
public void testDeserialization02_oneBased() throws Exception | ||
{ | ||
assertEquals(Month.AUGUST, readerForOneBased().readValue("\"08\"")); | ||
} | ||
|
||
@Test | ||
public void testDeserializationWithTypeInfo01_oneBased() throws Exception | ||
{ | ||
ObjectMapper MAPPER = new ObjectMapper() | ||
.registerModule(new JavaTimeModule().enable(JavaTimeFeature.ONE_BASED_MONTHS)); | ||
MAPPER.addMixIn(TemporalAccessor.class, MockObjectConfiguration.class); | ||
|
||
TemporalAccessor value = MAPPER.readValue("[\"java.time.Month\",11]", TemporalAccessor.class); | ||
assertEquals(Month.NOVEMBER, value); | ||
} | ||
|
||
@Test | ||
public void testDeserializationWithTypeInfo01_zeroBased() throws Exception | ||
{ | ||
ObjectMapper MAPPER = new ObjectMapper(); | ||
MAPPER.addMixIn(TemporalAccessor.class, MockObjectConfiguration.class); | ||
|
||
TemporalAccessor value = MAPPER.readValue("[\"java.time.Month\",\"11\"]", TemporalAccessor.class); | ||
assertEquals(Month.DECEMBER, value); | ||
} | ||
|
||
@Test | ||
public void testFormatAnnotation_zeroBased() throws Exception | ||
{ | ||
Wrapper output = readerForZeroBased().readValue("{\"value\":\"11\"}", Wrapper.class); | ||
assertEquals(new Wrapper(Month.DECEMBER).value, output.value); | ||
} | ||
|
||
@Test | ||
public void testFormatAnnotation_oneBased() throws Exception | ||
{ | ||
Wrapper output = readerForOneBased().readValue("{\"value\":\"11\"}", Wrapper.class); | ||
assertEquals(new Wrapper(Month.NOVEMBER).value, output.value); | ||
} | ||
|
||
/* | ||
/********************************************************** | ||
/* Tests for empty string handling | ||
/********************************************************** | ||
*/ | ||
|
||
@Test | ||
public void testDeserializeFromEmptyString() throws Exception | ||
{ | ||
final ObjectMapper mapper = newMapper(); | ||
|
||
// Nulls are handled in general way, not by deserializer so they are ok | ||
Month m = mapper.readerFor(Month.class).readValue(" null "); | ||
assertNull(m); | ||
|
||
// But coercion from empty String not enabled for Enums by default: | ||
try { | ||
mapper.readerFor(Month.class).readValue("\"\""); | ||
fail("Should not pass"); | ||
} catch (MismatchedInputException e) { | ||
verifyException(e, "Cannot coerce empty String"); | ||
} | ||
// But can allow coercion of empty String to, say, null | ||
ObjectMapper emptyStringMapper = mapperBuilder() | ||
.withCoercionConfig(Month.class, | ||
h -> h.setCoercion(CoercionInputShape.EmptyString, CoercionAction.AsNull)) | ||
.build(); | ||
m = emptyStringMapper.readerFor(Month.class).readValue("\"\""); | ||
assertNull(m); | ||
} | ||
|
||
private ObjectReader readerForZeroBased() { | ||
return JsonMapper.builder() | ||
.addModule(new JavaTimeModule() | ||
.disable(JavaTimeFeature.ONE_BASED_MONTHS)) | ||
.build() | ||
.readerFor(Month.class); | ||
} | ||
|
||
private ObjectReader readerForOneBased() { | ||
return JsonMapper.builder() | ||
.addModule(new JavaTimeModule().enable(JavaTimeFeature.ONE_BASED_MONTHS)) | ||
.build() | ||
.readerFor(Month.class); | ||
} | ||
} |
Oops, something went wrong.