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

A KeyDeserializer instance defined as private static not found even though added to registered SimpleModule #1447

Closed
PawelJagus opened this issue Nov 10, 2016 · 15 comments

Comments

@PawelJagus
Copy link

Matchday.java

package com.example;

import java.io.Serializable;
import java.util.Objects;

public class Matchday implements Serializable, Comparable<Matchday> {
    private static final long serialVersionUID = -8823049187525703664L;

    private final int matchdayNumber;

    public Matchday(final int matchdayNumber) {
        this.matchdayNumber = matchdayNumber;
    }

    public int getMatchdayNumber() {
        return matchdayNumber;
    }

    @Override
    public int compareTo(Matchday o) {
        return Integer.compare(matchdayNumber, o.getMatchdayNumber());
    }

    @Override
    public final int hashCode() {
        return Objects.hash(matchdayNumber);
    }

    @Override
    public final boolean equals(final Object obj) {
        return obj instanceof Matchday && Integer.valueOf(matchdayNumber).equals(((Matchday) obj).matchdayNumber);
    }
    
    @Override
    public String toString() {
        return Integer.toString(matchdayNumber);
    }
}

TeamPlayer.java

package com.example;

import java.io.Serializable;

import org.apache.commons.lang3.builder.ToStringBuilder;

public class TeamPlayer implements Serializable {
    private static final long serialVersionUID = -6057852081020631549L;

    private int id;
    private String name;
    private String surname;

    public int getId() {
        return id;
    }

    public void setId(int id) {
        this.id = id;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public String getSurname() {
        return surname;
    }

    public void setSurname(String surname) {
        this.surname = surname;
    }

    @Override
    public String toString() {
        return new ToStringBuilder(this).append("id", id).append("name", name).append("surname", surname).build()
                .toString();
    }
}

KeyDeserializerStaticTest.java (Probable Bug in Jackson)

package com.example;

import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.List;
import java.util.SortedMap;

import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.core.Version;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.DeserializationContext;
import com.fasterxml.jackson.databind.KeyDeserializer;
import com.fasterxml.jackson.databind.Module;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.module.SimpleModule;

public class KeyDeserializerStaticTest {
    public static final ObjectMapper OBJECT_MAPPER = createObjectMapper();

    private static final KeyDeserializer MATCHDAY_KEY_DESERIALIZER = new KeyDeserializer() {

        @Override
        public Object deserializeKey(String key, DeserializationContext ctxt)
                throws IOException, JsonProcessingException {
            return new Matchday(Integer.valueOf(key));
        }
    };

    private static ObjectMapper createObjectMapper() {
        ObjectMapper objectMapper = new ObjectMapper();
        objectMapper.registerModule(createSimpleModule());
        return objectMapper;
    }

    private static Module createSimpleModule() {
        SimpleModule simpleModule = new SimpleModule("dummy", new Version(0, 0, 0, "dummy", "dummy", "dummy"));
        simpleModule.addKeyDeserializer(Matchday.class, MATCHDAY_KEY_DESERIALIZER);
        return simpleModule;
    }

    public static void main(String[] args) throws IOException {
        final InputStream inputStream = new ByteArrayInputStream(
                "{\"1\":[{\"id\": 1, \"name\": \"Arkadiusz\", \"surname\": \"Malarz\"}]}".getBytes());
        SortedMap<Matchday, List<TeamPlayer>> map = OBJECT_MAPPER.readValue(inputStream,
                new TypeReference<SortedMap<Matchday, List<TeamPlayer>>>() {
                });
        System.out.println(map);
    }
}

The main method of KeyDeserializerStaticTest ends unexpectedly with this runtime exception.

Exception in thread "main" com.fasterxml.jackson.databind.JsonMappingException: Can not find a (Map) Key deserializer for type [simple type, class com.example.Matchday]
 at [Source: java.io.ByteArrayInputStream@bbc1e0; line: 1, column: 1]
    at com.fasterxml.jackson.databind.JsonMappingException.from(JsonMappingException.java:270)
    at com.fasterxml.jackson.databind.DeserializationContext.reportMappingException(DeserializationContext.java:1234)
    at com.fasterxml.jackson.databind.deser.DeserializerCache._handleUnknownKeyDeserializer(DeserializerCache.java:585)
    at com.fasterxml.jackson.databind.deser.DeserializerCache.findKeyDeserializer(DeserializerCache.java:168)
    at com.fasterxml.jackson.databind.DeserializationContext.findKeyDeserializer(DeserializationContext.java:499)
    at com.fasterxml.jackson.databind.deser.std.MapDeserializer.createContextual(MapDeserializer.java:247)
    at com.fasterxml.jackson.databind.DeserializationContext.handleSecondaryContextualization(DeserializationContext.java:681)
    at com.fasterxml.jackson.databind.DeserializationContext.findRootValueDeserializer(DeserializationContext.java:481)
    at com.fasterxml.jackson.databind.ObjectMapper._findRootDeserializer(ObjectMapper.java:3899)
    at com.fasterxml.jackson.databind.ObjectMapper._readMapAndClose(ObjectMapper.java:3794)
    at com.fasterxml.jackson.databind.ObjectMapper.readValue(ObjectMapper.java:2915)
    at com.example.KeyDeserializerStaticTest.main(KeyDeserializerStaticTest.java:43)

But if I define the custom map key deserializer a bit different semantically, then everything works as expected. It looks like for unknown reason, a map key deserializer instance defined as private static is not visible even if the module it is assigned to is registered.

KeyDeserializerTest.java (OK)

package com.example;

import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.List;
import java.util.SortedMap;

import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.core.Version;
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.module.SimpleModule;

public class KeyDeserializerTest {

    public static void main(String[] args) throws IOException {
        final ObjectMapper objectMapper = new ObjectMapper();
        final SimpleModule mySimpleModule = new SimpleModule("dummy", new Version(0, 0, 0, "dummy", "dummy", "dummy"));
        mySimpleModule.addKeyDeserializer(Matchday.class, new KeyDeserializer() {

            @Override
            public Object deserializeKey(String arg0, DeserializationContext arg1)
                    throws IOException, JsonProcessingException {
                return new Matchday(Integer.valueOf(arg0));
            }
        });
        objectMapper.registerModule(mySimpleModule);

        final InputStream inputStream = new ByteArrayInputStream(
                "{\"1\":[{\"id\": 1, \"name\": \"Arkadiusz\", \"surname\": \"Malarz\"}]}".getBytes());
        SortedMap<Matchday, List<TeamPlayer>> map = objectMapper.readValue(inputStream,
                new TypeReference<SortedMap<Matchday, List<TeamPlayer>>>() {
                });
        System.out.println(map);
    }

}
@cowtowncoder
Copy link
Member

@PawelJagus Unfortunately I can not reproduce with code from Jackson 2.8 branch; test passes. without problems. Which version does the problem affect? It could be that fix for either #1441 or #1445 could have resolved this too.

@PawelJagus
Copy link
Author

PawelJagus commented Nov 11, 2016

@cowtowncode I am currently using version 2.8.4 and can reproduce this issue each and every time.

@PawelJagus
Copy link
Author

@cowtowncoder Just ran it with 2.9.0-SNAPSHOT. It is still there. I am trying to test it with 2.8.5-SNAPSHOT as well but currently having some issues with overriding managed versions of Spring Boot.

@cowtowncoder
Copy link
Member

Ok. Combining classes into a single junit test class might help too; I simplified code slightly but do not think that should change results.

@PawelJagus
Copy link
Author

PawelJagus commented Nov 11, 2016

Combined that stuff into a single file with JUnit test.

StaticKeyDeserializerTest.java

package com.example;

import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.Serializable;
import java.util.List;
import java.util.Objects;
import java.util.SortedMap;

import org.apache.commons.lang3.builder.ToStringBuilder;
import org.junit.Test;

import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.core.Version;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.DeserializationContext;
import com.fasterxml.jackson.databind.KeyDeserializer;
import com.fasterxml.jackson.databind.Module;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.module.SimpleModule;

class Matchday implements Serializable, Comparable<Matchday> {
    private static final long serialVersionUID = -8823049187525703664L;

    private final int matchdayNumber;

    public Matchday(final int matchdayNumber) {
        this.matchdayNumber = matchdayNumber;
    }

    public int getMatchdayNumber() {
        return matchdayNumber;
    }

    @Override
    public int compareTo(Matchday o) {
        return Integer.compare(matchdayNumber, o.getMatchdayNumber());
    }

    @Override
    public final int hashCode() {
        return Objects.hash(matchdayNumber);
    }

    @Override
    public final boolean equals(final Object obj) {
        return obj instanceof Matchday && Integer.valueOf(matchdayNumber).equals(((Matchday) obj).matchdayNumber);
    }

    @Override
    public String toString() {
        return Integer.toString(matchdayNumber);
    }
}

class TeamPlayer implements Serializable {
    private static final long serialVersionUID = -6057852081020631549L;

    private int id;
    private String name;
    private String surname;

    public int getId() {
        return id;
    }

    public void setId(int id) {
        this.id = id;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public String getSurname() {
        return surname;
    }

    public void setSurname(String surname) {
        this.surname = surname;
    }

    @Override
    public String toString() {
        return new ToStringBuilder(this).append("id", id).append("name", name).append("surname", surname).build()
                .toString();
    }
}

public class StaticKeyDeserializerTest {
    private static final String JSON_2_BE_PARSED = "{\"1\":[{\"id\": 1, \"name\": \"Arkadiusz\", \"surname\": \"Malarz\"}]}";

    public static final ObjectMapper OBJECT_MAPPER = createObjectMapper();

    private static final KeyDeserializer MATCHDAY_KEY_DESERIALIZER = new KeyDeserializer() {

        @Override
        public Object deserializeKey(String key, DeserializationContext ctxt)
                throws IOException, JsonProcessingException {
            return new Matchday(Integer.valueOf(key));
        }
    };

    private static ObjectMapper createObjectMapper() {
        ObjectMapper objectMapper = new ObjectMapper();
        objectMapper.registerModule(createSimpleModule());
        return objectMapper;
    }

    private static Module createSimpleModule() {
        SimpleModule simpleModule = new SimpleModule("dummy", new Version(0, 0, 0, "dummy", "dummy", "dummy"));
        simpleModule.addKeyDeserializer(Matchday.class, MATCHDAY_KEY_DESERIALIZER);
        return simpleModule;
    }

    @Test
    public void testStatic() throws IOException {
        final InputStream inputStream = new ByteArrayInputStream(JSON_2_BE_PARSED.getBytes());
        SortedMap<Matchday, List<TeamPlayer>> map = OBJECT_MAPPER.readValue(inputStream,
                new TypeReference<SortedMap<Matchday, List<TeamPlayer>>>() {
                });
        System.out.println(map);
    }

    @Test
    public void testInPlace() throws IOException {
        final ObjectMapper objectMapper = new ObjectMapper();
        final SimpleModule mySimpleModule = new SimpleModule("dummy", new Version(0, 0, 0, "dummy", "dummy", "dummy"));
        mySimpleModule.addKeyDeserializer(Matchday.class, new KeyDeserializer() {

            @Override
            public Object deserializeKey(String arg0, DeserializationContext arg1)
                    throws IOException, JsonProcessingException {
                return new Matchday(Integer.valueOf(arg0));
            }
        });
        objectMapper.registerModule(mySimpleModule);

        final InputStream inputStream = new ByteArrayInputStream(JSON_2_BE_PARSED.getBytes());
        SortedMap<Matchday, List<TeamPlayer>> map = objectMapper.readValue(inputStream,
                new TypeReference<SortedMap<Matchday, List<TeamPlayer>>>() {
                });
        System.out.println(map);
    }
}

The exception does still occur.

@cowtowncoder
Copy link
Member

Thanks. I can reproduce the problem now; not sure what I did differently first.

@cowtowncoder
Copy link
Member

Wow. This is interesting... it comes down to having two tests. Almost as if caching was failing...

@PawelJagus
Copy link
Author

I know, right? I do not know if having it as private static does the change here but theoretically it should not make any difference as just the reference to it gets passed and all the methods of this instance's class are public anyway. Unless there is some strange reflection thingy going on somewhere.

@cowtowncoder
Copy link
Member

Indeed. And... with some more changes, exception disappears as well. Very weird. Now need to try to undo some changes...

@cowtowncoder
Copy link
Member

Lol. Actually, not a mystery after all. It's just the problem with initialization order, after all:

First public static final ObjectMapper OBJECT_MAPPER = createObjectMapper(); is executed; method createObjectMapper() tries to reference MATCHDAY_KEY_DESERIALIZER, which isn't yet initialized... since that would be second statement in order, but due to call, isn't yet there.
Changing declaration order will fix this.

@PawelJagus
Copy link
Author

PawelJagus commented Nov 11, 2016

OMG, you are right. I would expect an ExceptionInInitializerError or NullPointerException kind of thing. Are you swallowing that somehow in the stack while catching and rethrowing maybe? If so could you add the root cause exception there?

@PawelJagus
Copy link
Author

Well, in the source code I see that null references are completely ignored and silently added to the key deserializer map anyway...

@cowtowncoder
Copy link
Member

@PawelJagus perhaps nulls could be checked against; but one could alternatively consider adding of null to indicate "remove" handler so I don't know whether this should be done or not.
But not swallowing is done by Jackson, so it's just not checking for null.

@PawelJagus
Copy link
Author

@cowtowncoder Well, I would consider using a method that begins with add to remove something at least odd. I can see that there is no method like removeKeyDeserializer but maybe you should consider it for a future major release and at the same time check for null in add method.

@cowtowncoder
Copy link
Member

@PawelJagus for now adding null check sounds reasonable.

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

2 participants