TL;DR

When you store a tool-call/tool-result UI message as it is into the database, you should remove properties with null values like rawInput, errorText, preliminary, and providerExecuted before passing it to the validateUIMessages function.

const storedUiMessages = [
  {
    type: "tool-get_something",
    toolCallId: "call_iW4QcfoKhNc4k2Hmt8BFKHHI",
    state: "output-available",
    input: { a: 1 },
    output: { b: 2 },
    rawInput: null,
    errorText: null,
    providerExecuted: null,
    preliminary: null,
    callProviderMetadata: { c: 3 },
  },
  {
    type: "text",
    text: "Hello, world!",
    state: "done",
    providerMetadata: { d: 4 },
  },
];

const cleanedUiMessages = storedUiMessages.map((m) => ({
  ...m,
  parts: m.parts.map((p) => {
    const cleanedPart: any = {};
    for (const [key, value] of Object.entries(p)) {
      if (value !== null && value !== undefined) {
        cleanedPart[key] = value;
      }
    }
    return cleanedPart;
  }),
}));

const validatedUiMessages = await validateUIMessages({
  messages: cleanedUiMessages,
  tools,
});

const modelMessages = convertToModelMessages(validatedUiMessages, {
  tools,
});

Bug hunting method

What worked:

I approached the bug in these two ways simultaneously:

  1. I read the source code of the validateUIMessages function and its tests.
  2. I asked my AI assistant (Claude 4.5 Sonnet) to create a simple JS file and try different arguments for the validateUIMessages function until it broke.

What didn’t work:

  • Randomly editing parts of the code
  • Asking my AI assistant to find the bug without any instructions on how to do it
  • Searching the web and GitHub issues (this sometimes works, but it didn’t before I wrote this post)