Skip to content

Zod transform functions are lost during JSON Schema conversion, breaking union types #702

Open
@shadowyman

Description

@shadowyman

Zod transform functions are lost during JSON Schema conversion, breaking union types

Description

When using Zod schemas with the MCP SDK, transform functions are stripped during the conversion to JSON Schema. This causes union types like z.union([z.array(z.string()), z.string()]).transform(parseJsonStringArray) to lose the string option, making it impossible for MCP clients to send JSON-serialized arrays.

Current Behavior

When a Zod schema is defined as:

const schema = z.union([
  z.array(z.string()), 
  z.string()
]).transform(parseJsonStringArray);

The MCP SDK converts this to JSON Schema using zodToJsonSchema, which strips the transform and results in:

{
  "type": "array",
  "items": { "type": "string" }
}

The string option is lost, so when clients (like Claude) send '["file1.ts", "file2.ts"]', they get a validation error: "Expected array, received string".

Expected Behavior

MCP clients should be able to see both options in the schema and send either:

  • Native arrays: ["file1.ts", "file2.ts"]
  • JSON strings: '["file1.ts", "file2.ts"]'

Root Cause

In src/server/mcp.ts lines 120-122:

zodSchema: zodToJsonSchema(tool.inputSchema, {
  strictUnions: true,
}),

The zod-to-json-schema library explicitly cannot preserve transforms because JSON Schema has no way to represent transformation functions.

Proposed Solutions

Solution 1: Add preprocessing hooks

Allow tools to define preprocessors that run before validation:

mcpServer.tool('myTool', 'Description', schema, {
  preprocess: (params) => {
    // Transform JSON strings to arrays
    for (const [key, value] of Object.entries(params)) {
      if (typeof value === 'string' && value.startsWith('[')) {
        try {
          params[key] = JSON.parse(value);
        } catch {}
      }
    }
    return params;
  }
}, handler);

Solution 2: Support custom validation

Allow tools to provide their own validation function:

mcpServer.tool('myTool', 'Description', schema, {
  validate: (params) => {
    // Use Zod directly, preserving transforms
    return schema.parse(params);
  }
}, handler);

Solution 3: Preserve original Zod schema

Include both the original Zod schema and JSON Schema in the protocol:

{
  "inputSchema": {
    "type": "zod",
    "zodSchema": /* serialized Zod schema */,
    "jsonSchema": /* current JSON Schema for compatibility */
  }
}

Impact

This issue affects any MCP server that needs to accept parameters that can be either arrays or JSON-serialized arrays, which is common when:

  • Clients have different serialization capabilities
  • Parameters are passed through various systems
  • Maintaining backward compatibility

Reproduction

  1. Create an MCP server with a Zod schema using transform on a union type
  2. Try to invoke the tool with a JSON string array
  3. Observe validation error

Example Use Case

In the SonarQube MCP server, we need to accept file filters as either:

  • component_keys: ["src/file1.ts", "src/file2.ts"]
  • component_keys: '["src/file1.ts", "src/file2.ts"]'

Currently, we have to work around this by:

  1. Removing transforms from schemas (losing type safety)
  2. Manually parsing JSON strings in handlers (error-prone and repetitive)

Related Issues

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions