Skip to content

Commit

Permalink
274: add MonthDeserializer and JavaTimeFeature option (#292)
Browse files Browse the repository at this point in the history
  • Loading branch information
etrandafir93 authored Jan 16, 2024
1 parent f7780a4 commit d8d0f5c
Show file tree
Hide file tree
Showing 10 changed files with 423 additions and 3 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,16 @@ public enum JavaTimeFeature implements JacksonFeature
* stringified numbers are always accepted as timestamps regardless of
* this feature.
*/
ALWAYS_ALLOW_STRINGIFIED_DATE_TIMESTAMPS(false)
ALWAYS_ALLOW_STRINGIFIED_DATE_TIMESTAMPS(false),

/**
* Feature that determines whether {@link java.time.Month} is serialized
* and deserialized as using a zero-based index (FALSE) or a one-based index (TRUE).
* For example, "1" would be serialized/deserialized as Month.JANUARY if TRUE and Month.FEBRUARY if FALSE.
*<p>
* Default setting is false, meaning that Month is serialized/deserialized as a zero-based index.
*/
ONE_BASED_MONTHS(false)
;

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -101,7 +101,6 @@ public final class JavaTimeModule
public JavaTimeModule()
{
super(PackageVersion.VERSION);

_features = JacksonFeatureSet.fromDefaults(JavaTimeFeature.values());
}

Expand Down Expand Up @@ -142,6 +141,11 @@ public void setupModule(SetupContext context) {
desers.addDeserializer(ZoneOffset.class, JSR310StringParsableDeserializer.ZONE_OFFSET);

context.addDeserializers(desers);

final boolean oneBasedMonthEnabled = _features.isEnabled(JavaTimeFeature.ONE_BASED_MONTHS);

context.addBeanDeserializerModifier(new JavaTimeDeserializerModifier(oneBasedMonthEnabled));
context.addBeanSerializerModifier(new JavaTimeSerializerModifier(oneBasedMonthEnabled));
// 20-Nov-2023, tatu: [modules-java8#288]: someone may have directly
// added entries, need to add for backwards compatibility
if (_deserializers != null) {
Expand Down
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;
}
}
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);
}
}
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;
}
}
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);
}
}
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);
}
}
Loading

0 comments on commit d8d0f5c

Please sign in to comment.