Table of Contents#
- Understanding the Problem: Why LinkedHashMap?
- Prerequisites
- Solutions to Deserialize LinkedHashMap to List
- Solution 1: Use
ObjectMapperwithTypeReference - [Solution 2: Custom Deserializer with
@JsonDeserialize](#solution-2-custom-deserializer-with-json deserialize) - Solution 3: DynamoDB-Specific Mappings with
@DynamoDBTypeConvertedJson
- Solution 1: Use
- Common Pitfalls to Avoid
- Practical Example: End-to-End Workflow
- Conclusion
- 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:#
- Retrieve the raw list from DynamoDB (e.g., as a
List<LinkedHashMap>or JSON string). - Use
ObjectMapperto convert the raw list toList<CustomObject>withTypeReference.
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 theList<LinkedHashMap>intoList<Task>by mapping eachLinkedHashMapentry toTaskfields (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:#
- Annotate the list field in the parent class with
@JsonDeserialize(contentAs = CustomObject.class). - 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 aTaskinstance, avoidingLinkedHashMap.
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:#
- Annotate the list field with
@DynamoDBTypeConvertedJsonand specify theTypeReference. - Use
DynamoDBMapperto 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:#
@DynamoDBTypeConvertedJsonserializesList<Task>to a JSON string in DynamoDB and deserializes it back using the specifiedTypeReference, avoidingLinkedHashMap.
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
Listin Parent Class: Avoid raw types likeList(useList<Task>instead). Raw types cause Jackson to default toLinkedHashMap. -
Null Values: Handle nulls in DynamoDB responses (e.g.,
mapper.convertValuethrowsNullPointerExceptionifrawTasksis 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.convertValuewithTypeReferencefor ad-hoc deserialization. - Annotate fields with
@JsonDeserialize(contentAs = CustomObject.class)for nested lists. - Leverage
@DynamoDBTypeConvertedJsonfor 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.