FasterXML / jackson-modules-java8

Set of support modules for Java 8 datatypes (Optionals, date/time) and features (parameter names)
Apache License 2.0
399 stars 116 forks source link

Deserialize enum with @JsonCreator failed: argument type mismatch #234

Closed Brozen closed 2 years ago

Brozen commented 2 years ago

version: 2.10.4 JDK: AdoptOpenJDK (HotSpot) 11.0.9 OS: macOS Monterey CPU: 2GHz 4C Interl Core i5

I'm serializing and deserializing enums with @JsonValu and @JsonCreator. The code works in a long term, but after I register ParameterNamesModule, I got a exception when deserializing(serializing is OK), stack trace likes:

com.fasterxml.jackson.databind.exc.ValueInstantiationException: Cannot construct instance of `ParameterNameIgnoreTest$MeetingStatus`, problem: argument type mismatch
 at [Source: (String)"{"id":1000,"status":2}"; line: 1, column: 21] (through reference chain: ParameterNameIgnoreTest$Meeting["status"])
    at com.fasterxml.jackson.databind.exc.ValueInstantiationException.from(ValueInstantiationException.java:47)
    at com.fasterxml.jackson.databind.DeserializationContext.instantiationException(DeserializationContext.java:1735)
    at com.fasterxml.jackson.databind.DeserializationContext.handleInstantiationProblem(DeserializationContext.java:1109)
    at com.fasterxml.jackson.databind.deser.std.FactoryBasedEnumDeserializer.deserialize(FactoryBasedEnumDeserializer.java:146)
    at com.fasterxml.jackson.databind.deser.impl.MethodProperty.deserializeAndSet(MethodProperty.java:129)
    at com.fasterxml.jackson.databind.deser.BeanDeserializer.vanillaDeserialize(BeanDeserializer.java:288)
    at com.fasterxml.jackson.databind.deser.BeanDeserializer.deserialize(BeanDeserializer.java:151)
    at com.fasterxml.jackson.databind.ObjectMapper._readMapAndClose(ObjectMapper.java:4218)
    at com.fasterxml.jackson.databind.ObjectMapper.readValue(ObjectMapper.java:3214)
    at com.fasterxml.jackson.databind.ObjectMapper.readValue(ObjectMapper.java:3182)
    at ParameterNameIgnoreTest.test(ParameterNameIgnoreTest.java:113)
         ...... 
Caused by: java.lang.IllegalArgumentException: argument type mismatch
    at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
    at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
    at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
    at java.base/java.lang.reflect.Method.invoke(Method.java:566)
    at com.fasterxml.jackson.databind.introspect.AnnotatedMethod.callOnWith(AnnotatedMethod.java:122)
    at com.fasterxml.jackson.databind.deser.std.FactoryBasedEnumDeserializer.deserialize(FactoryBasedEnumDeserializer.java:138)
    ... 31 more

Here is my test case:


    enum MeetingStatus {

        UNKNOWN(0),

        INITIALIZING(1),
        PROGRESSING(2),
        TERMINATED(3),
        ;

        public final int status;

        @JsonValue
        public int getStatus() {
            return status;
        }

        MeetingStatus(int status) {
            this.status = status;
        }

        @JsonCreator
        public static MeetingStatus parse(Number status) {
            if (status == null) {
                return UNKNOWN;
            }

            int s = status.intValue();
            for (MeetingStatus value : values()) {
                if (s == value.status) {
                    return value;
                }
            }

            return UNKNOWN;
        }

    }

    public static class Meeting {
        private MeetingStatus status;

        public MeetingStatus getStatus() {
            return status;
        }

        public void setStatus(MeetingStatus status) {
            this.status = status;
        }
    }

    @Test
    public void test() throws IOException {
        ObjectMapper mapper = new ObjectMapper();
        mapper.registerModule(new ParameterNamesModule());

        // it's ok when serializing
        Meeting meeting = new Meeting();
        meeting.setStatus(MeetingStatus.PROGRESSING);

        StringWriter writer = new StringWriter();
        mapper.writeValue(writer, meeting);

        String json = writer.toString();
        System.out.println(json);

        // but throws exception when deserializing
        meeting = mapper.readValue(json, Meeting.class);
        System.out.println(meeting);
    }

Maybe the reason is ParameterNamesAnnotationIntrospector has parsed parameter name of MeetingStatus.parse(Number), and Jackson thinks MeetingStats has a _creatorProps in class FactoryBasedEnumDeserializer, so _deser field in FactoryBasedEnumDeserializer is not setted (line 92 at FactoryBasedEnumDeserializer).

    @Override
    public JsonDeserializer<?> createContextual(DeserializationContext ctxt,
            BeanProperty property)
        throws JsonMappingException
    {
        // line 92
        if ((_deser == null) && (_inputType != null) && (_creatorProps == null)) {
            return new FactoryBasedEnumDeserializer(this,
                    ctxt.findContextualValueDeserializer(_inputType, property));
        }
        return this;
    }

And then, when deserialize enum value, due to _deser is null and p.isExpectedStartObjectToken() return false, the value treated as String, and I got the exception above.

        // from com.fasterxml.jackson.databind.deser.std.FactoryBasedEnumDeserializer#deserialize
        Object value = null;
        if (_deser != null) {
            value = _deser.deserialize(p, ctxt);
        } else if (_hasArgs) {
            JsonToken curr = p.currentToken();
            //There can be a JSON object passed for deserializing an Enum,
            //the below case handles it.
            if (curr == JsonToken.VALUE_STRING || curr == JsonToken.FIELD_NAME) {
                value = p.getText();
            } else if ((_creatorProps != null) && p.isExpectedStartObjectToken()) {
                if (_propCreator == null) {
                    _propCreator = PropertyBasedCreator.construct(ctxt, _valueInstantiator, _creatorProps,
                            ctxt.isEnabled(MapperFeature.ACCEPT_CASE_INSENSITIVE_PROPERTIES));
                }
                p.nextToken();
                return deserializeEnumUsingPropertyBased(p, ctxt, _propCreator);
            } else {
                // here, got a string value
                value = p.getValueAsString();
            }
        }

To resolve this problem, I must modify parameter type of MeetingStatus.parse(Number) method and accept a String parameter, just like this:

        @JsonCreator
        public static MeetingStatus parse(String status) {
            if (status == null) {
                return UNKNOWN;
            }

            int s = Integer.parseInt(status);
            for (MeetingStatus value : values()) {
                if (s == value.status) {
                    return value;
                }
            }

            return UNKNOWN;
        }

But too difficut to accept this solution: there is over 100 enums in project, I cannot modify every @JsonCreator method body……

Do you have any solutions? Or I have to remove registration of ParameterNamesModule, but it's very helpful😢

Brozen commented 2 years ago

I upgrade Jackson bom to 2.13.2.1, the exception still throws, message changed but the same result😢

Brozen commented 2 years ago

I add an annotation to help ignore parameter parse and submit a PR #235 , will you accept it ?😊

cowtowncoder commented 2 years ago

Does adding of mode = Mode.DELEGATING for @JsonCreator help? That should ensure right interpretation for handling of the single-argument constructor.

cowtowncoder commented 2 years ago

Adding mode = JsonCreator.Mode.DELEGATING changes behavior to use delegation and test passes. I don't think there is anything to fix here: single-arg Creator case is often ambiguous due to 2 different possibilities.

Brozen commented 2 years ago

Yes, It works, Thanks for that!

cowtowncoder commented 2 years ago

Ok. I'll close the PR since it seems like only usable for this specific issue.