Skip to content

When serializing a Map using the serializer obtained with SerializerProvider.findValueSerializer, a NullPointerException is thrown due to missing key serializer #4928

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

Closed
1 task done
k163377 opened this issue Jan 26, 2025 · 5 comments
Labels
will-not-fix Closed as either non-issue or something not planned to be worked on

Comments

@k163377
Copy link
Contributor

k163377 commented Jan 26, 2025

Search before asking

  • I searched in the issues and found nothing similar.

Describe the bug

This issue is a continuation of #4878 (FasterXML/jackson-module-kotlin#873).
There was a misunderstanding about reproducing the bug in Java, and the test case seems to have detected another similar bug.

Version Information

Confirmed on 2.18 and branches.

Reproduction

package com.fasterxml.jackson.databind;

import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.module.SimpleModule;
import com.fasterxml.jackson.databind.module.SimpleSerializers;
import com.fasterxml.jackson.databind.ser.std.StdSerializer;
import org.junit.jupiter.api.Test;

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

public class Kotlin873Test {
    // region: DTO Definitions
    static class MapWrapper {
        private final Map<String, Object> value;

        MapWrapper(Map<String, Object> value) {
            this.value = value;
        }

        public Map<String, Object> getValue() {
            return value;
        }
    }
    // endregion

    // region: Jackson settings
    static class UnboxSerializer extends StdSerializer<MapWrapper> {
        UnboxSerializer() {
            super(MapWrapper.class);
        }

        @Override
        public void serialize(MapWrapper value, JsonGenerator gen, SerializerProvider provider) throws IOException {
            Map<String, Object> unboxed = value.getValue();
            unboxed.put("unboxed", true);

            provider.findValueSerializer(unboxed.getClass()).serialize(unboxed, gen, provider);
        }
    }

    static class MySerializers extends SimpleSerializers {
        @Override
        public JsonSerializer<?> findSerializer(SerializationConfig config, JavaType type, BeanDescription beanDesc) {
            Class<?> rawClass = type.getRawClass();
            if (MapWrapper.class.isAssignableFrom(rawClass)) {
                return new UnboxSerializer();
            }
            return super.findSerializer(config, type, beanDesc);
        }
    }

    private final ObjectMapper mapper;

    public Kotlin873Test() {
        SimpleModule sm = new SimpleModule() {
            @Override
            public void setupModule(SetupContext context) {
                super.setupModule(context);

                context.addSerializers(new MySerializers());
            }
        };
        sm.setSerializers(new MySerializers());
        mapper = new ObjectMapper().registerModule(sm);
    }
    // endregion

    @Test
    public void test() throws JsonProcessingException {
        Map<String, Object> map = new HashMap<>();
        map.put("a", 1);

        System.out.println(mapper.writeValueAsString(new MapWrapper(map)));
    }
}

Expected behavior

At least it shouldn't be a NullPointerException.

Additional context

Finally, if the following tests succeed, the problem in kotlin-module should be solved.

package com.fasterxml.jackson.databind;

import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.introspect.Annotated;
import com.fasterxml.jackson.databind.introspect.AnnotatedMethod;
import com.fasterxml.jackson.databind.introspect.NopAnnotationIntrospector;
import com.fasterxml.jackson.databind.module.SimpleModule;
import com.fasterxml.jackson.databind.module.SimpleSerializers;
import com.fasterxml.jackson.databind.ser.std.StdSerializer;
import com.fasterxml.jackson.databind.util.StdConverter;
import org.junit.jupiter.api.Test;

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

public class Kotlin873Test {
    // region: DTO Definitions
    static class MapWrapper {
        private final Map<String, Object> value;

        MapWrapper(Map<String, Object> value) {
            this.value = value;
        }

        public Map<String, Object> getValue() {
            return value;
        }
    }

    static class Dto {
        private final Map<String, Object> value;

        Dto(Map<String, Object> value) {
            this.value = value;
        }

        public Map<String, Object> getValue() {
            return value;
        }
    }
    // endregion

    // region: Jackson settings
    static class BoxConverter extends StdConverter<Map<String, Object>, MapWrapper> {
        @Override
        public MapWrapper convert(Map<String, Object> value) {
            return new MapWrapper(value);
        }
    }

    static class UnboxSerializer extends StdSerializer<MapWrapper> {
        UnboxSerializer() {
            super(MapWrapper.class);
        }

        @Override
        public void serialize(MapWrapper value, JsonGenerator gen, SerializerProvider provider) throws IOException {
            Map<String, Object> unboxed = value.getValue();
            unboxed.put("unboxed", true);

            provider.findValueSerializer(unboxed.getClass()).serialize(unboxed, gen, provider);
        }
    }

    static class MySerializers extends SimpleSerializers {
        @Override
        public JsonSerializer<?> findSerializer(SerializationConfig config, JavaType type, BeanDescription beanDesc) {
            Class<?> rawClass = type.getRawClass();
            if (MapWrapper.class.isAssignableFrom(rawClass)) {
                return new UnboxSerializer();
            }
            return super.findSerializer(config, type, beanDesc);
        }
    }

    static class AI extends NopAnnotationIntrospector {
        @Override
        public Object findSerializationConverter(Annotated a) {
            if (a instanceof AnnotatedMethod) {
                return new BoxConverter();
            }

            return null;
        }
    }

    private final ObjectMapper mapper;

    public Kotlin873Test() {
        SimpleModule sm = new SimpleModule() {
            @Override
            public void setupModule(SetupContext context) {
                super.setupModule(context);

                context.addSerializers(new MySerializers());
                context.appendAnnotationIntrospector(new AI());
            }
        };
        sm.setSerializers(new MySerializers());
        mapper = new ObjectMapper().registerModule(sm);
    }
    // endregion

    @Test
    public void test() throws JsonProcessingException {
        Map<String, Object> map = new HashMap<>();
        map.put("a", 1);

        Dto dto = new Dto(map);

        String json = mapper.writeValueAsString(dto);
        System.out.println(json);
    }
}
@JooHyukKim
Copy link
Member

@k163377 What's the difference between the two reproductions? One under "reproduction" and one under "addtional context" 👀

@k163377
Copy link
Contributor Author

k163377 commented Jan 26, 2025

@JooHyukKim
What I wrote in the reproduction section is a MVCE of this issue.
What I wrote in the additional context section is a pseudo-reproduction of the value class serialization process in kotlin-module, which I eventually want to solve.

@cowtowncoder
Copy link
Member

I think reproduction has a bug, this can not work:

        @Override
        public void serialize(MapWrapper value, JsonGenerator gen, SerializerProvider provider) throws IOException {
            Map<String, Object> unboxed = value.getValue();
            unboxed.put("unboxed", true);

            provider.findValueSerializer(unboxed.getClass()).serialize(unboxed, gen, provider);
        }

Since provider.findValueSerializer() returns non-contextualized instance -- it MUST be contextualized, or different method called to find contextualized serializer (findPrimaryPropertySerializer(), passing null BeanProperty).

Or, perhaps better yet call:

  provider.defaultSerializeValue(value, gen);

Unfortunately I do not think there is a good way to handle things more gracefully here: adding null checks for invalid state is kind of defensive coding I don't really like.

But I will see if Javadoc note on findValueSerializer() could be added.

cowtowncoder added a commit that referenced this issue Jan 26, 2025
@cowtowncoder
Copy link
Member

So, basically, handling by caller needs to be changed: no new bug demonstrated.

@cowtowncoder cowtowncoder added will-not-fix Closed as either non-issue or something not planned to be worked on and removed to-evaluate Issue that has been received but not yet evaluated labels Jan 28, 2025
@cowtowncoder cowtowncoder closed this as not planned Won't fix, can't repro, duplicate, stale Jan 28, 2025
@k163377
Copy link
Contributor Author

k163377 commented Feb 2, 2025

@cowtowncoder
Sorry for the late reply.

provider.defaultSerializeValue(value, gen);

Thank you, I have confirmed that this fixes the kotlin-module issue.
FasterXML/jackson-module-kotlin#904

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
will-not-fix Closed as either non-issue or something not planned to be worked on
Projects
None yet
Development

No branches or pull requests

3 participants