Skip to content

String-based Map key deserializer is not deterministic when there is no single arg constructor #3143

@hisener

Description

@hisener

Describe the bug

StdKeyDeserializers#findStringBasedKeyDeserializer uses a static factory method when there is no single arg constructor. It gets the factory method using BasicBeanDescription#findFactoryMethod that returns the first single-arg static method. However, the behavior is not deterministic as java.lang.Class#getDeclaredMethods Javadoc states:

The elements in the returned array are not sorted and are not in any particular order.

The factory methods are collected using AnnotatedCreatorCollector#_findPotentialFactories -> ClassUtil#getClassMethods -> java.lang.Class#getDeclaredMethods.

So in the case of two static factory methods, one with @JsonCreator annotation and one named #valueOf or #fromString, Jackson may use either of them depending on the array order.

The second method needs to be named #valueOf or #fromString because of the following BasicBeanDescription#isFactoryMethod logic:

// 24-Oct-2016, tatu: As per [databind#1429] must ensure takes exactly one arg
if ("valueOf".equals(name)) {
if (am.getParameterCount() == 1) {
return true;
}
}
// [databind#208] Also accept "fromString()", if takes String or CharSequence
if ("fromString".equals(name)) {
if (am.getParameterCount() == 1) {
Class<?> cls = am.getRawParameterType(0);
if (cls == String.class || CharSequence.class.isAssignableFrom(cls)) {
return true;
}
}
}

Version information

2.12.2

To Reproduce

static class KeyTypeMultipleFactoryMethods {
    protected String value;

    private KeyTypeMultipleFactoryMethods(String v, boolean bogus) {
        value = v;
    }

    @JsonCreator
    public static KeyTypeMultipleFactoryMethods create(String v) {
        return new KeyTypeMultipleFactoryMethods(v, true);
    }

    public static KeyTypeMultipleFactoryMethods valueOf(String id) {
        return new KeyTypeMultipleFactoryMethods(id.toUpperCase(Locale.ROOT), false);
    }
}

public void testKeyWithCreatorAndMultipleFactoryMethods() throws Exception
{
    Map<KeyTypeMultipleFactoryMethods,Integer> map = MAPPER.readValue("{\"foo\":3}",
            new TypeReference<Map<KeyTypeMultipleFactoryMethods,Integer>>() {} );
    assertEquals(1, map.size());
    assertEquals("foo", map.keySet().iterator().next().value);
}
[ERROR] Failures: 
[ERROR]   MapDeserializationTest.testKeyWithCreatorAndMultipleFactoryMethods:486 expected:<[foo]> but was:<[FOO]>
[INFO] 
[ERROR] Tests run: 26, Failures: 1, Errors: 0, Skipped: 0

It's also available in this branch: 2.12...PicnicSupermarket:hsener/find-factory-method.

Expected behavior

@JsonCreator method has precedence over other static factory methods in String-based deserialization.

Additional context

We noticed this behavior in a custom key deserializer, which uses BasicBeanDescription#findFactoryMethod. The behavior was nondeterministic for an enum where we have Enum#valueOf and a static factory method with @JsonCreator annotation.

Metadata

Metadata

Assignees

No one assigned

    Labels

    has-failing-testIndicates that there exists a test case (under `failing/`) to reproduce the issue

    Type

    No type

    Projects

    No projects

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions