TL;DR - Handling DynamoDB Reserved Keywords
The Problem: DynamoDB has reserved keywords (like order
, name
, status
) that can't be used directly in queries, causing AmazonDynamoDBException
errors.
The Solution: Use Expression Attribute Names as placeholders
- Replace reserved words with placeholders starting with
#
(e.g.,#ord
fororder
) - Map the placeholder to the actual attribute name
- Use in your filter expression:
#ord = :val
Example:
// Instead of: "order = :val"
// Use: "#ord = :val"
Map<String, String> expressionAttributeNames = Map.of("#ord", "order");
Best Practices:
- Always use placeholders defensively to avoid issues
- Or design tables to avoid reserved words from the start (use prefixes like
item_order
or PascalCase)
Note: The article uses scan()
which is expensive. Consider adding a Global Secondary Index (GSI) if you frequently query by a specific attribute.
When working with Amazon DynamoDB, it's easy to run into one of its many reserved keywords — words you can't use directly as attribute names in expressions.
Recently, I stumbled upon this issue while trying to query a table that had an attribute named order
.
DynamoDB didn't like that, here's how I fixed it.
The Problem
Let's say you have a DynamoDB table mapped to a Template
entity, and one of its attributes is named order
.
You might try to scan it like this (AWS SDK v1):
DynamoDBScanExpression scanExpression = new DynamoDBScanExpression()
.withFilterExpression("order = :val")
.withExpressionAttributeValues(Map.of(":val", new AttributeValue().withN("1")))
.withLimit(1);
But this will throw an AmazonDynamoDBException
complaining that order
is a reserved keyword.
DynamoDB reserves certain words (like size
, name
, order
, status
, etc.) for its own use — so you can't use them directly in filter or key expressions.
Performance Note:
scan()
is expensive and reads the entire table. If you frequently query byorder
, consider adding a Global Secondary Index (GSI) to usequery()
instead ofscan()
.
The Solution: Expression Attribute Names
The trick is to use Expression Attribute Names, which let you create a "placeholder" for your reserved word.
Here's a working version of the same method:
AWS SDK v1 (DynamoDBMapper)
public Optional getByOrder(final Integer order) {
if (order == null) {
throw new IllegalArgumentException("Order must not be null");
}
// Map for replacing reserved attribute names
final Map expressionAttributeNames =
Collections.singletonMap("#ord", "order");
// Map for attribute values
final Map expressionAttributeValues =
Collections.singletonMap(":val", new AttributeValue().withN(order.toString()));
// Define the ScanExpression with limit
final DynamoDBScanExpression scanExpression = new DynamoDBScanExpression()
.withFilterExpression("#ord = :val")
.withExpressionAttributeNames(expressionAttributeNames)
.withExpressionAttributeValues(expressionAttributeValues)
.withLimit(1);
// Execute scan and return the first result if exists
final PaginatedScanList scanResult = dynamoDBMapper.scan(Template.class, scanExpression);
return scanResult.isEmpty() ? Optional.empty() : Optional.of(scanResult.get(0));
}
AWS SDK v2
public Optional getByOrder(Integer order) {
if (order == null) {
throw new IllegalArgumentException("Order must not be null");
}
// Get the DynamoDB table reference
DynamoDbTable table = enhancedClient.table("Template", TableSchema.fromBean(Template.class));
// Build expression with attribute name placeholder to handle reserved keyword "order"
// #ord is a placeholder for the reserved word "order"
// :val is a placeholder for the actual value to compare
Expression expression = Expression.builder()
.expression("#ord = :val")
.expressionNames(Map.of("#ord", "order")) // Map placeholder to actual attribute name
.expressionValues(Map.of(":val", AttributeValue.builder().n(order.toString()).build()))
.build();
// try-with-resources ensures PageIterable is properly closed to avoid resource leaks
try (var pages = table.scan(ScanEnhancedRequest.builder()
.filterExpression(expression)
.build())) {
// Return the first matching item, if any
return pages.items().stream().findFirst();
}
}
What's Happening Here
#ord
is a placeholder for the real attribute nameorder
. You can name it anything as long as it starts with a#
. Then, in theexpressionAttributeNames
map, you define what#ord
actually represents.:val
is a placeholder for the value you're comparing against — this is standard in DynamoDB filter expressions.Finally, the filter expression
#ord = :val
safely compares theorder
field without triggering a reserved word error.
Recommended Strategies
Option 1: Always use placeholders (defensive approach)
// Even if it's not a reserved word, use placeholders
.withExpressionAttributeNames(Map.of("#attr", "myAttribute"))
Option 2: Design - Avoid reserved words from the start
- Use prefixes:
item_order
,user_status
,product_name
- PascalCase:
OrderValue
,StatusCode
Conclusion
Whenever DynamoDB throws an error like:
"Invalid KeyConditionExpression: Attribute name is a reserved keyword"
Just use Expression Attribute Names to "escape" the reserved word.
It's clean, simple, and works everywhere — filters, updates, and queries.
Photo by Alireza Akhlaghi on Unsplash
Top comments (0)