Using DynamoDB DocumentClient with Native JavaScript Types Instead of AttributeValues

The @aws-sdk/lib-dynamodb DocumentClient automatically marshals native JavaScript values to DynamoDB AttributeValues and unmarshals responses back to plain objects, eliminating manual type conversion.

The aws/agent-toolkit-for-aws repository demonstrates modern patterns for interacting with Amazon DynamoDB using the AWS SDK for JavaScript v3. Instead of manually constructing verbose AttributeValue objects (like { S: "string" } or { N: "123" }), you can use the DynamoDB DocumentClient to work directly with native JavaScript types including strings, numbers, booleans, Sets, and Buffers.

How the DocumentClient Works

The DocumentClient acts as a wrapper around the low-level DynamoDBClient. When you call DynamoDBDocumentClient.from(), the SDK injects a marshaller and unmarshaller that handle all translation between JavaScript types and DynamoDB's wire format.

According to the source code in skills/core-skills/aws-sdk-js-v3-usage/references/dynamodb.md (lines 7-11), the construction pattern looks like this:

import { DynamoDBClient } from "@aws-sdk/client-dynamodb";
import { DynamoDBDocumentClient } from "@aws-sdk/lib-dynamodb";

const ddbDoc = DynamoDBDocumentClient.from(new DynamoDBClient({ region: "us-east-1" }));

The wrapper intercepts your native JavaScript objects before they reach the network layer, converting them to the { S: ..., N: ..., BOOL: ... } format that DynamoDB requires, then reverses the process for responses.

Type Mapping Between JavaScript and DynamoDB

The DocumentClient handles automatic type conversion for all common JavaScript primitives. As documented in the reference file (lines 32-43), the mapping follows these rules:

  • StringS (String)
  • NumberN (Number, converted to string representation)
  • BooleanBOOL (Boolean)
  • Object/ArrayM (Map) or L (List), recursively marshalled
  • SetSS (String Set), NS (Number Set), or BS (Binary Set)
  • Buffer/Uint8ArrayB (Binary)

This automatic mapping eliminates the need to wrap every value in type descriptors, making your code significantly more readable and maintainable.

Configuring Marshall Options

When constructing the DocumentClient, you can customize how the marshaller handles edge cases. The marshallOptions and unmarshallOptions parameters (shown in lines 48-58 of the reference) control behavior for undefined values, empty strings, and number precision.

const client = DynamoDBDocumentClient.from(new DynamoDBClient({}), {
  marshallOptions: {
    removeUndefinedValues: true,   // Remove undefined keys from items
    convertEmptyValues: false,     // Preserve empty strings instead of converting to null
    allowImpreciseNumbers: true,   // Accept numbers beyond MAX_SAFE_INTEGER
  },
  unmarshallOptions: {
    wrapNumbers: true, // Return NumberValue objects instead of plain numbers
  },
});

Setting removeUndefinedValues: true is particularly important because DynamoDB does not accept undefined values in items, and the SDK will throw an error unless you explicitly configure this option to strip them.

Handling Large Numbers with NumberValue

JavaScript's Number type cannot safely represent integers larger than Number.MAX_SAFE_INTEGER (9,007,199,254,740,991). To handle high-precision numbers, the DocumentClient provides the NumberValue class.

As demonstrated in lines 62-71 of the reference file, you can preserve precision by wrapping large numbers:

import { NumberValue, PutCommand } from "@aws-sdk/lib-dynamodb";

await client.send(
  new PutCommand({
    TableName: "MyTable",
    Item: {
      id: "1",
      bigNum: NumberValue.from("1000000000000000000000.000000001"),
    },
  })
);

When unmarshallOptions.wrapNumbers is enabled, the client returns NumberValue instances for all numeric fields, allowing you to convert them to strings or BigInts as needed without losing precision.

Pagination with Async Iterators

The DocumentClient simplifies pagination through helper functions that expose async iterators. Instead of manually handling LastEvaluatedKey and recursive calls, you can use paginateScan or paginateQuery (lines 82-89):

import { paginateScan } from "@aws-sdk/lib-dynamodb";

for await (const page of paginateScan({ client }, { TableName: "MyTable", Limit: 100 })) {
  console.log("Page items:", page.Items);
}

This pattern automatically retrieves subsequent pages when the iterator is consumed, yielding each page of results as native JavaScript objects.

Practical Implementation Examples

Basic CRUD with Native Types

The following example from the repository (lines 13-27) demonstrates full CRUD operations without any manual marshalling:

import { DynamoDBClient } from "@aws-sdk/client-dynamodb";
import {
  DynamoDBDocumentClient,
  GetCommand,
  PutCommand,
  QueryCommand,
  DeleteCommand,
} from "@aws-sdk/lib-dynamodb";

const client = DynamoDBDocumentClient.from(new DynamoDBClient({ region: "us-east-1" }));

// Create / Update using native types
await client.send(
  new PutCommand({
    TableName: "MyTable",
    Item: { 
      id: "1", 
      name: "Alice", 
      age: 30, 
      active: true, 
      tags: new Set(["admin", "user"]) 
    },
  })
);

// Retrieve
const { Item } = await client.send(
  new GetCommand({ TableName: "MyTable", Key: { id: "1" } })
);
console.log(Item); // { id: '1', name: 'Alice', age: 30, active: true, tags: Set { 'admin', 'user' } }

// Query using native values in ExpressionAttributeValues
const { Items } = await client.send(
  new QueryCommand({
    TableName: "MyTable",
    KeyConditionExpression: "id = :id",
    ExpressionAttributeValues: { ":id": "1" },
  })
);

// Delete
await client.send(new DeleteCommand({ TableName: "MyTable", Key: { id: "1" } }));

Using the Aggregated DynamoDBDocument Client

For convenience, you can use DynamoDBDocument (lines 92-99), which combines the marshaller with a full client API in a single object:

import { DynamoDBDocument } from "@aws-sdk/lib-dynamodb";

const doc = DynamoDBDocument.from(new DynamoDBClient({}));
await doc.put({ TableName: "MyTable", Item: { id: "1" } });
const result = await doc.get({ TableName: "MyTable", Key: { id: "1" } });
console.log(result);

Resource Cleanup

Because the DocumentClient is a thin wrapper, it holds no resources itself. You only need to call destroy() on the underlying DynamoDBClient when finished (lines 102-104):

await client.destroy(); // Cleans up the underlying DynamoDBClient

Summary

  • Automatic marshalling: The DynamoDBDocumentClient.from() method wraps your low-level client to convert native JavaScript types to DynamoDB AttributeValues transparently.
  • Supported types: Strings, numbers, booleans, objects, arrays, Sets, and Buffers are automatically mapped to their DynamoDB equivalents.
  • Configuration options: Use marshallOptions to handle undefined values, empty strings, and imprecise numbers according to your application's needs.
  • Large number support: The NumberValue class preserves precision for integers beyond Number.MAX_SAFE_INTEGER.
  • Simplified pagination: Async iterator helpers like paginateScan eliminate manual pagination logic.
  • Resource management: Always call destroy() on the underlying DynamoDBClient, not the DocumentClient wrapper.

Frequently Asked Questions

What is the difference between DynamoDBClient and DynamoDBDocumentClient?

The DynamoDBClient is the low-level client that requires you to format data as AttributeValue objects (e.g., { S: "value" }). The DynamoDBDocumentClient is a higher-level wrapper that automatically marshals native JavaScript types to the required format and unmarshals responses back to plain objects, making your code cleaner and reducing boilerplate.

How does DynamoDB DocumentClient handle JavaScript Sets?

The DocumentClient automatically converts JavaScript Set instances to the appropriate DynamoDB set type based on the set's contents. Set<string> becomes a String Set (SS), Set<number> becomes a Number Set (NS), and Set<Uint8Array> becomes a Binary Set (BS). When unmarshalling, these are converted back to native JavaScript Sets.

What happens to undefined values when using DynamoDB DocumentClient?

By default, the DocumentClient throws an error if you attempt to store an object containing undefined values because DynamoDB does not support undefined attributes. You must set marshallOptions.removeUndefinedValues: true when creating the client to automatically strip undefined keys from items before sending them to the service.

When should I use NumberValue instead of native JavaScript numbers?

You should use NumberValue when working with integers larger than Number.MAX_SAFE_INTEGER (9,007,199,254,740,991) or when you need to preserve exact decimal precision that JavaScript floating-point arithmetic might corrupt. Enable unmarshallOptions.wrapNumbers: true to receive all numeric values as NumberValue instances, which you can then convert to strings or BigInts without losing precision.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →