Skip to content

fix(ai): infer Zod outputSchema instead of collapsing to unknown#563

Merged
AlemTuzlak merged 1 commit into
mainfrom
562-chat-inferschematypezzodtypet-resolves-to-unknown-forces-parse-round-trip-or-cast
May 15, 2026
Merged

fix(ai): infer Zod outputSchema instead of collapsing to unknown#563
AlemTuzlak merged 1 commit into
mainfrom
562-chat-inferschematypezzodtypet-resolves-to-unknown-forces-parse-round-trip-or-cast

Conversation

@tombeckenham
Copy link
Copy Markdown
Contributor

Summary

`chat({ outputSchema: zodSchema })` returned `Promise` (and `StructuredOutputCompleteEvent` resolved with `T = unknown`) because `InferSchemaType` only matched `StandardJSONSchemaV1`. Zod's core `$ZodType['~standard']` is typed as `StandardSchemaV1.Props` — without the `jsonSchema` converter at the type level — so Zod schemas didn't structurally satisfy the JSON-schema branch and fell through to `unknown`, forcing callers to either cast or run a redundant `schema.parse()`.

The Zod type/runtime mismatch

Runtime Compile-time type
Zod 4.2+ `~standard` has both `validate` and `jsonSchema.input()` `~standard` declared as `StandardSchemaV1.Props` — no `jsonSchema` field visible
ArkType 2.1.28+ same public type does include the converter
Plain Valibot `~standard` has only `validate` `StandardSchemaV1.Props` (validator only)

Zod schemas are truly `StandardJSONSchemaV1` at runtime — Zod just hasn't widened the public `~standard` declaration on `$ZodType` to include the `jsonSchema` converter (the `StandardSchemaWithJSONProps` interface exists in their source but isn't applied to `$ZodType`). Looks like an in-progress upstream change.

Approach

`types.ts` — widen `SchemaInput` to also accept `StandardSchemaV1<any, any>`, add a `StandardSchemaV1` branch to `InferSchemaType`. The JSON-schema branch is still tried first (structurally narrower), so callers passing a real `StandardJSONSchemaV1` keep that resolution; Zod (and any future validator-only library) gets the inference via the new branch. `unknown` now means "I have no information," not "Zod was too clever for the matcher."

`schema-converter.ts` — add a runtime guard that throws an actionable error when a Standard Schema validator without `~standard.jsonSchema` is passed. This category was previously rejected at compile time; now that types accept it, the runtime needs to fail loudly instead of shipping `{ '~standard': ... }` straight to the LLM provider.

Runtime path is unchanged for Zod / ArkType / Valibot — `convertSchemaToJsonSchema` still uses the existing `isStandardJSONSchema` structural check, which Zod 4.2+ schemas pass at runtime regardless of their narrow type.

Test plan

  • `pnpm test:types` — passes; new `expectTypeOf<InferSchemaType>().toEqualTypeOf<{ greeting: string }>()` would have caught chat(): InferSchemaType<z.ZodType<T>> resolves to unknown — forces .parse() round trip or cast #562
  • `pnpm test:lib` — 774 passing in `@tanstack/ai`; full affected suite green (29 projects + 14 deps)
  • `pnpm test:eslint` — clean
  • New test in `standard-converter.test.ts` covers the validator-only error path
  • Existing e2e (`structured-output.spec.ts` + `api.chat.ts:46`) already exercises Zod `outputSchema` at runtime — behavior is unchanged before/after this fix

Closes #562

🤖 Generated with Claude Code

Zod's core $ZodType['~standard'] is typed as StandardSchemaV1.Props
(no jsonSchema converter), so chat({ outputSchema: zodSchema }) fell
through to unknown and forced callers to cast or re-parse. Widen
SchemaInput + InferSchemaType to also match StandardSchemaV1 so the
inference recovers; runtime path is unchanged. Add an actionable
runtime error for validator-only schemas (no ~standard.jsonSchema)
that the type widening now lets through.

Closes #562

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@github-actions
Copy link
Copy Markdown
Contributor

🚀 Changeset Version Preview

1 package(s) bumped directly, 22 bumped as dependents.

🟩 Patch bumps

Package Version Reason
@tanstack/ai 0.17.0 → 0.17.1 Changeset
@tanstack/ai-client 0.9.2 → 0.9.3 Dependent
@tanstack/ai-code-mode 0.1.11 → 0.1.12 Dependent
@tanstack/ai-code-mode-models-eval 0.0.16 → 0.0.17 Dependent
@tanstack/ai-code-mode-skills 0.1.11 → 0.1.12 Dependent
@tanstack/ai-devtools-core 0.3.28 → 0.3.29 Dependent
@tanstack/ai-event-client 0.3.1 → 0.3.2 Dependent
@tanstack/ai-fal 0.7.4 → 0.7.5 Dependent
@tanstack/ai-isolate-cloudflare 0.2.2 → 0.2.3 Dependent
@tanstack/ai-isolate-node 0.1.11 → 0.1.12 Dependent
@tanstack/ai-isolate-quickjs 0.1.11 → 0.1.12 Dependent
@tanstack/ai-preact 0.6.23 → 0.6.24 Dependent
@tanstack/ai-react 0.9.0 → 0.9.1 Dependent
@tanstack/ai-solid 0.8.0 → 0.8.1 Dependent
@tanstack/ai-svelte 0.8.0 → 0.8.1 Dependent
@tanstack/ai-vue 0.8.0 → 0.8.1 Dependent
@tanstack/ai-vue-ui 0.1.34 → 0.1.35 Dependent
@tanstack/preact-ai-devtools 0.1.32 → 0.1.33 Dependent
@tanstack/react-ai-devtools 0.2.32 → 0.2.33 Dependent
@tanstack/solid-ai-devtools 0.2.32 → 0.2.33 Dependent
ts-svelte-chat 0.1.42 → 0.1.43 Dependent
ts-vue-chat 0.1.42 → 0.1.43 Dependent
vanilla-chat 0.0.38 → 0.0.39 Dependent

@nx-cloud
Copy link
Copy Markdown

nx-cloud Bot commented May 15, 2026

View your CI Pipeline Execution ↗ for commit e001048

Command Status Duration Result
nx run-many --targets=build --exclude=examples/** ✅ Succeeded 1m 59s View ↗

☁️ Nx Cloud last updated this comment at 2026-05-15 05:06:26 UTC

@pkg-pr-new
Copy link
Copy Markdown

pkg-pr-new Bot commented May 15, 2026

Open in StackBlitz

@tanstack/ai

npm i https://pkg.pr.new/@tanstack/ai@563

@tanstack/ai-anthropic

npm i https://pkg.pr.new/@tanstack/ai-anthropic@563

@tanstack/ai-client

npm i https://pkg.pr.new/@tanstack/ai-client@563

@tanstack/ai-code-mode

npm i https://pkg.pr.new/@tanstack/ai-code-mode@563

@tanstack/ai-code-mode-skills

npm i https://pkg.pr.new/@tanstack/ai-code-mode-skills@563

@tanstack/ai-devtools-core

npm i https://pkg.pr.new/@tanstack/ai-devtools-core@563

@tanstack/ai-elevenlabs

npm i https://pkg.pr.new/@tanstack/ai-elevenlabs@563

@tanstack/ai-event-client

npm i https://pkg.pr.new/@tanstack/ai-event-client@563

@tanstack/ai-fal

npm i https://pkg.pr.new/@tanstack/ai-fal@563

@tanstack/ai-gemini

npm i https://pkg.pr.new/@tanstack/ai-gemini@563

@tanstack/ai-grok

npm i https://pkg.pr.new/@tanstack/ai-grok@563

@tanstack/ai-groq

npm i https://pkg.pr.new/@tanstack/ai-groq@563

@tanstack/ai-isolate-cloudflare

npm i https://pkg.pr.new/@tanstack/ai-isolate-cloudflare@563

@tanstack/ai-isolate-node

npm i https://pkg.pr.new/@tanstack/ai-isolate-node@563

@tanstack/ai-isolate-quickjs

npm i https://pkg.pr.new/@tanstack/ai-isolate-quickjs@563

@tanstack/ai-ollama

npm i https://pkg.pr.new/@tanstack/ai-ollama@563

@tanstack/ai-openai

npm i https://pkg.pr.new/@tanstack/ai-openai@563

@tanstack/ai-openrouter

npm i https://pkg.pr.new/@tanstack/ai-openrouter@563

@tanstack/ai-preact

npm i https://pkg.pr.new/@tanstack/ai-preact@563

@tanstack/ai-react

npm i https://pkg.pr.new/@tanstack/ai-react@563

@tanstack/ai-react-ui

npm i https://pkg.pr.new/@tanstack/ai-react-ui@563

@tanstack/ai-solid

npm i https://pkg.pr.new/@tanstack/ai-solid@563

@tanstack/ai-solid-ui

npm i https://pkg.pr.new/@tanstack/ai-solid-ui@563

@tanstack/ai-svelte

npm i https://pkg.pr.new/@tanstack/ai-svelte@563

@tanstack/ai-utils

npm i https://pkg.pr.new/@tanstack/ai-utils@563

@tanstack/ai-vue

npm i https://pkg.pr.new/@tanstack/ai-vue@563

@tanstack/ai-vue-ui

npm i https://pkg.pr.new/@tanstack/ai-vue-ui@563

@tanstack/openai-base

npm i https://pkg.pr.new/@tanstack/openai-base@563

@tanstack/preact-ai-devtools

npm i https://pkg.pr.new/@tanstack/preact-ai-devtools@563

@tanstack/react-ai-devtools

npm i https://pkg.pr.new/@tanstack/react-ai-devtools@563

@tanstack/solid-ai-devtools

npm i https://pkg.pr.new/@tanstack/solid-ai-devtools@563

commit: e001048

@AlemTuzlak AlemTuzlak merged commit e810153 into main May 15, 2026
10 checks passed
@AlemTuzlak AlemTuzlak deleted the 562-chat-inferschematypezzodtypet-resolves-to-unknown-forces-parse-round-trip-or-cast branch May 15, 2026 08:32
@github-actions github-actions Bot mentioned this pull request May 15, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

chat(): InferSchemaType<z.ZodType<T>> resolves to unknown — forces .parse() round trip or cast

2 participants