Jagger Jagger - 11 days ago 6
Java Question

Statically defined KeyDeserializer not found but if defined locally everything perfect

I am baffled by how registering a custom

KeyDeserializer
works.

Here is my code:

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


Now if I define a custom map key deserializer for my class Matchday.java, it works like a charm if I do it like this.

KeyDeserializerTest.java



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

}


It prints

{1=[com.example.TeamPlayer@3a8624[id=1,name=Arkadiusz,surname=Malarz]]}


But if I define both the object mapper and my deserializer instances as static attributes then I get the following exception!

KeyDeserializerStaticTest.java



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


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)


What is wrong here? Semantically there is no difference between the above presented main methods. Is this a feature that is somewhere documented or is it simply a bug in Jackson?

Answer

The root problem here was the order of initialization of static variables.

It is

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

while it should be

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

public static final ObjectMapper OBJECT_MAPPER = createObjectMapper();

This was hard to spot because the method addKeyDeserializer(Class<?>, KeyDeserializer) of the class SimpleModule silently adds null references to an internal key deserializers' map. In my opinion it should throw a NullPointerException upon trying adding a key deserializer reference that is null.

The Jackson code for it looks like this.

First addKeKeyDeserializer(Class<?>, KeyDeserializer)

public SimpleModule addKeyDeserializer(Class<?> type, KeyDeserializer deser)
{
    if (_keyDeserializers == null) {
        _keyDeserializers = new SimpleKeyDeserializers();
    }
    _keyDeserializers.addDeserializer(type, deser);
    return this;
}

there is no check here whether deser is null.

Then it delegates to addDeserializer(Class, KeyDeserializer) of class SimpleKeyDeserializers.

public SimpleKeyDeserializers addDeserializer(Class<?> forClass, KeyDeserializer deser)
{
    if (_classMappings == null) {
        _classMappings = new HashMap<ClassKey,KeyDeserializer>();
    }
    _classMappings.put(new ClassKey(forClass), deser);
    return this;
}

Here is the null reference also ignored and silently put into _classMappings map.

Here is the issue I posted on GitHub together with the discussion.