Skip to content

@JsonEnumDefaultValue should take precedence over FAIL_ON_NUMBERS_FOR_ENUMS #1505

Closed
@Stephan202

Description

@Stephan202

Consider the following ObjectMapper definition:

ObjectMapper m =
        new ObjectMapper()
                .enable(DeserializationFeature.FAIL_ON_NUMBERS_FOR_ENUMS)
                .enable(DeserializationFeature.READ_UNKNOWN_ENUM_VALUES_USING_DEFAULT_VALUE);

With this ObjectMapper, when one attempts to deserialize an enum value V for an enum with an @JsonEnumDefaultValue element, the deserialization will:

  • Pass, if V is a valid enum value. ✅
  • Pass, if V is an invalid non-integer value. ✅
  • Fail, if V is an invalid integer value. ❌

To me this seems highly unintuitive. I would have expected the READ_UNKNOWN_ENUM_VALUES_USING_DEFAULT_VALUE feature to take precedence over the FAIL_ON_NUMBERS_FOR_ENUMS feature in those cases where it applies (i.e., when deserializing an enum with a default element).

I've put together a test class enumerating the relevant cases. See the inline comments towards the bottom. In four cases I feel that Jackson's current behavior is not as one might expect.

import static com.fasterxml.jackson.databind.DeserializationFeature.FAIL_ON_NUMBERS_FOR_ENUMS;
import static com.fasterxml.jackson.databind.DeserializationFeature.READ_UNKNOWN_ENUM_VALUES_USING_DEFAULT_VALUE;
import static org.testng.Assert.assertEquals;
import static org.testng.Assert.fail;

import com.fasterxml.jackson.annotation.JsonEnumDefaultValue;
import com.fasterxml.jackson.annotation.JsonValue;
import com.fasterxml.jackson.databind.ObjectMapper;
import javax.annotation.Nullable;
import org.testng.annotations.Test;

public final class JacksonEnumTest {
    @Test
    public void testWithoutCustomFeatures() {
        final ObjectMapper m = new ObjectMapper();

        verifyDeserialization(m, "ZERO", SimpleEnum.class, SimpleEnum.ZERO);
        verifyDeserialization(m, "ONE", SimpleEnum.class, SimpleEnum.ONE);
        verifyDeserialization(m, "TWO", SimpleEnum.class, null);
        verifyDeserialization(m, "0", SimpleEnum.class, SimpleEnum.ZERO);
        verifyDeserialization(m, "1", SimpleEnum.class, SimpleEnum.ONE);
        verifyDeserialization(m, "2", SimpleEnum.class, null);

        verifyDeserialization(m, "ZERO", SimpleEnumWithDefault.class, SimpleEnumWithDefault.ZERO);
        verifyDeserialization(m, "ONE", SimpleEnumWithDefault.class, SimpleEnumWithDefault.ONE);
        verifyDeserialization(m, "TWO", SimpleEnumWithDefault.class, null);
        verifyDeserialization(m, "0", SimpleEnumWithDefault.class, SimpleEnumWithDefault.ZERO);
        verifyDeserialization(m, "1", SimpleEnumWithDefault.class, SimpleEnumWithDefault.ONE);
        verifyDeserialization(m, "2", SimpleEnumWithDefault.class, null);

        verifyDeserialization(m, "ZERO", CustomEnum.class, null);
        verifyDeserialization(m, "ONE", CustomEnum.class, null);
        verifyDeserialization(m, "TWO", CustomEnum.class, null);
        verifyDeserialization(m, "0", CustomEnum.class, CustomEnum.ZERO);
        verifyDeserialization(m, "1", CustomEnum.class, CustomEnum.ONE);
        verifyDeserialization(m, "2", CustomEnum.class, null);

        verifyDeserialization(m, "ZERO", CustomEnumWithDefault.class, null);
        verifyDeserialization(m, "ONE", CustomEnumWithDefault.class, null);
        verifyDeserialization(m, "TWO", CustomEnumWithDefault.class, null);
        verifyDeserialization(m, "0", CustomEnumWithDefault.class, CustomEnumWithDefault.ZERO);
        verifyDeserialization(m, "1", CustomEnumWithDefault.class, CustomEnumWithDefault.ONE);
        verifyDeserialization(m, "2", CustomEnumWithDefault.class, null);
    }

    @Test
    public void testWithFailOnNumbersForEnums() {
        final ObjectMapper m = new ObjectMapper().enable(FAIL_ON_NUMBERS_FOR_ENUMS);

        verifyDeserialization(m, "ZERO", SimpleEnum.class, SimpleEnum.ZERO);
        verifyDeserialization(m, "ONE", SimpleEnum.class, SimpleEnum.ONE);
        verifyDeserialization(m, "TWO", SimpleEnum.class, null);
        verifyDeserialization(m, "0", SimpleEnum.class, null);
        verifyDeserialization(m, "1", SimpleEnum.class, null);
        verifyDeserialization(m, "2", SimpleEnum.class, null);

        verifyDeserialization(m, "ZERO", SimpleEnumWithDefault.class, SimpleEnumWithDefault.ZERO);
        verifyDeserialization(m, "ONE", SimpleEnumWithDefault.class, SimpleEnumWithDefault.ONE);
        verifyDeserialization(m, "TWO", SimpleEnumWithDefault.class, null);
        verifyDeserialization(m, "0", SimpleEnumWithDefault.class, null);
        verifyDeserialization(m, "1", SimpleEnumWithDefault.class, null);
        verifyDeserialization(m, "2", SimpleEnumWithDefault.class, null);

        verifyDeserialization(m, "ZERO", CustomEnum.class, null);
        verifyDeserialization(m, "ONE", CustomEnum.class, null);
        verifyDeserialization(m, "TWO", CustomEnum.class, null);
        verifyDeserialization(m, "0", CustomEnum.class, CustomEnum.ZERO);
        verifyDeserialization(m, "1", CustomEnum.class, CustomEnum.ONE);
        verifyDeserialization(m, "2", CustomEnum.class, null);

        verifyDeserialization(m, "ZERO", CustomEnumWithDefault.class, null);
        verifyDeserialization(m, "ONE", CustomEnumWithDefault.class, null);
        verifyDeserialization(m, "TWO", CustomEnumWithDefault.class, null);
        verifyDeserialization(m, "0", CustomEnumWithDefault.class, CustomEnumWithDefault.ZERO);
        verifyDeserialization(m, "1", CustomEnumWithDefault.class, CustomEnumWithDefault.ONE);
        verifyDeserialization(m, "2", CustomEnumWithDefault.class, null);
    }

    @Test
    public void testWithReadUnknownEnumValuesUsingDefaultValue() {
        final ObjectMapper m =
                new ObjectMapper().enable(READ_UNKNOWN_ENUM_VALUES_USING_DEFAULT_VALUE);

        verifyDeserialization(m, "ZERO", SimpleEnum.class, SimpleEnum.ZERO);
        verifyDeserialization(m, "ONE", SimpleEnum.class, SimpleEnum.ONE);
        verifyDeserialization(m, "TWO", SimpleEnum.class, null);
        verifyDeserialization(m, "0", SimpleEnum.class, SimpleEnum.ZERO);
        verifyDeserialization(m, "1", SimpleEnum.class, SimpleEnum.ONE);
        verifyDeserialization(m, "2", SimpleEnum.class, null);

        verifyDeserialization(m, "ZERO", SimpleEnumWithDefault.class, SimpleEnumWithDefault.ZERO);
        verifyDeserialization(m, "ONE", SimpleEnumWithDefault.class, SimpleEnumWithDefault.ONE);
        verifyDeserialization(m, "TWO", SimpleEnumWithDefault.class, SimpleEnumWithDefault.ZERO);
        verifyDeserialization(m, "0", SimpleEnumWithDefault.class, SimpleEnumWithDefault.ZERO);
        verifyDeserialization(m, "1", SimpleEnumWithDefault.class, SimpleEnumWithDefault.ONE);
        verifyDeserialization(m, "2", SimpleEnumWithDefault.class, SimpleEnumWithDefault.ZERO);

        verifyDeserialization(m, "ZERO", CustomEnum.class, null);
        verifyDeserialization(m, "ONE", CustomEnum.class, null);
        verifyDeserialization(m, "TWO", CustomEnum.class, null);
        verifyDeserialization(m, "0", CustomEnum.class, CustomEnum.ZERO);
        verifyDeserialization(m, "1", CustomEnum.class, CustomEnum.ONE);
        verifyDeserialization(m, "2", CustomEnum.class, null);

        verifyDeserialization(m, "ZERO", CustomEnumWithDefault.class, CustomEnumWithDefault.ZERO);
        verifyDeserialization(m, "ONE", CustomEnumWithDefault.class, CustomEnumWithDefault.ZERO);
        verifyDeserialization(m, "TWO", CustomEnumWithDefault.class, CustomEnumWithDefault.ZERO);
        verifyDeserialization(m, "0", CustomEnumWithDefault.class, CustomEnumWithDefault.ZERO);
        verifyDeserialization(m, "1", CustomEnumWithDefault.class, CustomEnumWithDefault.ONE);
        verifyDeserialization(m, "2", CustomEnumWithDefault.class, CustomEnumWithDefault.ZERO);
    }

    @Test
    public void testWithFailOnNumbersForEnumsAndReadUnknownEnumValuesUsingDefaultValue() {
ObjectMapper m = new ObjectMapper()
		                .enable(FAIL_ON_NUMBERS_FOR_ENUMS)
		                .enable(READ_UNKNOWN_ENUM_VALUES_USING_DEFAULT_VALUE);

        verifyDeserialization(m, "ZERO", SimpleEnum.class, SimpleEnum.ZERO);
        verifyDeserialization(m, "ONE", SimpleEnum.class, SimpleEnum.ONE);
        verifyDeserialization(m, "TWO", SimpleEnum.class, null);
        verifyDeserialization(m, "0", SimpleEnum.class, null);
        verifyDeserialization(m, "1", SimpleEnum.class, null);
        verifyDeserialization(m, "2", SimpleEnum.class, null);

        verifyDeserialization(m, "ZERO", SimpleEnumWithDefault.class, SimpleEnumWithDefault.ZERO);
        verifyDeserialization(m, "ONE", SimpleEnumWithDefault.class, SimpleEnumWithDefault.ONE);
        verifyDeserialization(m, "TWO", SimpleEnumWithDefault.class, SimpleEnumWithDefault.ZERO);
        // The three tests below fail; Jackson throws an exception on the basis that
        // "FAIL_ON_NUMBERS_FOR_ENUMS" is enabled. I claim the default value should be returned instead.
        verifyDeserialization(m, "0", SimpleEnumWithDefault.class, SimpleEnumWithDefault.ZERO);
        verifyDeserialization(m, "1", SimpleEnumWithDefault.class, SimpleEnumWithDefault.ZERO);
        verifyDeserialization(m, "2", SimpleEnumWithDefault.class, SimpleEnumWithDefault.ZERO);

        verifyDeserialization(m, "ZERO", CustomEnum.class, null);
        verifyDeserialization(m, "ONE", CustomEnum.class, null);
        verifyDeserialization(m, "TWO", CustomEnum.class, null);
        verifyDeserialization(m, "0", CustomEnum.class, CustomEnum.ZERO);
        verifyDeserialization(m, "1", CustomEnum.class, CustomEnum.ONE);
        verifyDeserialization(m, "2", CustomEnum.class, null);

        verifyDeserialization(m, "ZERO", CustomEnumWithDefault.class, CustomEnumWithDefault.ZERO);
        verifyDeserialization(m, "ONE", CustomEnumWithDefault.class, CustomEnumWithDefault.ZERO);
        verifyDeserialization(m, "TWO", CustomEnumWithDefault.class, CustomEnumWithDefault.ZERO);
        verifyDeserialization(m, "0", CustomEnumWithDefault.class, CustomEnumWithDefault.ZERO);
        verifyDeserialization(m, "1", CustomEnumWithDefault.class, CustomEnumWithDefault.ONE);
        // Fails. Jackson throws an exception on the basis that "FAIL_ON_NUMBERS_FOR_ENUMS"
        // is enabled, but the default value should have been returned instead.
        verifyDeserialization(m, "2", CustomEnumWithDefault.class, CustomEnumWithDefault.ZERO);
    }

    private <T> void verifyDeserialization(
            final ObjectMapper objectMapper,
            final String fromValue,
            final Class<T> toValueType,
            @Nullable final T toValue) {
        if (toValue != null) {
            assertEquals(objectMapper.convertValue(fromValue, toValueType), toValue);
        } else {
            try {
                objectMapper.convertValue(fromValue, toValueType);
                fail("Deserialization should have failed");
            } catch (final IllegalArgumentException e) {
                /* Expected. */
            }
        }
    }

    enum SimpleEnum {
        ZERO,
        ONE;
    }

    enum SimpleEnumWithDefault {
        @JsonEnumDefaultValue
        ZERO,
        ONE;
    }

    enum CustomEnum {
        ZERO(0),
        ONE(1);

        private final int number;

        CustomEnum(final int number) {
            this.number = number;
        }

        @JsonValue
        int getNumber() {
            return this.number;
        }
    }

    enum CustomEnumWithDefault {
        @JsonEnumDefaultValue
        ZERO(0),
        ONE(1);

        private final int number;

        CustomEnumWithDefault(final int number) {
            this.number = number;
        }

        @JsonValue
        int getNumber() {
            return this.number;
        }
    }
}

Would love to hear your thoughts on this. (If there is some other feature I should enable to get the behavior I'm looking for, let me know :).)

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions