Explain Codes LogoExplain Codes Logo

Jackson and generic type reference

java
jackson
deserialization
generics
Nikita BarsukovbyNikita Barsukov·Feb 25, 2025
TLDR

Get straight to TypeReference for quick JSON deserialization into a Java generic type with Jackson:

ObjectMapper mapper = new ObjectMapper(); String json = "[{\"name\":\"example\"}]"; TypeReference<List<MyClass<String>>> typeRef = new TypeReference<>() {}; List<MyClass<String>> list = mapper.readValue(json, typeRef);

Just bring on an anonymous subclass to instantly solve the generics issue, providing precise information for accurate readValue deserialization with TypeReference.

Dealing with custom generics deserialization

To deserialize JSON into a custom generic collection using Jackson, fight off Java's type erasure using JavaType:

//Remember: with great power comes great JavaType! JavaType type = mapper.getTypeFactory().constructCollectionType(List.class, MyClass.class); List<MyClass> myClasses = mapper.readValue(json, type);

This use case demonstrates how to leverage JavaType to convey the precise generic type of the collection to Jackson more effectively than using a Class<?>.

Working magic with complex nested generics

For JSON structures with nested generics like List<Map<String, List<MyClass>>>, Jackson's JavaType becomes your best friend. Handle complex paired types with ease:

//Yo dawg, I heard you like JavaType, so we put a JavaType in your JavaType! JavaType innerListType = mapper.getTypeFactory().constructCollectionType(List.class, MyClass.class); JavaType mapType = mapper.getTypeFactory().constructMapType(Map.class, String.class, innerListType); JavaType outerListType = mapper.getTypeFactory().constructCollectionType(List.class, mapType); List<Map<String, List<MyClass>>> listOfMaps = mapper.readValue(json, outerListType);

A hierarchical approach to defining JavaType helps to map the complete generics landscape of your JSON data cleanly.

Bypassing generic type erasure with bounds

Type erasure can be a real party pooper in Java - make sure T doesn't default to Object by indicating type bounds or using an actual class:

public class GenericHolder<T extends MyClass> { public T value; public static <T extends MyClass> GenericHolder<T> fromJson(String json, Class<T> clazz) throws JsonProcessingException { ObjectMapper mapper = new ObjectMapper(); JavaType type = mapper.getTypeFactory().constructParametricType(GenericHolder.class, clazz); return mapper.readValue(json, type); } }

By telling Jackson that T extends MyClass, your code remains generic whilst ensuring the right class is on the receiving end of the deserialization party.

Handling custom classes and annotations

Make your custom classes do the hard work when dealing with marshalling surprises. Use annotations correctly so that Jackson gets your class properties right:

//Somebody built a Jackson Annotation Station here! public class Station { @JsonProperty("name") String stationName; } ObjectMapper mapper = new ObjectMapper(); mapper.registerModule(new JodaModule()); mapper.configure(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS, false); mapper.setAnnotationIntrospector(new JacksonAnnotationIntrospector()); String jsonInput = "[{\"name\":\"Central Station\"}]"; List<Station> stations = mapper.readValue(jsonInput, new TypeReference<List<Station>>() {});

Decoding JSON and confirming output

Not all JSON inputs will be in ideal String format. Sometimes, they may come wrapped in an InputStream needing unwrapping:

//It's not stealing if you convert InputStream into String. It's ... err, "repurposing". String dataJson = new String(Files.readAllBytes(Paths.get("data.json")), StandardCharsets.UTF_8); List<Station> stations = mapper.readValue(dataJson, new TypeReference<List<Station>>() {}); stations.forEach(station -> System.out.println(station.getStationName()));

Including the expected output allows you to validate that your deserialization has run as smoothly as a lubed up otter on a waterslide.