Skip to content

TypeScript: z.custom<Decimal>() in generated decimal.ts breaks Zod v4 toJSONSchema() for MCP tool schemas #2008

Description

@BryanEddy5

Summary

The TypeScript SDK generator produces z.custom<Decimal>() branches in src/types/decimal.ts. In Zod v4, toJSONSchema() throws "Custom types cannot be represented in JSON Schema" when it encounters a ZodCustom type, which causes the MCP server's tools/list to fail entirely for any SDK that has Decimal fields in its tool schemas.

Generator version

Speakeasy-generated TypeScript SDK (using Zod v4 mini, as shipped in the recent Zod v4 release).

Generated code (current)

src/types/decimal.tsdecimal() function (and all variants):

export function decimal() {
  return z.union([
    z.custom<Decimal>((val) => val instanceof Decimal),  // ← breaks toJSONSchema()
    z.number().transform((v) => new Decimal(v)),
    z.string().transform((v, ctx) => { ... }),
  ]);
}

Same pattern appears in decimalStr, decimalOptional, decimalNullable, decimalStrOptional, decimalStrNullable, decimalConst, decimalStrConst.

Error

When the MCP SDK calls z4mini.toJSONSchema(toolInputSchema, { unrepresentable: "throw" }) during tools/list:

Custom types cannot be represented in JSON Schema

This fails the entire tools/list response, not just the affected tool — so none of the MCP tools are accessible.

Expected / suggested fix

Replace z.custom<Decimal>(...) branches with JSON Schema-representable types. For MCP tool input schemas, z.number() and z.string() are sufficient — LLMs always send primitive values, never Decimal instances.

Suggested output for decimal():

export function decimal() {
  return z.union([
    z.number().transform((v) => new Decimal(v)),
    z.string().transform((v, ctx) => { ... }),
  ]);
}

The z.custom<Decimal>((val) => val instanceof Decimal) branch is only useful at runtime when calling the SDK programmatically with Decimal instances directly. It is never exercised via MCP tool calls. Removing it from the union keeps runtime SDK behavior intact while making the schema JSON Schema-serializable.

Similarly, z.undefined() branches in the optional/nullable variants should be removed or replaced, as undefined is also not representable in JSON Schema.

Impact

  • Affected: any Speakeasy-generated MCP server for an API with Decimal/currency fields (e.g. shipping rates, financial amounts)
  • Workaround: post-codegen script that strips z.custom<Decimal>() lines from decimal.ts before building

Steps to reproduce

  1. Generate a TypeScript SDK with Speakeasy for an API that has Decimal fields
  2. Build the MCP server (npm run build)
  3. Send tools/list via the MCP protocol
  4. Observe: server returns error -32603 with message "Custom types cannot be represented in JSON Schema"

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions