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

The KeyDeserializer specified in the class with @JsonDeserialize(keyUsing = ...) is overwritten by the KeyDeserializer specified in the ObjectMapper. #4444

Closed
1 task done
k163377 opened this issue Mar 23, 2024 · 5 comments
Labels
Milestone

Comments

@k163377
Copy link
Contributor

k163377 commented Mar 23, 2024

Search before asking

  • I searched in the issues and found nothing similar.

Describe the bug

SSIA

Also, the attached Java reproduction code is directly adding KeyDeserializer to SimpleModule, but it seemed to be reproduced when using KeyDeserializers.

Version Information

Reproduced in the latest 2.17 branch(fe42cf7).
Also, 2.16.1 seems to have the same problem.

Reproduction

import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.DeserializationContext;
import com.fasterxml.jackson.databind.KeyDeserializer;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
import com.fasterxml.jackson.databind.module.SimpleModule;
import org.junit.jupiter.api.Test;

import java.io.IOException;
import java.util.Map;

import static org.junit.jupiter.api.Assertions.assertEquals;

public class KeyDeserializerOverwritten {
    @JsonDeserialize(keyUsing = ForClass.class)
    static class MyKey {
        private final String value;

        MyKey(String value) {
            this.value = value;
        }
    }

    static class ForClass extends KeyDeserializer {
        @Override
        public Object deserializeKey(String key, DeserializationContext ctxt) throws IOException {
            return new MyKey(key + "-class");
        }
    }

    static class ForMapper extends KeyDeserializer {
        @Override
        public Object deserializeKey(String key, DeserializationContext ctxt) throws IOException {
            return new MyKey(key + "-mapper");
        }
    }

    TypeReference<Map<MyKey, String>> typeRef = new TypeReference<>() {};

    @Test
    void notCustom() throws JsonProcessingException {
        ObjectMapper mapper = new ObjectMapper();
        Map<MyKey, String> result = mapper.readValue("{\"foo\":null}", typeRef);

        // OK
        assertEquals("foo-class", result.keySet().stream().findFirst().get().value);
    }

    @Test
    void addKeyDeserializer() throws JsonProcessingException {
        SimpleModule sm = new SimpleModule();
        sm.addKeyDeserializer(MyKey.class, new ForMapper());

        ObjectMapper mapper = new ObjectMapper().registerModule(sm);
        Map<MyKey, String> result = mapper.readValue("{\"foo\":null}", typeRef);

        // NG
        assertEquals("foo-class", result.keySet().stream().findFirst().get().value);
    }
}

Expected behavior

Like JsonDeserializer, the KeyDeserializer specified for the class must be used.

Additional context

This problem was discovered during prototyping to solve FasterXML/jackson-module-kotlin#777.
ProjectMapK/jackson-module-kogera#224

I'm not sure if I should merge it into kotlin-module as it is, since the default content provided by KotlinModule overrides any user customization.

@k163377 k163377 added the to-evaluate Issue that has been received but not yet evaluated label Mar 23, 2024
@cowtowncoder cowtowncoder added 2.17 Issues planned at earliest for 2.17 and removed to-evaluate Issue that has been received but not yet evaluated labels Mar 24, 2024
@cowtowncoder
Copy link
Member

Sounds like a bug indeed.

@k163377
Copy link
Contributor Author

k163377 commented Jan 26, 2025

@cowtowncoder
I did a little analysis.
The direct cause is the following process

KeyDeserializer deser = null;
if (_factoryConfig.hasKeyDeserializers()) {
beanDesc = config.introspectClassAnnotations(type);
for (KeyDeserializers d : _factoryConfig.keyDeserializers()) {
deser = d.findKeyDeserializer(type, config, beanDesc);
if (deser != null) {
break;
}
}
}
// the only non-standard thing is this:
if (deser == null) {
// [databind#2452]: Support `@JsonDeserialize(keyUsing = ...)`
if (beanDesc == null) {
beanDesc = config.introspectClassAnnotations(type.getRawClass());
}
deser = findKeyDeserializerFromAnnotation(ctxt, beanDesc.getClassInfo());

Although it seems that the acquisition from the annotation should be given priority, in this processing order, the result acquired from _factoryConfig is given priority.

@k163377
Copy link
Contributor Author

k163377 commented Jan 26, 2025

I am not too sure, but I have issued a pull request to fix this.
#4929

@cowtowncoder cowtowncoder added 2.18 and removed 2.17 Issues planned at earliest for 2.17 labels Jan 26, 2025
@cowtowncoder
Copy link
Member

Thank you @k163377 -- I think fix is correct & clean, will proceed with it!

@cowtowncoder cowtowncoder added this to the 2.18.3 milestone Jan 26, 2025
@cowtowncoder
Copy link
Member

Fixed in 2.18 for 2.18.3

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

No branches or pull requests

2 participants