Serialization¶
Checkpointed data transformation¶
The SDK serializes durable operation results to checkpoint storage. When your durable function replays the SDK uses those stored results rather than re-running the code wrapped inside the operation. A SerDes (serializer/deserializer) is the object that serializes the operation result, and deserializes it during replay.
Each SDK ships a default SerDes that handles the most common types. You only need a custom SerDes when the default SerDes cannot handle your data types, or when you need special behavior such as encryption, compression or writing to external storage.
The following example uses no SerDes configuration. The SDK serializes and deserializes the step result automatically.
import { DurableContext, withDurableExecution } from "@aws/durable-execution-sdk-js";
interface Order {
id: string;
total: string;
}
export const handler = withDurableExecution(
async (event: unknown, context: DurableContext) => {
// No SerDes config — the SDK serializes and deserializes the result automatically.
const order = await context.step("fetch-order", async (): Promise<Order> => {
return { id: "order-123", total: "99.99" };
});
return order;
},
);
from aws_durable_execution_sdk_python import (
DurableContext,
StepContext,
durable_execution,
durable_step,
)
@durable_step
def fetch_order(ctx: StepContext) -> dict:
return {"id": "order-123", "total": "99.99"}
@durable_execution
def handler(event: dict, context: DurableContext) -> dict:
# No SerDes config — the SDK serializes and deserializes the result automatically.
order = context.step(fetch_order())
return order
import java.util.Map;
import software.amazon.lambda.durable.DurableContext;
import software.amazon.lambda.durable.DurableHandler;
import software.amazon.lambda.durable.StepContext;
public class Walkthrough extends DurableHandler<Object, Map<String, String>> {
@Override
public Map<String, String> handleRequest(Object input, DurableContext context) {
// No SerDes config — the SDK serializes and deserializes the result automatically.
Map<String, String> order = context.step(
"fetch-order",
Map.class,
(StepContext ctx) -> Map.of("id", "order-123", "total", "99.99")
);
return order;
}
}
Lambda handler serialization¶
The Durable Execution SDK SerDes only applies to durable operation results. It does not affect the final return value of your Lambda handler, which Lambda serializes separately.
Lambda serializes handler return values with JSON.stringify. The same types that work
with defaultSerdes work as handler return values.
Lambda serializes handler return values with json.dumps. Types like datetime and
Decimal are safe inside steps because ExtendedTypeSerDes handles them, but returning
them directly from your handler raises
TypeError: Object of type X is not JSON serializable. This is a common error when
working with Amazon DynamoDB results. Convert those values to JSON-safe types before
returning from the handler.
Lambda serializes handler return values with Jackson. The same JacksonSerDes
configuration applies to both step results and handler return values when you use
DurableHandler.
Default serialization¶
Each SDK uses a default SerDes when you do not provide one.
The default is defaultSerdes, which uses JSON.stringify to serialize and
JSON.parse to deserialize. It handles any value that JSON.stringify accepts.
The default is ExtendedTypeSerDes. It uses plain JSON for primitives (None, str,
int, float, bool, and lists of primitives) and an envelope format for everything
else. The envelope format preserves the exact Python type through the round-trip.
Supported types beyond primitives: datetime, date, Decimal, UUID, bytes,
bytearray, memoryview, tuple, list, dict, and BatchResult.
The default is JacksonSerDes, which uses Jackson's ObjectMapper. It supports Java 8
time types, serializes dates as ISO-8601 strings, and ignores unknown properties during
deserialization.
Pass a custom ObjectMapper to the JacksonSerDes constructor to override the default
configuration.
SerDes interface definition¶
import { Serdes, SerdesContext } from "@aws/durable-execution-sdk-js";
// Serdes<T> interface
interface SerdesInterface<T> {
serialize: (value: T | undefined, context: SerdesContext) => Promise<string | undefined>;
deserialize: (data: string | undefined, context: SerdesContext) => Promise<T | undefined>;
}
// SerdesContext
interface SerdesContextShape {
entityId: string;
durableExecutionArn: string;
}
Parameters:
serializeAn async function that receives the value and aSerdesContext, and returnsPromise<string | undefined>.deserializeAn async function that receives the serialized string and aSerdesContext, and returnsPromise<T | undefined>.
Both methods are async so that implementations can interact with external services such as S3 or KMS.
SerdesContext fields:
entityIdThe operation ID for the current step or operation.durableExecutionArnThe ARN of the current durable execution.
from abc import ABC, abstractmethod
from dataclasses import dataclass
from typing import Generic, TypeVar
T = TypeVar("T")
@dataclass(frozen=True)
class SerDesContext:
operation_id: str = ""
durable_execution_arn: str = ""
class SerDes(ABC, Generic[T]):
@abstractmethod
def serialize(self, value: T, serdes_context: SerDesContext) -> str: ...
@abstractmethod
def deserialize(self, data: str, serdes_context: SerDesContext) -> T: ...
Parameters:
serialize(value, serdes_context)Converts the value to a string.deserialize(data, serdes_context)Converts the string back to the original type.
SerDesContext fields:
operation_idThe operation ID for the current step or operation.durable_execution_arnThe ARN of the current durable execution.
import software.amazon.lambda.durable.TypeToken;
import software.amazon.lambda.durable.serde.SerDes;
// SerDes interface
interface SerDesInterface {
String serialize(Object value);
<T> T deserialize(String data, TypeToken<T> typeToken);
}
Parameters:
serialize(Object value)Converts the value to a JSON string. Returnsnullifvalueisnull.deserialize(String data, TypeToken<T> typeToken)Converts the JSON string back to typeT. Returnsnullifdataisnull.
Use TypeToken<T> to capture generic type information that Java erases at runtime. For
example: new TypeToken<List<String>>() {}.
Custom SerDes example¶
Implement the SerDes interface when the default cannot handle your types, or when you need special behavior such as encryption or compression.
import { Serdes, SerdesContext } from "@aws/durable-execution-sdk-js";
interface Order {
id: string;
total: string;
}
const orderSerdes: Serdes<Order> = {
serialize: async (value: Order | undefined, _ctx: SerdesContext) => {
if (value === undefined) return undefined;
return JSON.stringify(value);
},
deserialize: async (data: string | undefined, _ctx: SerdesContext) => {
if (data === undefined) return undefined;
return JSON.parse(data) as Order;
},
};
import com.fasterxml.jackson.databind.ObjectMapper;
import software.amazon.lambda.durable.TypeToken;
import software.amazon.lambda.durable.exception.SerDesException;
import software.amazon.lambda.durable.serde.SerDes;
public class OrderSerDes implements SerDes {
private final ObjectMapper mapper = new ObjectMapper();
@Override
public String serialize(Object value) {
try {
return mapper.writeValueAsString(value);
} catch (Exception e) {
throw new SerDesException("Serialization failed", e);
}
}
@Override
public <T> T deserialize(String data, TypeToken<T> typeToken) {
try {
return mapper.readValue(data, mapper.getTypeFactory().constructType(typeToken.getType()));
} catch (Exception e) {
throw new SerDesException("Deserialization failed", e);
}
}
}
Custom SerDes on durable operations¶
Pass a SerDes instance in the configuration object for the operation you want to customize. The SDK uses that SerDes for that operation only. Other operations in the same handler continue to use the default.
StepConfig¶
import {
DurableContext,
Serdes,
SerdesContext,
StepConfig,
withDurableExecution,
} from "@aws/durable-execution-sdk-js";
interface Order {
id: string;
total: string;
}
const orderSerdes: Serdes<Order> = {
serialize: async (value: Order | undefined, _ctx: SerdesContext) =>
value !== undefined ? JSON.stringify(value) : undefined,
deserialize: async (data: string | undefined, _ctx: SerdesContext) =>
data !== undefined ? (JSON.parse(data) as Order) : undefined,
};
const stepConfig: StepConfig<Order> = { serdes: orderSerdes };
export const handler = withDurableExecution(
async (event: unknown, context: DurableContext) => {
const order = await context.step(
"fetch-order",
async (): Promise<Order> => ({ id: "order-123", total: "99.99" }),
stepConfig,
);
return order;
},
);
import json
from aws_durable_execution_sdk_python import (
DurableContext,
StepContext,
durable_execution,
durable_step,
)
from aws_durable_execution_sdk_python.config import StepConfig
from aws_durable_execution_sdk_python.serdes import SerDes, SerDesContext
class OrderSerDes(SerDes[dict]):
def serialize(self, value: dict, ctx: SerDesContext) -> str:
return json.dumps(value)
def deserialize(self, data: str, ctx: SerDesContext) -> dict:
return json.loads(data)
@durable_step
def fetch_order(ctx: StepContext) -> dict:
return {"id": "order-123", "total": "99.99"}
@durable_execution
def handler(event: dict, context: DurableContext) -> dict:
order = context.step(
fetch_order(),
config=StepConfig(serdes=OrderSerDes()),
)
return order
import java.util.Map;
import software.amazon.lambda.durable.DurableContext;
import software.amazon.lambda.durable.DurableHandler;
import software.amazon.lambda.durable.StepContext;
import software.amazon.lambda.durable.config.StepConfig;
import software.amazon.lambda.durable.serde.JacksonSerDes;
public class StepConfigExample extends DurableHandler<Object, Map<String, String>> {
@Override
public Map<String, String> handleRequest(Object input, DurableContext context) {
StepConfig config = StepConfig.builder()
.serDes(new JacksonSerDes())
.build();
Map<String, String> order = context.step(
"fetch-order",
Map.class,
(StepContext ctx) -> Map.of("id", "order-123", "total", "99.99"),
config
);
return order;
}
}
CallbackConfig¶
The callback SerDes controls how the SDK deserializes the payload that an external system sends when it completes the callback.
In TypeScript, the callback SerDes only needs deserialize. The serialize method is
not used because the external system provides the payload directly.
import {
DurableContext,
Serdes,
SerdesContext,
withDurableExecution,
} from "@aws/durable-execution-sdk-js";
interface ApprovalResult {
approved: boolean;
reason: string;
}
// Callbacks only need deserialization — the external system sends the payload.
const approvalSerdes: Omit<Serdes<ApprovalResult>, "serialize"> = {
deserialize: async (data: string | undefined, _ctx: SerdesContext) =>
data !== undefined ? (JSON.parse(data) as ApprovalResult) : undefined,
};
export const handler = withDurableExecution(
async (event: unknown, context: DurableContext) => {
const [approval, callbackId] = context.createCallback("await-approval", {
serdes: approvalSerdes,
});
// Send callbackId to the external system here.
console.log("Callback ID:", callbackId);
const result = await approval;
return result;
},
);
import json
from aws_durable_execution_sdk_python import (
DurableContext,
durable_execution,
)
from aws_durable_execution_sdk_python.config import CallbackConfig
from aws_durable_execution_sdk_python.serdes import SerDes, SerDesContext
class ApprovalSerDes(SerDes[dict]):
def serialize(self, value: dict, ctx: SerDesContext) -> str:
return json.dumps(value)
def deserialize(self, data: str, ctx: SerDesContext) -> dict:
return json.loads(data)
@durable_execution
def handler(event: dict, context: DurableContext) -> dict:
callback = context.create_callback(
"await-approval",
config=CallbackConfig(serdes=ApprovalSerDes()),
)
# Send callback.callback_id to the external system here.
result = callback.result()
return result
import java.util.Map;
import software.amazon.lambda.durable.DurableCallbackFuture;
import software.amazon.lambda.durable.DurableContext;
import software.amazon.lambda.durable.DurableHandler;
import software.amazon.lambda.durable.config.CallbackConfig;
import software.amazon.lambda.durable.serde.JacksonSerDes;
public class CallbackConfigExample extends DurableHandler<Object, Map<String, Object>> {
@Override
public Map<String, Object> handleRequest(Object input, DurableContext context) {
CallbackConfig config = CallbackConfig.builder()
.serDes(new JacksonSerDes())
.build();
DurableCallbackFuture<Map<String, Object>> callback =
context.createCallback("await-approval", Map.class, config);
// Send callback.getCallbackId() to the external system here.
return callback.get();
}
}
MapConfig & ParallelConfig¶
Map and parallel perations support two SerDes fields that apply at different levels.
import {
BatchResult,
DurableContext,
MapConfig,
Serdes,
SerdesContext,
withDurableExecution,
} from "@aws/durable-execution-sdk-js";
interface ProcessedItem {
id: string;
status: string;
}
const itemSerdes: Serdes<ProcessedItem> = {
serialize: async (value: ProcessedItem | undefined, _ctx: SerdesContext) =>
value !== undefined ? JSON.stringify(value) : undefined,
deserialize: async (data: string | undefined, _ctx: SerdesContext) =>
data !== undefined ? (JSON.parse(data) as ProcessedItem) : undefined,
};
const mapConfig: MapConfig<string, ProcessedItem> = {
itemSerdes,
};
export const handler = withDurableExecution(
async (event: unknown, context: DurableContext) => {
const items = ["a", "b", "c"];
const result: BatchResult<ProcessedItem> = await context.map(
"process-items",
items,
async (_ctx, item) => ({ id: item, status: "done" }),
mapConfig,
);
return result.getResults();
},
);
itemSerdesserializes each item result.serdesserializes the aggregatedBatchResult.ParallelConfighas the same two fields.
import json
from aws_durable_execution_sdk_python import (
DurableContext,
durable_execution,
)
from aws_durable_execution_sdk_python.config import MapConfig
from aws_durable_execution_sdk_python.serdes import SerDes, SerDesContext
class ItemSerDes(SerDes[dict]):
def serialize(self, value: dict, ctx: SerDesContext) -> str:
return json.dumps(value)
def deserialize(self, data: str, ctx: SerDesContext) -> dict:
return json.loads(data)
@durable_execution
def handler(event: dict, context: DurableContext) -> list:
items = ["a", "b", "c"]
result = context.map(
items,
lambda ctx, item, idx, arr: {"id": item, "status": "done"},
config=MapConfig(item_serdes=ItemSerDes()),
)
return result.get_results()
item_serdesserializes each item result.serdesserializes the aggregatedBatchResult.- When you provide only
serdes, the SDK uses it for both for backward compatibility.ParallelConfighas the same two fields.
import java.util.List;
import java.util.Map;
import software.amazon.lambda.durable.DurableContext;
import software.amazon.lambda.durable.DurableHandler;
import software.amazon.lambda.durable.config.MapConfig;
import software.amazon.lambda.durable.model.MapResult;
import software.amazon.lambda.durable.serde.JacksonSerDes;
public class MapConfigExample extends DurableHandler<Object, List<Map<String, String>>> {
@Override
public List<Map<String, String>> handleRequest(Object input, DurableContext context) {
MapConfig config = MapConfig.builder()
.serDes(new JacksonSerDes())
.build();
List<String> items = List.of("a", "b", "c");
MapResult<Map<String, String>> result = context.map(
"process-items",
items,
Map.class,
(item, index, ctx) -> Map.of("id", item, "status", "done"),
config
);
return result.succeeded();
}
}
serDesapplies to each item result.- Java
ParallelConfigdoes not have aserDesfield.
Built-in SerDes helpers¶
createClassSerdes(cls) creates a Serdes<T> that preserves class instances through
the round-trip. It deserializes by calling Object.assign(new cls(), JSON.parse(data)),
so class methods are available on the deserialized value. The constructor must take no
required parameters. Private fields and getters are not preserved.
import {
DurableContext,
createClassSerdes,
withDurableExecution,
} from "@aws/durable-execution-sdk-js";
class Order {
id: string = "";
total: string = "";
label() {
return `Order ${this.id} — ${this.total}`;
}
}
const orderSerdes = createClassSerdes(Order);
export const handler = withDurableExecution(
async (event: unknown, context: DurableContext) => {
const order = await context.step(
"fetch-order",
async () => Object.assign(new Order(), { id: "order-123", total: "99.99" }),
{ serdes: orderSerdes },
);
// order.label() works — class methods are preserved after deserialization
return order.label();
},
);
PassThroughSerDes stores the value as-is (the value must already be a string).
JsonSerDes uses json.dumps and json.loads without the envelope format that
ExtendedTypeSerDes adds for complex types.
from aws_durable_execution_sdk_python import (
DurableContext,
StepContext,
durable_execution,
durable_step,
)
from aws_durable_execution_sdk_python.config import StepConfig
from aws_durable_execution_sdk_python.serdes import JsonSerDes, PassThroughSerDes
# PassThroughSerDes — stores the value as-is (must already be a string)
# JsonSerDes — standard json.dumps / json.loads, no envelope format
@durable_step
def fetch_raw(ctx: StepContext) -> str:
return '{"id":"order-123"}'
@durable_execution
def handler(event: dict, context: DurableContext) -> str:
raw = context.step(
fetch_raw(),
config=StepConfig(serdes=PassThroughSerDes()),
)
return raw
The built-in JacksonSerDes handles class instances via Jackson's ObjectMapper. You
can create your own passthrough SerDes like this:
import software.amazon.lambda.durable.DurableContext;
import software.amazon.lambda.durable.DurableHandler;
import software.amazon.lambda.durable.StepContext;
import software.amazon.lambda.durable.TypeToken;
import software.amazon.lambda.durable.config.StepConfig;
import software.amazon.lambda.durable.exception.SerDesException;
import software.amazon.lambda.durable.serde.SerDes;
// A pass-through SerDes stores the value as-is (already a JSON string).
class PassThroughSerDes implements SerDes {
@Override
public String serialize(Object value) {
return (String) value;
}
@Override
public <T> T deserialize(String data, TypeToken<T> typeToken) {
@SuppressWarnings("unchecked")
T result = (T) data;
return result;
}
}
public class PassThroughSerdesExample extends DurableHandler<Object, String> {
@Override
public String handleRequest(Object input, DurableContext context) {
StepConfig config = StepConfig.builder()
.serDes(new PassThroughSerDes())
.build();
return context.step(
"fetch-raw",
String.class,
(StepContext ctx) -> "{\"id\":\"order-123\"}",
config
);
}
}
Serialization errors¶
When serialization or deserialization fails, each SDK raises or throws a specific exception type. See Serialization errors for the exception hierarchy and how to handle serialization failures.
See also¶
- StepConfig Step serialization configuration
- CallbackConfig Callback serialization configuration
- MapConfig Map serialization configuration
- Serialization errors Serialization error types