# Using DynamoDB DocumentClient with Native JavaScript Types Instead of AttributeValues

> Learn how to use DynamoDB DocumentClient with native JavaScript types. Eliminate manual type conversion with automatic marshaling and unmarshaling.

- Repository: [Amazon Web Services/agent-toolkit-for-aws](https://github.com/aws/agent-toolkit-for-aws)
- Tags: how-to-guide
- Published: 2026-06-29

---

**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`](https://github.com/aws/agent-toolkit-for-aws/blob/main/skills/core-skills/aws-sdk-js-v3-usage/references/dynamodb.md) (lines 7-11), the construction pattern looks like this:

```javascript
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:

- **String** → `S` (String)
- **Number** → `N` (Number, converted to string representation)
- **Boolean** → `BOOL` (Boolean)
- **Object/Array** → `M` (Map) or `L` (List), recursively marshalled
- **Set** → `SS` (String Set), `NS` (Number Set), or `BS` (Binary Set)
- **Buffer/Uint8Array** → `B` (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.

```javascript
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:

```javascript
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):

```javascript
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:

```javascript
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:

```javascript
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):

```javascript
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.