In Jackson 2.12, you could define a simple @JsonCreator method that takes a String, which would get called even if the source json was an object - looks like always with a null value. In 2.16+, this fails with Cannot deserialize value of type `RuleType` from Object value (token `JsonToken.START_OBJECT

It looks like this can be fixed by add @JsonProperty on the @JsonCreator method. However, adding the @JsonProperty in Jackson 2.12 results in Jackson requiring the source json to be an object.

I have concerns with the change in expectations on source json being either an object or String depending on whether @JsonProperty exists. Specifically, that in newer versions of Jackson, including 2.18 ,the existence of @JsonProperty enables support for both strings and object. While this makes it easy to preserve the existing behavior, it seems like it could lead to similar challenges in the future if there is ever a desire to make handling of the different input json formats more explicit. It is also very weird to have to add @JsonProperty to read a property that should never exist. At the same time, the conversion of the object to null in prior versions was also odd behavior.

Although more interested in options to make enums forward and backward compatible with 2.12 and 2.18+ without having to create a custom deserializer? I have tried the following all with the same error:

mapper.coercionConfigFor(Enum.class)
      .setCoercion(CoercionInputShape.Object, CoercionAction.AsNull);
// and .setCoercion(CoercionInputShape.Object, CoercionAction.TryConvert);
mapper.enable(DeserializationFeature.READ_ENUMS_USING_TO_STRING);
  public enum RuleType {
        ONE,
        TWO,
        UNKNOWN;

        @JsonCreator
        public static RuleType fromValue(String value) {
       //  2.18  public static RuleType fromValue(@JsonProperty("doesntreallymatter") String value) {
            try {
                return RuleType.valueOf(value);
            } catch (Exception e) {
                return RuleType.UNKNOWN;
            }
        }
    }

   private ObjectMapper mapper = new ObjectMapper();


    @Test
    public void testObject() throws JsonMappingException, JsonProcessingException {
        Assert.assertEquals(RuleType.UNKNOWN, mapper.readValue("{\"key\": \"value\"}", RuleType.class));
    }

    @Test
    public void testEmpty() throws JsonMappingException, JsonProcessingException {
        Assert.assertEquals(RuleType.UNKNOWN, mapper.readValue("\"\"", RuleType.class));
    }

    @Test
    public void testHappy() throws JsonMappingException, JsonProcessingException {
        Assert.assertEquals(RuleType.ONE, mapper.readValue("\"ONE\"", RuleType.class));  
    }

Comment From: cowtowncoder

One quick note: wrt @JsonCreator, existence of @JsonProperty should always imply Object value as matching input. It only really matters for 1-parameter case, but that should be hard rule -- only Objects have Properties.

Beyond that, I do not really know what "backwards-compatible" specifically means.

Comment From: seadbrane

should always imply Object value as matching input. It only really matters for 1-parameter case, but that should be hard rule -- only Objects have Properties.

That makes sense and appears to be the behavior in 2.12 - but not in 2.18. In 2.18, ideally would like to leave the @JsonCreator method as is (no @JsonProperty) and set something else to make objects be converted to a default value - such as mapper.enable(DeserializationFeature.READ_UNKNOWN_ENUM_VALUES_USING_DEFAULT_VALUE);.

Beyond that, I do not really know what "backwards-compatible" specifically means.

For context, we are upgrading a large code base from 2.12 to 2.18+ and as a general rule it is easier to make the code forward compatible with new version - as in, make changes that work with current version but will also work when upgraded.
So in this case, it looks like I could do that by creating a custom deserializer that handles both objects and non-object input, but now this is now additional code that someone will have to maintain - the alternative would be to make 2 round of changes 1) forward compatible 2) rip out forward compatibility layer and replace with 2.18 supported code.

Comment From: cowtowncoder

One thing to note: unfortunately Enum support for @JsonCreator is much less advanced/mature than that for regular POJOs. So it definitely has raw edges.

So whereas with POJOs you definitely could annotate 2 separate Creators (factory methods since Enum constructors are not good), not sure that actually works with Enums; guessing it doesn't.

If so, it might be best to just have one like:

  @JsonCreator(mode = JsonCreator.Mode.PROPERTIES)
  public static RuleType fromValue(JsonNode json) {
    // extract from JSON Object or String as appropriate
  }

and if serialization customization needed, could use @JsonValue to indicate getter or field to use as serialization.

Hope this helps.

Comment From: seadbrane

Other than the behavior being more consistent between 2.12 and 2.18, @JsonCreator(mode = JsonCreator.Mode.PROPERTIES) doesn't really work.
Without @JsonProperty all input options fail with no property based creator. With @JsonProperty, the non object input values are not supported.

Comment From: cowtowncoder

Ugh. Sorry about that @seadbrane . What I MEANT to suggest is:

  @JsonCreator(mode = JsonCreator.Mode.DELEGATING)
  public static RuleType fromValue(JsonNode json) {
    // extract from JSON Object or String as appropriate
  }

and NOT PROPERTIES.

Comment From: seadbrane

Thanks. This looks like it will work. I thought I had tried a @JsonCreator that took JsonNode as it doesn't look like I even need to add mode = JsonCreator.Mode.DELEGATING. Although, based on the javadocs - it does look like I do want to use it here.

full enum sample that looks to be sufficient for the current code in case others run into this.

public enum RuleType {
        ONE,
        TWO,
        UNKNOWN;

        @JsonCreator(mode = JsonCreator.Mode.DELEGATING)
        public static RuleType fromJson(JsonNode value) {
              return fromValue(value == null ? null : value.asText());
        }

        public static RuleType fromValue(String value) {
            try {
                return RuleType.valueOf(value);
            } catch (Exception e) {
                return RuleType.UNKNOWN;
            }
        }
    }
}

Comment From: cowtowncoder

Right, use of mode = JsonCreator.Mode.DELEGATING is often not required; it's just that occasionally heuristic gets it wrong (thinks there is named property). Although if I recall correctly, use of @JsonValue is taken as hint to use DELEGATING (so if that was used, no need to add mode).

Comment From: seadbrane

I have another enum use case where the behavior has changed. In this case, the enum is serialized as an object with type information and there is a creator with @JsonProperty.

Works fine in 2.12, but in 2.18 with this input I get the following , which seems odd given that I thought @JsonProperty implies object, and it is an object with that field.

{"_type":"EnumObjectTest$RuleType","ruleName":"one"}

com.fasterxml.jackson.databind.exc.MismatchedInputException: Input mismatch reading Enum `EnumObjectTest$RuleName`: properties-based `@JsonCreator` ([method EnumObjectTest$RuleName#fromJson(com.fasterxml.jackson.databind.JsonNode)]) expects Object Value, got Object value (`JsonToken.FIELD_NAME`)
 at [Source: REDACTED (`StreamReadFeature.INCLUDE_SOURCE_IN_LOCATION` disabled); line: 1, column: 36]
    at com.fasterxml.jackson.databind.exc.MismatchedInputException.from(MismatchedInputException.java:59)

Sample code:

public class EnumObjectTest {

    @JsonFormat(shape = JsonFormat.Shape.OBJECT)
    @JsonTypeInfo(use = JsonTypeInfo.Id.CLASS, include = JsonTypeInfo.As.PROPERTY,
    property = "_type")
    public enum RuleName {
        ONE("one"),
        TWO("two"),
        UNKNOWN("unknown");

        private final String ruleName;
        RuleName(String ruleName) {
            this.ruleName = ruleName;
        }

        public String getRuleName() {
            return ruleName;
        }
         /** 
        Can be fixed with this and commenting out the @JsonCreator below. 
        @JsonCreator(mode = JsonCreator.Mode.DELEGATING)
        public static RuleName fromJson(JsonNode json) {
            JsonNode ruleName = json.get("ruleName");
            if (ruleName == null) {
                ruleName = json;
            }
            return fromRuleName(ruleName.asText());
        }
        */

        @JsonCreator
        public static RuleName fromRuleName(@JsonProperty("ruleName") String ruleName) {
            try {
                return EnumSet.allOf(RuleName.class).stream().filter(e -> e.ruleName.equals(ruleName)).findFirst().orElse(null);
            } catch (Exception e) {
                return RuleName.UNKNOWN;
            }
        }
    }

    private ObjectMapper mapper =  new ObjectMapper();


    @Test
    public void testObject() throws JsonMappingException, JsonProcessingException {
        String value = mapper.writeValueAsString(RuleName.ONE);
        Assert.assertEquals(RuleName.ONE, mapper.readValue(value, RuleName.class));
    }
}

Comment From: JooHyukKim

May I ask how far you would go to avoid writing a custom deserializer? Your enum configuration is becoming quite complex already and difficult to maintain from both user and library perspective. If I were you I would just go ahead and write a custom deserializer

Comment From: seadbrane

It wasn't originally complicated - the "fromJson" method did not exist - that is just an example of how it could now be fixed, which is considerably simpler option than writing a custom custom deserializer.
The concern is that behavior has changed in what seems to be very subtle ways between versions. And per earlier discussions, @JsonProperty should always imply object, so why can it no longer be deserialized from an object.

Comment From: cowtowncoder

FWTW, part of the problem really is that handling of Enums is -- code-wise -- very different from POJOs/Beans (since they are "lightweight", non-instantiatable values; but also due to historical reasons). And specifically support for @JsonCreator has been included only for small subset of possible cases.

Same is true wrt polymorphism: since Enums themselves cannot be polymorphic originally thinking was no support was really needed. ... except when over time realizing that of course Enums can be polymorphic subtypes of something more generic (by implementing interfaces, or for java.lang.Object base type).

This is just giving context of why things are hard with Enums, not to argue they shouldn't work better.

But going back to the original ask, this:

define a simple @JsonCreator method that takes a String, which would get called even if the source json was an object - looks like always with a null value.

is not going to be changed: no logic for POJOs does this, and it is probably worked that way by accident.

Also: I don't understand use of @JsonTypeInfo in this particular case -- is the test showing actual usage approximation? It is not needed here, as far as I can see, and complicates handling in all kinds of ways. So if not needed, that should just be removed and type property either ignored, or handled as "regular" property.

Comment From: seadbrane

is not going to be changed: no logic for POJOs does this, and it is probably worked that way by accident.

yes, makes sense. Workarond using JsonNode and Mode.DELEGATING is sufficient to solve that case.

This other case is related but only needs to support object representation - hence the inclusion of @JsonProperty. As for @JsonTypeInfo - it is included in the existing code and there is likely data that exists with the serialized typing. Perhaps that can be ignored, perhaps not - although would certainly prefer to maintain the existing behavior. Or at least understand why the behavior has changed, since unlike the case above - support for this before doesn't seem like it should have just worked by accident.

Comment From: cowtowncoder

@seadbrane on @JsonTypeInfo, I would say that if that annotation is on Enum, it is wrong and should be removed -- but if it was on an interface Enum implements, it might be used for specific purpose.

I say this because realistically there is no way it can make much sense: Enum types are not polymorphic, and since target type of that Enum class is required on ObjectMapper.readValue() there's nothing to be gained. Nothing but addition of type id on serialization, which has no use on deserialization. Or at least I cannot fabricate any case in my mind where it'd make sense.

I realize this might be from legacy code base, but I have seen fair bit of speculative use for @JsonTypeInfo, used sort of "just in case" style. And this has that same feel. But ultimately it's of course up to you. I would just get rid of that if it was my codebase. :) (which is not).