DEV Community

Cover image for DynamoDB Expression Attribute Names and Values
DynoTable
DynoTable

Posted on • Edited on • Originally published at dynotable.com

DynamoDB Expression Attribute Names and Values

DynamoDB expressions are templates: you write placeholders, then supply the real
attribute names and values in two side maps. #name is a name placeholder;
:value is a value placeholder. Get the two confused and DynamoDB rejects the
whole call.

What is the difference between #name and :value in DynamoDB?

#name is a placeholder for an attribute name, supplied through ExpressionAttributeNames; :value is a placeholder for an attribute value, supplied through ExpressionAttributeValues. Use #name to dodge reserved words, dots, or spaces, and :value for every literal — DynamoDB never inlines values. They are not interchangeable; swapping them throws a ValidationException.

  • #name substitutes an attribute name via ExpressionAttributeNames — use it whenever an attribute clashes with a reserved word or contains a dot/space.
  • :value substitutes a value via ExpressionAttributeValues — DynamoDB never inlines literals into expression text, so every value is a placeholder.
  • They are not interchangeable. A # where a : belongs is a ValidationException, not a silent no-op.

Coming from SQL, you inline both — WHERE status = 'published'. DynamoDB inlines
neither. That split is the thing that trips up every newcomer.

Why the two maps exist

In SQL the query string carries everything: column names, literals, operators.
DynamoDB deliberately separates the shape of the expression from its data.

Values go in their own map so DynamoDB can type each one (S, N, BOOL, …)
and so the parser never has to guess where a string ends — there is no quoting
or escaping to get wrong. See data types in DynamoDB for
the full type-tag list.

Names get the same treatment for a different reason: DynamoDB has a long list of
reserved words, and any attribute matching one cannot appear as a bare name
in an expression. The placeholder dodges the reservation entirely.

The reserved-word trap

Here is a CMS articles table — partition key BLOG#<blog>, sort key
ARTICLE#<slug> — whose attributes read naturally but happen to collide with
reserved words:

Attribute Reserved? What it holds
status yes draft / published
name yes author display name
size yes rendered byte length
ttl yes archive expiry (epoch)
slug no URL slug

status, name, size, and ttl are all on AWS's reserved-words list, so this
filter fails on the first word:

Example Notes
FilterExpression status = :s

DynamoDB returns a ValidationException"Attribute name is a reserved
keyword; reserved keyword: status"
. The fix is a name placeholder, never
renaming the attribute:

Example Notes
FilterExpression #status = :s
ExpressionAttributeNames { "#status": "status" }
ExpressionAttributeValues { ":s": { "S": "published" } }

The trap: slug is not reserved, so a query you tested against slug works,
and you assume the next one will too. Then status breaks it. The full list
moves, so don't memorize it — placeholder every name and you never get bitten.

Tip

You can't tell which words are reserved by looking at them — ttl and name
look harmless. The cheap, boring habit is to use a # for every attribute
name in expressions. It costs nothing and removes a whole class of error. Or
paste your attribute names into the
reserved-words checker — it flags the
collisions and hands back a ready-to-paste ExpressionAttributeNames alias map.

Map every value, always

Values are non-negotiable: there is no syntax for an inline literal. Even a plain
number gets a placeholder. This update marks an article published, stamps its
size, and sets a 30-day archive TTL:

UpdateExpression:          SET #status = :s, #size = :sz, #ttl = :exp
ExpressionAttributeNames:  { "#status": "status", "#size": "size", "#ttl": "ttl" }
ExpressionAttributeValues: {
  ":s":   { "S": "published" },
  ":sz":  { "N": "20480" },
  ":exp": { "N": "1719792000" }
}
Enter fullscreen mode Exit fullscreen mode

Note :sz and :exp are sent as N strings — DynamoDB's number type is wire-
encoded as a string. The value map is also where you reuse a value across
clauses: define :s once, reference it in both a ConditionExpression and a
FilterExpression.

Building these two maps by hand is where typos hide. The
Expression Builder generates the expression
string and both maps together, with the type tags filled in, so the placeholders
can't drift out of sync.

The builder below filters on status — a reserved word — so you can see it
auto-alias to #status in the ExpressionAttributeNames map:

Names for nested and awkward paths

The # placeholder does more than dodge reserved words. Document-path syntax uses
dots and brackets, so an attribute that literally contains a dot — say a metadata
key og.title — is unaddressable without a placeholder:

Example Notes
ProjectionExpression #og
ExpressionAttributeNames { "#og": "og.title" }

Without it, DynamoDB reads og.title as "the title field inside the og map"
— a different thing entirely. Same story for names with spaces or leading digits.
For nesting, you placeholder each segment: #meta.#author with both #meta and
#author defined.

Names vs values, side by side

#name :value
Substitutes an attribute name an attribute value
Map ExpressionAttributeNames ExpressionAttributeValues
Prefix # :
Needed for reserved words, dots, spaces always — no inline literals
Wrong one errors ValidationException ValidationException

If a value were typed as a name, DynamoDB would look for an attribute called
published and your condition would never match the way you meant — so the API
fails loud instead. That strictness is a feature: there is no quiet wrong answer.

Pitfalls and next steps

  • Declaring a placeholder you don't use — DynamoDB rejects unused entries in either map. Build the maps from the expression, not ahead of it.
  • Reusing :v after editing the expression — drop a clause and its value can linger, triggering the unused-entry error. The builder keeps them in lockstep.
  • Assuming a name is safe because it worked once — reserved-word collisions are per-attribute. Placeholder uniformly and stop guessing.

These maps show up in every write path, so they pair naturally with
single-table design and with knowing
when to Query vs Scan before you ever attach a filter.

Generate the expression plus both maps with the
Expression Builder, then
try DynoTable to run them against your own tables and watch the
placeholders resolve.

Top comments (0)