cyberangles blog

How to Cast LinkedHashMap to List of Complex Objects in Jackson DynamoDB Deserialization

When working with Amazon DynamoDB and Jackson (the popular JSON parsing library for Java), developers often encounter a common deserialization challenge: nested lists of custom objects are unexpectedly deserialized as List<LinkedHashMap> instead of List<CustomObject>. This issue arises because DynamoDB stores complex data as JSON-like structures, and Jackson, lacking explicit type information, defaults to LinkedHashMap (a map implementation preserving insertion order) for untyped objects.

This blog post will demystify why this happens, walk through step-by-step solutions to resolve it, and provide practical examples to ensure smooth deserialization of complex object lists. By the end, you’ll confidently convert LinkedHashMap lists into strongly typed List<CustomObject> instances.

2025-11

Table of Contents#

  1. Understanding the Problem: Why LinkedHashMap?
  2. Prerequisites
  3. Solutions to Deserialize LinkedHashMap to List
  4. Common Pitfalls to Avoid
  5. Practical Example: End-to-End Workflow
  6. Conclusion
  7. References

Understanding the Problem: Why LinkedHashMap?#

DynamoDB stores non-scalar data (like lists or objects) as AttributeValue types (e.g., L for lists, M for maps). When retrieving these values using the AWS SDK, they are often parsed into raw Java collections (e.g., List<Map<String, Object>>). Jackson, by default, deserializes untyped JSON objects into LinkedHashMap (instead of a custom class) because it lacks compile-time type information for nested structures.

For example, suppose you have a DynamoDB item with a tasks attribute containing a list of Task objects (with fields id and description). When you retrieve this item, DynamoDB returns the tasks list as a List<LinkedHashMap<String, Object>>, where each LinkedHashMap represents a Task’s key-value pairs. Attempting to cast this list to List<Task> will throw a ClassCastException:

// ❌ Error: ClassCastException: LinkedHashMap cannot be cast to Task  
List<Task> tasks = (List<Task>) dynamoDbItem.get("tasks");  

Prerequisites#

Before diving into solutions, ensure you have the following tools/libraries:

  • Java 8+: For lambda expressions and type inference.
  • AWS SDK for DynamoDB: To interact with DynamoDB (e.g., software.amazon.awssdk:dynamodb:2.20.0).
  • Jackson Databind: For JSON serialization/deserialization (com.fasterxml.jackson.core:jackson-databind:2.15.2).
  • Lombok (optional): To reduce boilerplate (getters, setters, constructors).

Solutions to Deserialize LinkedHashMap to List#

Solution 1: Use ObjectMapper with TypeReference#

The most straightforward fix is to explicitly tell Jackson the target type using TypeReference. This class captures generic type information (e.g., List<Task>) at runtime, allowing Jackson to deserialize the LinkedHashMap list into your custom object list.

Steps:#

  1. Retrieve the raw list from DynamoDB (e.g., as a List<LinkedHashMap> or JSON string).
  2. Use ObjectMapper to convert the raw list to List<CustomObject> with TypeReference.

Example Code:#

Suppose we have a Task class:

import lombok.Data;  
 
@Data // Generates getters, setters, no-arg constructor (critical for Jackson)  
public class Task {  
    private String id;  
    private String description;  
}  

Retrieve and deserialize the list:

import com.fasterxml.jackson.core.type.TypeReference;  
import com.fasterxml.jackson.databind.ObjectMapper;  
import software.amazon.awssdk.services.dynamodb.model.AttributeValue;  
 
// 1. Retrieve the "tasks" attribute from DynamoDB (as a List of maps)  
List<Map<String, Object>> rawTasks = (List<Map<String, Object>>) dynamoDbItem.get("tasks").l().stream()  
    .map(AttributeValue::m) // Convert each AttributeValue to a Map  
    .collect(Collectors.toList());  
 
// 2. Use ObjectMapper with TypeReference to deserialize  
ObjectMapper mapper = new ObjectMapper();  
List<Task> tasks = mapper.convertValue(  
    rawTasks,  
    new TypeReference<List<Task>>() {} // Explicitly specify List<Task>  
);  

How It Works:#

  • mapper.convertValue(rawTasks, TypeReference) converts the List<LinkedHashMap> into List<Task> by mapping each LinkedHashMap entry to Task fields (using getters/setters).
  • TypeReference<List<Task>> bypasses Java’s type erasure, letting Jackson know the target generic type.

Solution 2: Custom Deserializer with @JsonDeserialize#

If the list is nested within a parent class (e.g., a User class with a List<Task> field), use Jackson’s @JsonDeserialize annotation to specify the content type of the list.

Steps:#

  1. Annotate the list field in the parent class with @JsonDeserialize(contentAs = CustomObject.class).
  2. Ensure Jackson can access the custom object’s constructor and fields.

Example Code:#

Define a parent User class with a nested List<Task>:

import com.fasterxml.jackson.databind.annotation.JsonDeserialize;  
import lombok.Data;  
 
@Data  
public class User {  
    private String userId;  
 
    // Tell Jackson to deserialize list elements as Task  
    @JsonDeserialize(contentAs = Task.class)  
    private List<Task> tasks;  
}  

Deserialize the entire User object from DynamoDB:

// Retrieve the User item as a Map (e.g., from DynamoDB's GetItemResponse)  
Map<String, Object> userMap = dynamoDbResponse.item();  
 
// Deserialize the map to User  
User user = mapper.convertValue(userMap, User.class);  
// Now user.getTasks() is List<Task>, not List<LinkedHashMap>  

How It Works:#

  • @JsonDeserialize(contentAs = Task.class) tells Jackson to treat each element of the list as a Task instance, avoiding LinkedHashMap.

Solution 3: DynamoDB-Specific Mappings with @DynamoDBTypeConvertedJson#

For seamless integration with DynamoDB, use @DynamoDBTypeConvertedJson (from the AWS SDK) to serialize/deserialize complex objects directly. This annotation automatically handles type information using Jackson under the hood.

Steps:#

  1. Annotate the list field with @DynamoDBTypeConvertedJson and specify the TypeReference.
  2. Use DynamoDBMapper to read/write items, leveraging the annotation for type safety.

Example Code:#

import com.amazonaws.services.dynamodbv2.datamodeling.DynamoDBMapper;  
import com.amazonaws.services.dynamodbv2.datamodeling.DynamoDBTable;  
import com.amazonaws.services.dynamodbv2.datamodeling.DynamoDBTypeConvertedJson;  
import com.fasterxml.jackson.core.type.TypeReference;  
 
@DynamoDBTable(tableName = "Users")  
@Data  
public class User {  
    private String userId;  
 
    // Serialize/deserialize List<Task> using DynamoDBTypeConvertedJson  
    @DynamoDBTypeConvertedJson(converter = TaskListConverter.class)  
    private List<Task> tasks;  
 
    // Custom converter to handle List<Task>  
    public static class TaskListConverter extends DynamoDBTypeConvertedJson<List<Task>> {  
        public TaskListConverter() {  
            super(new TypeReference<List<Task>>() {}); // Explicit type  
        }  
    }  
}  

Retrieve the User item using DynamoDBMapper:

DynamoDBMapper mapper = new DynamoDBMapper(dynamoDbClient);  
User user = mapper.load(User.class, "user123");  
// user.getTasks() is now List<Task>  

How It Works:#

  • @DynamoDBTypeConvertedJson serializes List<Task> to a JSON string in DynamoDB and deserializes it back using the specified TypeReference, avoiding LinkedHashMap.

Common Pitfalls to Avoid#

  • Missing No-Arg Constructor: Jackson requires a no-argument constructor to instantiate CustomObject. Use @NoArgsConstructor (Lombok) or define one manually.

    @NoArgsConstructor // Critical for Jackson  
    @Data  
    public class Task {  
        private String id;  
        private String description;  
    }  
  • Missing Getters/Setters: Jackson uses getters/setters to populate fields. Use @Data (Lombok) or define them explicitly.

  • Untyped List in Parent Class: Avoid raw types like List (use List<Task> instead). Raw types cause Jackson to default to LinkedHashMap.

  • Null Values: Handle nulls in DynamoDB responses (e.g., mapper.convertValue throws NullPointerException if rawTasks is null).

Practical Example: End-to-End Workflow#

Let’s walk through a complete scenario to solidify the concepts.

Step 1: Define the Custom Object (Task)#

import lombok.Data;  
import lombok.NoArgsConstructor;  
 
@Data  
@NoArgsConstructor // Required for Jackson  
public class Task {  
    private String id;  
    private String description;  
 
    public Task(String id, String description) {  
        this.id = id;  
        this.description = description;  
    }  
}  

Step 2: Store a List of Task in DynamoDB#

Insert a sample item into DynamoDB with a tasks list:

{  
  "userId": "user123",  
  "tasks": [  
    {"id": "task1", "description": "Buy groceries"},  
    {"id": "task2", "description": "Finish blog post"}  
  ]  
}  

Step 3: Retrieve and Deserialize with TypeReference#

import com.fasterxml.jackson.core.type.TypeReference;  
import com.fasterxml.jackson.databind.ObjectMapper;  
import software.amazon.awssdk.services.dynamodb.DynamoDbClient;  
import software.amazon.awssdk.services.dynamodb.model.AttributeValue;  
import software.amazon.awssdk.services.dynamodb.model.GetItemRequest;  
 
import java.util.List;  
import java.util.Map;  
 
public class DynamoDbDeserializerExample {  
    public static void main(String[] args) {  
        DynamoDbClient dynamoDbClient = DynamoDbClient.create();  
        ObjectMapper objectMapper = new ObjectMapper();  
 
        // 1. Retrieve the item from DynamoDB  
        GetItemRequest request = GetItemRequest.builder()  
            .tableName("Users")  
            .key(Map.of("userId", AttributeValue.builder().s("user123").build()))  
            .build();  
 
        Map<String, AttributeValue> item = dynamoDbClient.getItem(request).item();  
 
        // 2. Extract the raw "tasks" list (List<LinkedHashMap>)  
        List<Map<String, Object>> rawTasks = item.get("tasks").l().stream()  
            .map(AttributeValue::m) // Convert each AttributeValue to a Map  
            .collect(Collectors.toList());  
 
        // 3. Deserialize to List<Task> using TypeReference  
        List<Task> tasks = objectMapper.convertValue(  
            rawTasks,  
            new TypeReference<List<Task>>() {}  
        );  
 
        // 4. Verify the result  
        tasks.forEach(task -> System.out.println(task.getId() + ": " + task.getDescription()));  
        // Output:  
        // task1: Buy groceries  
        // task2: Finish blog post  
    }  
}  

Conclusion#

Deserializing LinkedHashMap lists to List<CustomObject> in DynamoDB and Jackson is resolved by explicitly providing type information. The key solutions are:

  • Use ObjectMapper.convertValue with TypeReference for ad-hoc deserialization.
  • Annotate fields with @JsonDeserialize(contentAs = CustomObject.class) for nested lists.
  • Leverage @DynamoDBTypeConvertedJson for DynamoDB-specific type safety.

By avoiding common pitfalls like missing constructors or raw types, you’ll ensure Jackson correctly maps DynamoDB’s LinkedHashMap lists to your custom objects.

References#