/ src / utils / semanticBoolean.ts
semanticBoolean.ts
 1  import { z } from 'zod/v4'
 2  
 3  /**
 4   * Boolean that also accepts the string literals "true"/"false".
 5   *
 6   * Tool inputs arrive as model-generated JSON. The model occasionally quotes
 7   * booleans — `"replace_all":"false"` instead of `"replace_all":false` — and
 8   * z.boolean() rejects that with a type error. z.coerce.boolean() is the wrong
 9   * fix: it uses JS truthiness, so "false" → true.
10   *
11   * z.preprocess emits {"type":"boolean"} to the API schema, so the model is
12   * still told this is a boolean — the string tolerance is invisible client-side
13   * coercion, not an advertised input shape.
14   *
15   * .optional()/.default() go INSIDE (on the inner schema), not chained after:
16   * chaining them onto ZodPipe widens z.output<> to unknown in Zod v4.
17   *
18   *   semanticBoolean()                              → boolean
19   *   semanticBoolean(z.boolean().optional())        → boolean | undefined
20   *   semanticBoolean(z.boolean().default(false))    → boolean
21   */
22  export function semanticBoolean<T extends z.ZodType>(
23    inner: T = z.boolean() as unknown as T,
24  ) {
25    return z.preprocess(
26      (v: unknown) => (v === 'true' ? true : v === 'false' ? false : v),
27      inner,
28    )
29  }