Mistral Chat Completion with tools first fail

When I tested the Mistral Vibe Code generated client for the Mistral Chat Completion API, everything looked good until I added a couple of tool calls. The very first request exploded with a runtime exception:

The Mistral Chat Completion API rejected the malformed assistant message: “Assistant message must have either content or tool_calls, but not none” This was the first clue that the polymorphic message model wasn’t being serialised correctly.

Inspecting the raw JSON request inside Visual Studio’s JSON Visualizer. The error object clearly showed the invalid assistant message structure generated by the client.

After digging through the JSON returned by Mistral and inspecting the request object in Visual Studio, I figured out that System.Text.Json wasn’t serializing the polymorphic MessageBase hierarchy correctly.

namespace MistralAI.Client.DTOs.Shared
{
   /// <summary>
   /// Represents a message in a chat conversation.
   /// </summary>
   [JsonPolymorphic(TypeDiscriminatorPropertyName = "role")]
   [JsonDerivedType(typeof(SystemMessage), "system")]
   [JsonDerivedType(typeof(UserMessage), "user")]
   [JsonDerivedType(typeof(AssistantMessage), "assistant")]
   [JsonDerivedType(typeof(ToolMessage), "tool")]
   public abstract class MessageBase
    {
        /// <summary>
        /// The role of the message author.
        /// </summary>
        [JsonPropertyName("role")]
        public string Role { get; set; } = string.Empty;

        /// <summary>
        /// The content of the message.
        /// </summary>
        [JsonPropertyName("content")]
        public string? Content { get; set; }
    }

    /// <summary>
    /// A message from the system.
    /// </summary>
    public class SystemMessage : MessageBase
    {
        public SystemMessage() => Role = "system";
    }

    /// <summary>
    /// A message from the user.
    /// </summary>
    public class UserMessage : MessageBase
    {
        public UserMessage() => Role = "user";
    }

    /// <summary>
    /// A message from the assistant.
    /// </summary>
    public class AssistantMessage : MessageBase
    {
        public AssistantMessage() => Role = "assistant";

        /// <summary>
        /// Tool calls made by the assistant.
        /// </summary>
        [JsonPropertyName("tool_calls")]
        public List<ToolCall>? ToolCalls { get; set; }
    }

The fix was to annotate the base class with JsonPolymorphic and JsonDerivedType attributes so the serialiser could select the correct concrete type based on the role field.

Ironically both Claude Code and Github Copilot had the serialisation attributes correct.

Leave a comment

This site uses Akismet to reduce spam. Learn how your comment data is processed.