Skip to content

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

Closed as not planned

Description

@k163377

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);
    }
}

Activity

  1. added
    to-evaluateIssue that has been received but not yet evaluated
    on Jan 26, 2025
  2. JooHyukKim commented on Jan 26, 2025

    @JooHyukKim
    Member

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

  3. k163377 commented on Jan 26, 2025

    @k163377
    ContributorAuthor

    @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.

  4. cowtowncoder commented on Jan 26, 2025

    @cowtowncoder
    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.

  5. added a commit that references this issue on Jan 26, 2025
  6. cowtowncoder commented on Jan 26, 2025

    @cowtowncoder
    Member

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

  7. added
    will-not-fixClosed as either non-issue or something not planned to be worked on
    and removed
    to-evaluateIssue that has been received but not yet evaluated
    on Jan 28, 2025
  8. k163377 commented on Feb 2, 2025

    @k163377
    ContributorAuthor

    @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

Metadata

Metadata

Assignees

No one assigned

    Labels

    will-not-fixClosed as either non-issue or something not planned to be worked on

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions