Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Argument type mismatch for enum with @JsonCreator that takes String, gets JSON Number #3006

Closed
GrozaAnton opened this issue Jan 4, 2021 · 7 comments
Milestone

Comments

@GrozaAnton
Copy link

GrozaAnton commented Jan 4, 2021

Describe the bug
Array of number does not parse.
problem: argument type mismatch (through reference chain: java.lang.Object[][0])

Version information
2.12.0

To Reproduce
Sample in Java

public class Foo {

    public static void main(String[] args) throws JsonProcessingException {
        Type type = Operation[].class;
        List<String> sources = List.of("[1,3]", "[\"1\",\"3\"]");

        ObjectMapper mapper = new CustomObjectMapper();

            sources.forEach(s -> {
                try {
                    mapper.readValue(s, mapper.constructType(type));
                } catch (IOException e) {
                    e.printStackTrace();
                }
            });

    }

    public static class CustomJsonFactory extends JsonFactory {
        public CustomJsonFactory() {
            super();
            enable(JsonGenerator.Feature.WRITE_BIGDECIMAL_AS_PLAIN);
            enable(JsonGenerator.Feature.WRITE_NUMBERS_AS_STRINGS);
        }
    }

    public static class CustomObjectMapper extends ObjectMapper {
        public CustomObjectMapper() {
            super(new CustomJsonFactory());

            setSerializationInclusion(JsonInclude.Include.NON_NULL);

            // Disabling failure on received unknown properties
            configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
            configure(JsonParser.Feature.INCLUDE_SOURCE_IN_LOCATION, false);
            configure(DeserializationFeature.ACCEPT_FLOAT_AS_INT, false);

            // Disabling getter and setter during bean mapping
            setVisibility(PropertyAccessor.FIELD, JsonAutoDetect.Visibility.ANY);
            setVisibility(PropertyAccessor.GETTER, JsonAutoDetect.Visibility.NONE);
            setVisibility(PropertyAccessor.SETTER, JsonAutoDetect.Visibility.NONE);
            setVisibility(PropertyAccessor.IS_GETTER, JsonAutoDetect.Visibility.NONE);
        }
    }

    @RequiredArgsConstructor
    enum Operation {
        ONE(1L),
        TWO(2L),
        THREE(3L);

        private static final Map<Long, Operation> mapping;

        static {
            HashMap<Long, Operation> operations = new HashMap<>();
            for (Operation operation : Operation.values()) {
                operations.put(operation.getId(), operation);
            }

            mapping = Collections.unmodifiableMap(operations);
        }

        @Getter
        private final long id;

        @JsonCreator
        public static Operation forValue(@NonNull final String idStr) {
            Operation candidate = mapping.get(Long.parseLong(idStr));
            if (candidate == null) {
                throw new IllegalArgumentException("Unable " + idStr);
            }
            return candidate;
        }

        @JsonValue
        public long toValue() {
            return id;
        }
    }
}

Expected behavior
On version 2.10.5.1 I have two works cases for my sample "[1,3]" and "["1","3"]"
Since 2.11.0 case with JSON "[1,3]" does not work;
I did not find in the change logs changes of this behavior.

@GrozaAnton GrozaAnton added the to-evaluate Issue that has been received but not yet evaluated label Jan 4, 2021
@cowtowncoder
Copy link
Member

I think I would need a smaller reproduction for the issue, without use of Lombok (just include post-processed sources that Lombok would produce). I suspect issue is not related to containment in arrays, but I am bit confused as to why

@RequiredArgsConstructor

is used with Enum type. Does leaving that out make any difference?

@GrozaAnton
Copy link
Author

GrozaAnton commented Jan 5, 2021

I have cut the code from my project.
It turned out to be big.

I removed the lombok and wrote a constructor and getter.

Got the similar error.

problem: argument type mismatch
 at [Source: (String)"[1,3]"; line: 1, column: 2] (through reference chain: java.lang.Object[][0])
public class Foo {

    public static void main(String[] args) {
        Type type = Operation[].class;
        List<String> sources = List.of("[1,3]", "[\"1\",\"3\"]");

        ObjectMapper mapper = new ObjectMapper();

            sources.forEach(s -> {
                try {
                    mapper.readValue(s, mapper.constructType(type));
                } catch (IOException e) {
                    e.printStackTrace();
                }
            });

    }

    enum Operation {
        ONE(1L),
        TWO(2L),
        THREE(3L);

        private static final Map<Long, Operation> mapping;

        static {
            HashMap<Long, Operation> operations = new HashMap<>();
            for (Operation operation : Operation.values()) {
                operations.put(operation.getId(), operation);
            }

            mapping = Collections.unmodifiableMap(operations);
        }

        Operation(final long id) {
            this.id = id;
        }

        private final long id;

        public long getId() {
            return id;
        }

        @JsonCreator
        public static Operation forValue(final String idStr) {
            Operation candidate = mapping.get(Long.parseLong(idStr));
            if (candidate == null) {
                throw new IllegalArgumentException("Unable " + idStr);
            }
            return candidate;
        }

        @JsonValue
        public long toValue() {
            return id;
        }
    }
}

@cowtowncoder
Copy link
Member

Ok, one more thing: instead of iterating over various inputs, could you just include input that fails? So I can see (without yet running) which one of cases fails.

But I think the problem is that code expects to support both JSON numbers and JSON Strings as input to Enum creator methods. That would be possible to support with POJOs, by having two separate methods, but EnumDeserializer may expect just one. Specifically, you have:

        @JsonCreator
        public static Operation forValue(final String idStr) {

which expects a String, not number; so passing in JSON number does not match.

With POJOs you can cover both cases by accepting Object

        @JsonCreator
        public static Operation forValue(final Object id) {

in which case you get either Integer or String as argument.

Ideally you could just have two creator methods, keeping in mind that Enums constructors should never be called (so it has to be 2 factory methods), but I don't think that is yet supported.

@GrozaAnton
Copy link
Author

GrozaAnton commented Jan 6, 2021

More than one of @JsonCreator not supported.
My decision is:

@JsonCreator
public static Operation forValue(final long id) {

Both cases work with this method. I mean "[1,3]"(numbers) and "["1","3"]"(strings). in 2.11.0.

The main question is what influenced this behavior in 2.11.0. (why the numbers stopped working).
v. 2.10.5.1 works correctly without any changes. Without new method.

public class Foo {

    public static void main(String[] args) throws JsonProcessingException {
        Type type = Operation[].class;
        String s = "[1,3]";
        ObjectMapper mapper = new ObjectMapper();
        mapper.readValue(s, mapper.constructType(type));
    }

    enum Operation {
        ONE(1L),
        TWO(2L),
        THREE(3L);

        private static final Map<Long, Operation> mapping;

        static {
            HashMap<Long, Operation> operations = new HashMap<>();
            for (Operation operation : Operation.values()) {
                operations.put(operation.getId(), operation);
            }

            mapping = Collections.unmodifiableMap(operations);
        }

        Operation(final long id) {
            this.id = id;
        }

        private final long id;

        public long getId() {
            return id;
        }

        @JsonCreator
        public static Operation forValue(final String idStr) {
            Operation candidate = mapping.get(Long.parseLong(idStr));
            if (candidate == null) {
                throw new IllegalArgumentException("Unable " + idStr);
            }
            return candidate;
        }

        @JsonValue
        public long toValue() {
            return id;
        }
    }
}
/usr/lib/jvm/java-1.11.0-openjdk-amd64/bin/java -agentlib:jdwp=transport=dt_socket,address=127.0.0.1:41765,suspend=y,server=n -javaagent:/snap/intellij-idea-ultimate/266/plugins/java/lib/rt/debugger-agent.jar -Dfile.encoding=UTF-8 -classpath /home/antongroza/projects/test/target/classes:/home/antongroza/.m2/repository/com/fasterxml/jackson/core/jackson-databind/2.11.0/jackson-databind-2.11.0.jar:/home/antongroza/.m2/repository/com/fasterxml/jackson/core/jackson-annotations/2.11.0/jackson-annotations-2.11.0.jar:/home/antongroza/.m2/repository/com/fasterxml/jackson/core/jackson-core/2.11.0/jackson-core-2.11.0.jar:/snap/intellij-idea-ultimate/266/lib/idea_rt.jar Foo
Connected to the target VM, address: '127.0.0.1:41765', transport: 'socket'
OpenJDK 64-Bit Server VM warning: Sharing is only supported for boot loader classes because bootstrap classpath has been appended
Exception in thread "main" com.fasterxml.jackson.databind.exc.ValueInstantiationException: Cannot construct instance of `Foo$Operation`, problem: argument type mismatch
 at [Source: (String)"[1,3]"; line: 1, column: 2] (through reference chain: java.lang.Object[][0])
	at com.fasterxml.jackson.databind.exc.ValueInstantiationException.from(ValueInstantiationException.java:47)
	at com.fasterxml.jackson.databind.DeserializationContext.instantiationException(DeserializationContext.java:1754)
	at com.fasterxml.jackson.databind.DeserializationContext.handleInstantiationProblem(DeserializationContext.java:1128)
	at com.fasterxml.jackson.databind.deser.std.FactoryBasedEnumDeserializer.deserialize(FactoryBasedEnumDeserializer.java:159)
	at com.fasterxml.jackson.databind.deser.std.ObjectArrayDeserializer.deserialize(ObjectArrayDeserializer.java:195)
	at com.fasterxml.jackson.databind.deser.std.ObjectArrayDeserializer.deserialize(ObjectArrayDeserializer.java:21)
	at com.fasterxml.jackson.databind.ObjectMapper._readMapAndClose(ObjectMapper.java:4482)
	at com.fasterxml.jackson.databind.ObjectMapper.readValue(ObjectMapper.java:3434)
	at Foo.main(Foo.java:17)
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:116)
	at com.fasterxml.jackson.databind.deser.std.FactoryBasedEnumDeserializer.deserialize(FactoryBasedEnumDeserializer.java:151)
	... 5 more
Disconnected from the target VM, address: '127.0.0.1:41765', transport: 'socket'

Process finished with exit code 1

@cowtowncoder
Copy link
Member

Yes, 2.10.x may have accidentally behaved in a way you describe, by basically treating all scalar values as if they were text (and accessing numbers using JsonParser.getText()). Unfortunately this behavior has other problems for cases where factory method expects int.
There are a few fixed issues in 2.11(.x), changes for which may have changed behavior here (due to rewrite of functionality to work the way intended), for example:

Another possible improvement might be to allow coercion of number into @JsonCreator that takes String, but I am bit hesitant to try work on this because the default interpretation of integer numbers for Enums is to use enum index; and if so such a change would probably break other existing usage where String-taking creator is only to be used to match to names, but number should be used as index.

@cowtowncoder cowtowncoder changed the title problem: argument type mismatch (through reference chain: java.lang.Object[][0]) Argument type mismatch (through reference chain: java.lang.Object[][0]) for enum with @JsonCreator Jan 8, 2021
@cowtowncoder cowtowncoder removed the to-evaluate Issue that has been received but not yet evaluated label Jan 23, 2021
@cowtowncoder
Copy link
Member

Ok, yes; the problem is that the factory method expects String but is given Number as argument.

I am bit torn here since on one hand it'd be possible to add specific check for String type, for this to work -- but at the same time, there are issues of coercion ("I sad STRING now Number or [any other non-String] type").
The error message is not ideal for sure, so maybe I'll try tackling that first.

@cowtowncoder cowtowncoder changed the title Argument type mismatch (through reference chain: java.lang.Object[][0]) for enum with @JsonCreator Argument type mismatch for enum with @JsonCreator that takes String, gets JSON Number Oct 13, 2021
@cowtowncoder cowtowncoder added this to the 2.10.0 milestone Oct 13, 2021
@godwinmg
Copy link

godwinmg commented Dec 3, 2021

Is there a workaround for this issue until we get the fix?

@cowtowncoder cowtowncoder modified the milestones: 2.10.0, 2.13.1 Dec 4, 2021
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

3 participants