Search code examples
javajsonjacksonobjectmapperobject-pooling

Supply Long Object Pool to Jackson Object Mapper


I have a JSON that I convert into POJOs. The JSON is read from a GZIPInputStream gis.

ObjectMapper mapper = new ObjectMapper();

TypeReference<Map<Long, ConfigMasterAirportData>> typeRef =
                new TypeReference<Map<Long, ConfigMasterAirportData>>() {};

Map<Long, ConfigMasterAirportData> configMasterAirportMap = 
                mapper.readValue(gis, typeRef);

I do not want new Long objects to be created for each entry. I want it to get Long objects from a custom LongPool I have created. Is there a way to pass such a LongPool to the mapper?

If not, is there another JSON library I can use to do that?


Solution

  • There are many ways to achieve this if you are sure that object pooling is required in your case.

    First of all, Java already does Long object pooling for a small range between -128 and 127 inclusive. See source code of Long.valueOf.

    Let us have 2 JSON objects that we want to deserialize: map1 and map2:

        final String map1 = "{\"1\": \"Hello\", \"10000000\": \"world!\"}";
        final String map2 = "{\"1\": \"You\", \"10000000\": \"rock!\"}";
    

    Standard deserialization

    If we use standard deserialization:

        final ObjectMapper mapper = new ObjectMapper();
        final TypeReference<Map<Long, String>> typeRef = new TypeReference<Map<Long, String>>() {};
        final Map<Long, String> deserializedMap1 = mapper.readValue(map1, typeRef);
        final Map<Long, String> deserializedMap2 = mapper.readValue(map2, typeRef);
    
        printMap(deserializedMap1);
        printMap(deserializedMap2);
    

    Where printMap is defined as

    private static void printMap(Map<Long, String> longStringMap) {
        longStringMap.forEach((Long k, String v) -> {
            System.out.printf("key object id %d \t %s -> %s %n", System.identityHashCode(k), k, v);
        });
    }
    

    we get the following output:

    key object id 1635756693     1 -> Hello 
    key object id 504527234      10000000 -> world! 
    key object id 1635756693     1 -> You 
    key object id 101478235      10000000 -> rock! 
    

    Note that key 1 is the same object with hashcode 1635756693 in both maps. This is due to built-in pool for [-128,127] range.

    Solution1: @JsonAnySetter deserialization

    We can define a wrapper object for the map and use @JsonAnySetter annotation to intercept all key-value pairs being deserialized. Then we can intern each Long object using Guava StrongInterner:

    static class CustomLongPoolingMap {
        private static final Interner<Long> LONG_POOL = Interners.newStrongInterner();
        private final Map<Long, String> map = new HashMap<>();
    
        @JsonAnySetter
        public void addEntry(String key, String value) {
            map.put(LONG_POOL.intern(Long.parseLong(key)), value);
        }
    
        public Map<Long, String> getMap() {
            return map;
        }
    }
    

    We will use it like this:

        final ObjectMapper mapper = new ObjectMapper();
        final Map<Long, String> deserializedMap1 = mapper.readValue(map1, CustomLongPoolingMap.class).getMap();
        final Map<Long, String> deserializedMap2 = mapper.readValue(map2, CustomLongPoolingMap.class).getMap();
    

    Output:

    key object id 1635756693     1 -> Hello 
    key object id 1596467899     10000000 -> world! 
    key object id 1635756693     1 -> You 
    key object id 1596467899     10000000 -> rock! 
    

    Now you can see that key 10000000 is also the same object in both maps with hashcode 1596467899

    Solution 2: Register custom KeyDeserializer

    Define custom KeySerializer:

    public static class MyCustomKeyDeserializer extends KeyDeserializer {
        private static final Interner<Long> LONG_POOL = Interners.newStrongInterner();
        @Override
        public Long deserializeKey(String key, DeserializationContext ctxt) {
            return LONG_POOL.intern(Long.parseLong(key));
        }
    }
    

    And register it with the ObjectMapper:

        final SimpleModule module = new SimpleModule();
        module.addKeyDeserializer(Long.class, new MyCustomKeyDeserializer());
        final ObjectMapper mapper = new ObjectMapper().registerModule(module);
        final TypeReference<Map<Long, String>> typeRef = new TypeReference<Map<Long, String>>() {};
        final Map<Long, String> deserializedMap1 = mapper.readValue(map1, typeRef);
        final Map<Long, String> deserializedMap2 = mapper.readValue(map2, typeRef);
    

    Solution 3: Use custom KeyDeserializer via @JsonDeserialize annotation

    Define a wrapper object

    static class MapWrapper {
        @JsonDeserialize(keyUsing = MyCustomKeyDeserializer.class)
        private Map<Long, String> map1;
        @JsonDeserialize(keyUsing = MyCustomKeyDeserializer.class)
        private Map<Long, String> map2;
    }
    

    And deserialize it:

        final ObjectMapper mapper = new ObjectMapper();
        final String json = "{\"map1\": " + map1 + ", \"map2\": " + map2 + "}";
        final MapWrapper wrapper = mapper.readValue(json, MapWrapper.class);
        final Map<Long, String> deserializedMap1 = wrapper.map1;
        final Map<Long, String> deserializedMap2 = wrapper.map2;
    

    Solution 4: Use Trove library TLongObjectMap to avoid using Long objects entirely

    Trove library implements maps that use primitive types for keys to remove overhead of boxed objects entirely. It's in somewhat dormant state however.

    You need in your case TLongObjectHashMap.

    There is a library that defines a deserializer for TIntObjectMap: https://bitbucket.org/marshallpierce/jackson-datatype-trove/src/d7386afab0eece6f34a0af69b76b478f417f0bd4/src/main/java/com/palominolabs/jackson/datatype/trove/deser/TIntObjectMapDeserializer.java?at=master&fileviewer=file-view-default

    I think it will be quite easy to adapt it for TLongObjectMap.


    Full code for this answer can be found here: https://gist.github.com/shtratos/f0a81515d19b858dafb71e86b62cb474

    I've used answers to this question for solutions 2 & 3: Deserializing non-string map keys with Jackson