All writing

AI SDK v5 Migration: Handling Previously Saved Messages

A guide on migrating from AI SDK v4 to v5 while maintaining backward compatibility for existing chat messages stored in your database.

📋 TLDR - Quick Summary
Problem: When migrating from AI SDK v4 to v5, your existing saved chat messages will break because the message structure changed.
Solution: Implement a message transformer that automatically converts legacy v4 messages to the new v5 format at runtime.
Key Changes:
  • content property → parts array
  • toolInvocations array → individual tool parts in parts array
  • args/resultinput/output
  • reasoning property → reasoning parts with text property
  • • Tool states: partial-call/call/result/errorinput-streaming/input-available/output-available/output-error
Implementation: Use the provided autoTransformMessages() function to transform your saved messages before passing them to the v5 SDK. Use AI to help you fit the transformer for your use case and app.

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.

typescript
// 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#

typescript

// 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[]);

Add explicit version tags to messages for cleaner detection:

typescript
// 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:

typescript
[
  {
    "id": "1",
    "role": "user",
    "content": "Bonjour!"
  }
]

AI SDK 5.0:

typescript
[
  {
    "id": "1",
    "role": "user",
    "parts": [
      {
        "type": "text",
        "text": "Bonjour!"
      }
    ]
  }
]

2. Reasoning Structure Migration (UIMessage Reasoning Structure)#

AI SDK 4.0:

typescript
[
  {
    "role": "assistant",
    "content": "Hello",
    "reasoning": "I will greet the user"
  }
]

AI SDK 5.0:

typescript
[
  {
    "role": "assistant",
    "parts": [
      {
        "type": "reasoning",
        "text": "I will greet the user"
      }
    ]
  }
]

3. Complex Tool Call with Multiple States#

AI SDK 4.0:

typescript
[
  {
    "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:

typescript
[
  {
    "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.

typescript
// 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 🚀