AI SDK v5 Migration: Handling Previously Saved Messages
Prerequisites: This article builds upon the AI SDK v5 Migration Guide. Ensure you've completed the core migration before implementing this solution.
Who should read this article?
This article is only relevant if your application saves messages and you wish to migrate to the v5 AI SDK. If you don't save messages, you can skip this article. If you wish to begin saving messages, you can follow the guide here.
The Problem
Picture this: You've just upgraded to AI SDK v5, excited about the new features. You deploy to production, and suddenly your users can't access their chat history. Their saved conversations throw errors, the UI breaks, and you're getting support tickets.
The breaking change: AI SDK v5 introduced a completely new message structure. Your existing saved messages in v4 format are incompatible with the new SDK, and there's no automatic migration path.
You don't want to lose users' valuable chat history, break existing conversations in production, force users to start over with empty chat sessions, or spend weeks manually migrating database records.
Key Changes from v4 to v5
Understanding these structural changes is crucial for building an effective transformer:
Aspect | V4 | V5 | Documentation |
---|---|---|---|
Message Structure | content property | parts array structure | UIMessage Changes |
Tool Handling | Separate toolInvocations array | Individual parts in parts array | Tool Call Changes |
Tool Types | Generic tool-invocation | Specific tool-{toolName} types | Tool Call Changes |
State Management | Basic states: partial-call call result error | Granular states: input-streaming input-available output-available output-error | Tool Call Changes |
Property Names | args , result | input output | Tool Call Changes |
Reasoning | Top-level reasoning property | reasoning parts with text property | UIMessage Reasoning Structure |
Reasoning Parts | part.reasoning property | part.text property | UIMessage Reasoning Structure |
File Parts | data mimeType properties | url , mediaType properties | File Part Changes |
High-Level Migration Overview
Follow this step-by-step approach to safely migrate without breaking existing chats:
1. Upgrade to AI SDK v5
Install the new AI SDK v5 packages and update your codebase. See the migration guide for details.
Pro tip: Create a new branch, copy the official migration guide, and use AI tools like Cursor or Cody to help automate the migration. However, understanding your specific message object shape is crucial for the transformer.
2. Update Chat Components Refactor your chat and message-related components (reasoning, tool calls, file/data parts, etc.) to align with the new v5 message shape.
3. Implement the Message Transformer Call the transformer function to convert legacy messages at runtime, ensuring users can access their existing chats immediately after the upgrade.
4. Testing Validate against: historical messages, reasoning traces, tool calls, data parts, and file parts. Fix any issues that arise.
5. Future Migration Path (Optional) Once thoroughly tested, use the same transformer logic to migrate your stored messages in the database to the v5 format permanently.
Message Transformer Implementation
This transformer is designed to be simple and easy to understand, so you can modify it and use AI tools to adapt it to your specific requirements.
// You should use your type for v5 SDK https://ai-sdk.dev/docs/reference/ai-sdk-core/ui-message
interface MessagePart {
type: string;
[key: string]: unknown;
}
interface UIMessage {
id: string;
role: "user" | "assistant" | "system";
parts: MessagePart[];
metadata?: Record<string, unknown>;
}
// State mapping for tool calls
const STATE_MAP = {
"partial-call": "input-streaming",
call: "input-available",
result: "output-available",
error: "output-error",
} as const;
export function autoTransformMessages(messages: any[]): UIMessage[] {
return messages.map((msg, i) => {
// Check if message needs transformation
const needsTransform =
msg?.toolInvocations ||
msg?.parts?.some((p: any) => p.type === "tool-invocation") ||
msg?.role === "data" ||
msg?.reasoning ||
(msg?.content && !msg?.parts);
if (!needsTransform) return msg;
const parts: MessagePart[] = [];
// Top-level reasoning β reasoning part
if (msg.reasoning) {
parts.push({ type: "reasoning", text: msg.reasoning });
}
// Transform existing parts
for (const part of msg.parts || []) {
switch (part.type) {
case "tool-invocation":
const inv = part.toolInvocation;
parts.push({
type: `tool-${inv.toolName}`,
toolCallId: inv.toolCallId,
state: STATE_MAP[inv.state as keyof typeof STATE_MAP] || "output-available",
input: inv.args,
output: inv.result,
});
break;
case "reasoning":
parts.push({ type: "reasoning", text: part.reasoning || part.text });
break;
case "file":
const url = part.url || `data:${part.mimeType || part.mediaType};base64,${part.data}`;
parts.push({
type: "file",
url,
mediaType: part.mediaType || part.mimeType,
...(part.name && { filename: part.name }),
});
break;
default:
parts.push(part); // Pass through unchanged
}
}
// Fallback: plain content β text part
if (!parts.length && msg.content) {
parts.push({ type: "text", text: msg.content });
}
// Handle legacy 'data' role
let role = msg.role;
if (msg.role === "data") {
parts.push({ type: "data-legacy", payload: msg.data || msg.content });
role = "system";
}
return {
id: msg.id || `msg-${i}`,
role,
parts,
};
});
}
Usage & Implementation
Integration with useChat Hook
// Call the transformer before initializing the chat
const chat = useChat<ChatMessage>({
id: id,
initialMessages: autoTransformMessages(savedMessages),
});
// Or you can set messages
const transformedMessages = autoTransformMessages(chatData.messages);
setMessages(transformedMessages as UIMessage[]);
Versioning Strategy (Recommended)
Add explicit version tags to messages for cleaner detection:
// Tag new messages with version
const newMessage = {
...messageData,
messageVersion: "5.0.0"
};
// In your transformer, check version first
function needsTransformation(msg: any): boolean {
// If no version tag, assume v4
if (!msg.messageVersion) return true;
// Only transform pre-v5 messages
return msg.messageVersion < "5.0.0";
}
This approach is more reliable than inferring versions from message shape.
Detailed Transformation Examples
These examples show how the transformer handles different message types:
1. Content β Parts Array Migration (UIMessage Changes)
AI SDK 4.0:
[
{
"id": "1",
"role": "user",
"content": "Bonjour!"
}
]
AI SDK 5.0:
[
{
"id": "1",
"role": "user",
"parts": [
{
"type": "text",
"text": "Bonjour!"
}
]
}
]
2. Reasoning Structure Migration (UIMessage Reasoning Structure)
AI SDK 4.0:
[
{
"role": "assistant",
"content": "Hello",
"reasoning": "I will greet the user"
}
]
AI SDK 5.0:
[
{
"role": "assistant",
"parts": [
{
"type": "reasoning",
"text": "I will greet the user"
}
]
}
]
3. Complex Tool Call with Multiple States
AI SDK 4.0:
[
{
"toolInvocations": [
{
"args": {
"order": "asc",
"endDate": "2024-04-30",
"type": "EXPENSE",
"orderBy": "timestamp",
"startDate": "2024-04-01"
},
"toolCallId": "call_zmJWPPkej6kOpSxuqOZaTKTV",
"state": "result",
"toolName": "queryDatabaseBasedOnUserInput",
"step": 0,
"result": []
}
],
"parts": [
{
"type": "step-start"
},
{
"type": "tool-invocation",
"toolInvocation": {
"state": "result",
"step": 0,
"args": {
"order": "asc",
"startDate": "2024-04-01",
"orderBy": "timestamp",
"type": "EXPENSE",
"endDate": "2024-04-30"
},
"toolName": "queryDatabaseBasedOnUserInput",
"result": {
"totalExpenses": 2450.75,
"categories": [
{
"name": "Food",
"amount": 850.25
},
{
"name": "Transport",
"amount": 320.5
},
{
"name": "Utilities",
"amount": 1280
}
],
"month": "2024-04"
},
"toolCallId": "call_zmJWPPkej6kOpSxuqOZaTKTV"
}
},
{
"type": "step-start"
},
{
"type": "text",
"text": "It looks like there are no recorded expenses for April 2024 in your \"Demo\" workspace. If you need help with anything else, just let me know! π"
}
],
"role": "assistant",
"id": "msg-JyOgfoGzhSRcQUikvbiD3NCQ",
"content": "It looks like there are no recorded expenses for April 2024 in your \"Demo\" workspace. If you need help with anything else, just let me know! π",
"createdAt": {
"seconds": 1744677521,
"nanoseconds": 435000000
}
}
]
AI SDK 5.0:
[
{
"id": "msg-JyOgfoGzhSRcQUikvbiD3NCQ",
"role": "assistant",
"parts": [
{
"type": "step-start"
},
{
"type": "tool-queryDatabaseBasedOnUserInput",
"toolCallId": "call_zmJWPPkej6kOpSxuqOZaTKTV",
"state": "output-available",
"input": {
"order": "asc",
"startDate": "2024-04-01",
"orderBy": "timestamp",
"type": "EXPENSE",
"endDate": "2024-04-30"
},
"output": {
"totalExpenses": 2450.75,
"categories": [
{
"name": "Food",
"amount": 850.25
},
{
"name": "Transport",
"amount": 320.5
},
{
"name": "Utilities",
"amount": 1280
}
],
"month": "2024-04"
}
},
{
"type": "step-start"
},
{
"type": "text",
"text": "It looks like there are no recorded expenses for April 2024 in your \"Demo\" workspace. If you need help with anything else, just let me know! π"
},
{
"type": "tool-queryDatabaseBasedOnUserInput",
"toolCallId": "call_zmJWPPkej6kOpSxuqOZaTKTV",
"state": "output-available",
"input": {
"order": "asc",
"endDate": "2024-04-30",
"type": "EXPENSE",
"orderBy": "timestamp",
"startDate": "2024-04-01"
},
"output": []
}
]
}
]
Future Migration Path
Once your transformer is battle-tested in production, you can optionally use it to permanently migrate your database:
β οΈ Safety First: Always test in a sandbox database first. Migrate in small batches at a comfortable pace to avoid creating additional problems.
// Example database migration script
async function migrateDatabaseMessages() {
const allChats = await db.chats.findAll();
for (const chat of allChats) {
const transformedMessages = autoTransformMessages(chat.messages);
await db.chats.update(chat.id, {
messages: transformedMessages,
messageVersion: "5.0.0"
});
}
}
Conclusion
Thanks for reading, I enjoyed writing this article and I hope it helps you on your journey. If you have any questions, please feel free to reach out to me on Twitter (X) π.
I plan to keep this article updated and update the transformer as needed.
Huge thanks to the AI SDK team, and all contributors! We truly have an amazing community and I'm happy to be a part of it.
Stay Shipping like Next Day Delivery π