-
Notifications
You must be signed in to change notification settings - Fork 83
feat(react): migrate to Standard Schema for schema conversion #1446
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Conversation
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
- The migration to Standard Schema introduces a few behavioral regressions and loosened type guarantees, notably around
TamboTooltyping and component prop validation inrenderComponentIntoMessage. - Schema validation has been generalized from Zod AST inspection to JSON Schema pattern detection, which is more flexible but depends on converter behavior and could silently miss some dynamic-key map patterns.
- Several central helpers (
schemaToJsonSchema,safeSchemaToJsonSchema,getParametersFromToolSchema) currently handle errors and unknown schema types leniently, which may mask real integration problems. - Tests and utilities have largely been updated correctly for Zod 4, but there are a few brittle Zod-internal assumptions and minor API misuses that should be tightened to avoid future compatibility issues.
Additional notes (8)
- Maintainability |
react-sdk/src/model/component-metadata.ts:78-78
TamboTool.toolandtransformToContentwere previously strongly typed with Zod-based generics (Args,Returns). They've been relaxed to(...args: any[]) => anyandresult: any, which throws away compile-time safety for tool implementations.
Given that you're now vendor-agnostic at the schema level, you can still preserve type safety by keeping the generic interface and parameterizing it by the value-level types rather than the validator implementation itself. For example:
- Keep
TamboTool<Args extends unknown[] = unknown[], Result = unknown>withtool: (...args: Args) => ResultandtransformToContent?: (result: Result) => .... - Let
toolSchemaremainStandardSchemaV1 | JSONSchemaLitewithout tying it to those generics (document that they must match, but don’t try to encode that in the type system).
Right now this change makes it much easier to write tools whose runtime behavior doesn’t actually match the described schema, and TypeScript will no longer help catch that mismatch at call sites.
- Maintainability |
react-sdk/src/util/generate-component.ts:27-39
renderComponentIntoMessagenow determines whether to validate props by checkingisStandardSchema(registeredComponent.props). However,registeredComponent.propsappears to be the JSON Schema representation produced bygetSerializedProps/convertPropsToJsonSchema, not the original validator.
This means that in the usual registry flow you're likely never hitting the parse branch anymore:
- In the original design,
registeredComponent.propsheld a Zod schema, soinstanceof ZodTypeworked. - After migration,
propsis serialized to JSON Schema in the registry, but this function still assumes it can call.parse().
As a result, components that previously had their props validated at render-time may now be receiving unvalidated parsedProps if only JSON Schema is stored. That’s a subtle behavior regression and will be hard to spot in normal testing.
- Maintainability |
react-sdk/src/util/validate-schema.ts:175-181
You’ve reintroduced a localisJSONSchemaimplementation here after moving a more general one intoschema.ts. Maintaining two similar-but-not-identical JSON Schema detectors in different modules increases the risk of divergent behavior over time.
assertNoRecordSchema already depends on utilities from schema.ts; re-using that module’s isJSONSchema would reduce subtle inconsistencies around what the code considers a JSON Schema.
- Readability |
react-sdk/src/providers/tambo-registry-provider.test.tsx:189-189
The new registry tests intentionally rejectz.record()by asserting on the messageRecord types (objects with dynamic keys) are not supported .... That’s good. However, in a couple of cases you’ve updated the test schemas fromz.record(z.string())toz.record(z.string(), z.string())to fit the Zod 4 signature.
These tests currently pass only because your new validator operates on the JSON Schema output, not the Zod AST, so it doesn’t care about the exact Zod call shape. That’s fine for behavior, but the tests are now slightly misleading—they look like they’re verifying Zod behavior when in reality they’re verifying JSON Schema patterns.
To keep tests maintainable, it would help to make that distinction explicit in comments, especially where the same test file is mixing Zod 3 and Zod 4 usage via z3/z4.
- Maintainability |
react-sdk/src/testing/tools.ts:10-10
serializeRegistrynow wraps all tool parameters into a singleargsparameter whose schema is either: - The JSON Schema derived from a Standard Schema validator (for Zod/Valibot/etc.), or
- The JSON Schema passed in directly.
This is a significant behavioral shift from the previous implementation, which:
- Inspected Zod function schemas and emitted one parameter per argument (
param1,param2, etc.).
If any downstream consumers (showcase app, backend, or other tools) expect named positional parameters rather than a single args object, this will be a breaking change in how tools are described, even if the underlying schema content is equivalent. It’s not obvious from the PR description whether this change of shape is intentional and acceptable for all consumers.
- Maintainability |
react-sdk/src/util/registry.ts:138-138
getParametersFromToolSchemanow returns an empty parameter list and logs a warning when the schema is neither JSON Schema nor Standard Schema. This makes the system quite lenient in the face of misconfigured tool schemas: a tool with an unsupported or malformedtoolSchemawill be exposed to the AI with no parameters, which can be very confusing to debug.
Previously, misconfigured Zod schemas would either fail conversion or validation more loudly. Swallowing these into an empty parameter list could mask real integration bugs.
- Maintainability |
react-sdk/package.json:61-61
The newpeerDependenciesentry forzod-to-json-schemaon the React SDK is marked optional, which aligns with the intent: only Zod 3 users should need it. However, the code paths that actually require it (schema.ts’s Zod vendor registration) throw an error only when the converter is invoked for a Zod 3 schema.
This laziness is good, but there’s currently no high-level documentation or runtime guidance for SDK consumers who:
- Pass Zod 3 schemas,
- Haven’t installed
zod-to-json-schema, and - Encounter a thrown error during registry setup.
Given that this is a new requirement, you may want to make the failure mode more discoverable for users.
- Maintainability |
react-sdk/src/providers/tambo-registry-provider.tsx:29-32
deriveServerKeyis unchanged, but now that the registry is more schema-flexible, the validation/logging paths around MCP server infos become even more important. Currently, if a malformed URL is passed, you catch the error and fall back tourl.toLowerCase(), which may still be a completely bogus key.
While this isn’t new in this diff, the broader refactor is a good opportunity to consider whether silently accepting invalid URLs is still the right tradeoff, versus surfacing a clearer error to the caller.
Summary of changes
Overview
- Migrated the React SDK from directly using Zod 3 +
zod-to-json-schemato a Standard Schema-based abstraction using@standard-community/standard-json. - Introduced a new
schemautility module to detect Standard Schema vs JSON Schema and convert to JSON Schema, with vendor-specific handling for Zod 3 and Zod 4. - Replaced
validate-zod-schemawith a more generalvalidate-schemathat validates both Standard Schema and JSON Schema for unsupported record/map patterns. - Updated
TamboTool,TamboComponent, registry, interactable provider, tests, and tooling to:- Support both Zod 3 and Zod 4 (and other Standard Schema vendors) for component props and tool schemas.
- Treat
toolSchema/propsSchemaas Standard Schema or JSON Schema rather than Zod-only. - Handle Zod function schemas (v3 & v4) by extracting argument schemas before JSON Schema conversion.
- Adjusted package dependencies and peer dependencies to:
- Add
@standard-community/standard-json,@standard-schema/spec,zod@4(dev),zod3alias, and updatedzod-to-json-schema. - Make
zod-to-json-schemaan optional peer of the React SDK for Zod 3 users.
- Add
- Added extensive compatibility tests for Zod 3, Zod 4, and JSON Schema usage in the registry and validation utilities.
- Updated all test imports and code paths to use
zod/v4where appropriate and to work with the new schema abstraction.
1d9a672 to
59177e2
Compare
lachieh
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@CharlieHelps make these changes too
Expand this to see my work.
|
15393c9 to
c1764f4
Compare
alecf
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
looks good - seems like charlie caught the good stuff :) a few minor comments
| * }; | ||
| * ``` | ||
| */ | ||
| export interface TamboTool { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
sad to see that there's no easy way to keep Args and Returns - I assume this just gets incredibly hard when you're dealing with a combination of zod and StandardSchema?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It's definitely possible, and I think we should extend their usage to the functions that take them as args so that we get type errors inline when registering tools/components/interactables (unless I'm missing something and that already works?)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
well what already worked was you could assign the tool, and then pass in schema, and the type system would complain if things didn't line up:
{
tool: (message: string) => { console.log("I got a message: ", message); return "Hello!" },
toolSchema: z.function().args(z.number()).returns(z.number())
}
and you'd get a type error because the args / return values do not match
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I see, yeah I wasn't getting those because it wasn't specifying the type args. I'm going to try something to fix this.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ok, this is where some chunky changes happen. See the changes in react-sdk/src/model/component-metadata.ts and the new RegisterToolFn interface in react-sdk/src/providers/tambo-registry-provider.tsx. This should add some stronger type checking! With that, there are some opinions though. We should probably give it some use to see how useful or aggressive it is.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The diff successfully introduces a Standard Schema abstraction and JSON Schema conversion pipeline, but it also relaxes type safety around tools and props, particularly by dropping TamboTool generics and relying on any in several places. Record-schema detection and error handling in assertNoRecordSchema/getSerializedProps could be stricter to avoid silently skipping problematic schemas or masking conversion failures. The new registry and testing utilities centralize schema handling but change the shape of serialized tool parameters and treat unknown toolSchema types very leniently, which may lead to confusing behavior when integrations are misconfigured. Tightening these areas will make the new vendor-agnostic design more robust and easier to debug for SDK consumers.
Additional notes (9)
- Maintainability |
react-sdk/src/model/component-metadata.ts:55-55
TamboToolwas previously generic over argument and return types, which allowed strong typing oftoolandtransformToContentat compile time. The new non-generic interface withtool: (...args: any[]) => anyandresult: anydiscards those guarantees and makes it much easier to drift out of sync with the declaredtoolSchema.
Given that you’re now schema-vendor-agnostic, you can still keep value-level generics without coupling them to Zod:
- Keep
TamboTool<Args extends unknown[] = unknown[], Result = unknown>and typetool: (...args: Args) => ResultandtransformToContent?: (result: Result) => .... - Leave
toolSchemaasStandardSchemaV1 | JSONSchemaLiteand document that it must describeArgs/Resultwithout trying to encode that relationship in the type system.
Right now this change represents a regression in type safety for tool authors and callers, and it violates the “avoid unnecessary any” guidance implied by typical TypeScript best practices.
- Maintainability |
react-sdk/src/util/generate-component.ts:27-39
renderComponentIntoMessageused to checkinstanceof ZodTypeagainst the storedpropsand would only call.parsewhen the registry still held a Zod schema. After the migration,registeredComponent.propsappears to be the JSON Schema output fromgetSerializedPropsin most flows, but this function still attemptsprops.parse(...)wheneverisStandardSchemareturns true.
If props is already a JSON Schema object (which is the normal case once registered), isStandardSchema will be false and no validation occurs. However, when props is a non-Zod Standard Schema vendor implementation that doesn’t expose parse with the same runtime shape, this cast to { parse: (data: unknown) => unknown } risks a late runtime error.
More importantly, the branch no longer clearly distinguishes between validator instances and serialized schemas, which makes it easy to accidentally skip validation entirely and diverge from the design intent of validating props before rendering.
- Maintainability |
react-sdk/src/testing/tools.ts:1-1
The newserializeRegistryimplementation unconditionally wraps all tool parameters into a singleargsobject and no longer inspects Zod function schemas to emit one parameter per argument. This is a significant behavioral change from the previous version, which producedparam1,param2, etc. from Zod function.parameters().
If any downstream consumers (tests, tooling, backend expectations) rely on the old positional parameter shape, this will be an observable breaking change even though the overall JSON content is equivalent. The tests in this file only use serializeRegistry within Jest expectations, so they won’t catch external consumer regressions.
- Maintainability |
react-sdk/src/testing/tools.ts:5-5
serializeRegistrynow usessafeSchemaToJsonSchemaand always wraps tool parameters into a singleargsobject, mirroring the new runtime behavior. That’s consistent withgetParametersFromToolSchema, but unlikeschemaToJsonSchemathis helper drops any error information: the tests don’t exercise theundefinedpath, and callers ignore the possibility that conversion failed.
If safeSchemaToJsonSchema returns undefined for a Standard Schema (e.g. missing zod-to-json-schema for Zod 3), you’ll emit a parameter whose schema is undefined in the serialized registry. That’s a confusing artifact for tests that are supposed to describe what the AI sees and may mask issues in snapshot-style assertions.
- Maintainability |
react-sdk/src/util/registry.ts:129-177
The new Zod function helpers (isZod4FunctionSchema,isZod3FunctionSchema,getZodFunctionArgs) correctly delegate to the sharedisZodSchemaguard before touching internals, which is good. However, the return type ofisZod4FunctionSchema/isZod3FunctionSchemais justboolean, so call sites don’t benefit from type narrowing and still rely on manual casting when accessingdef/_def.
Given that these helpers exist specifically to identify function-shaped Zod schemas, you can make them proper type guards that refine to the appropriate Zod type and eliminate the remaining as assertions in getParametersFromToolSchema. This reduces the surface area of "brittle internal" usage further.
- Maintainability |
react-sdk/src/util/registry.ts:117-117
mapTamboToolToContextToolused to derive parameter metadata by inspecting Zod function schemas and emitting one parameter per argument; it now callsgetParametersFromToolSchema, which always collapses the tool parameters to a singleargsobject based on the converted JSON Schema.
This is a semantic change to how tools are exposed: downstream consumers that previously saw param1, param2, etc. will now only see { name: "args", type: "object", schema: ... }. While the underlying JSON Schema still captures the structure, any consumers that relied on positional param names or descriptions per-arg will break or behave differently.
The new behavior might be acceptable, but it should be clearly intentional and, ideally, guarded by tests that assert the new shape so regressions between releases are explicit rather than accidental.
- Readability |
react-sdk/src/util/schema.ts:67-134
looksLikeJSONSchemacorrectly avoids classifying Standard Schema values as JSON Schema, but the Zod-based validator is still quite broad and will accept any object that happens to match its shape, even if it isn’t actually a JSON Schema used in this system. Given thatschemaToJsonSchemaalready handles Standard Schema conversion centrally, you don’t strictly need this helper to be a type guard that returnsJSONSchema7—a looser structural check might be sufficient.
The main risk is that callers may treat arbitrary objects that happen to satisfy jsonSchemaTopLevel as trusted schemas and skip validation or conversion paths they should go through.
- Maintainability |
react-sdk/src/providers/tambo-registry-provider.tsx:463-494
getSerializedPropspreviously had a simple and predictable behavior: Zod schemas were converted withzod-to-json-schema, and other shapes were treated as raw JSON Schema. With the new Standard Schema abstraction, the error path now throws a generic"Failed to convert props schema for ${name}"after logging, but there’s no context about why conversion failed (e.g. missing Zod 3 converter, unsupported vendor, function schemas, etc.).
Given that this function is on the hot path for component registration, opaque errors here can be very frustrating to debug for consumers integrating new schema vendors or migrating from Zod 3 to Zod 4.
- Maintainability |
react-sdk/src/providers/hoc/with-tambo-interactable.tsx:7-13
InteractableConfig.propsSchemahas been widened fromz.ZodTypeAnytoSchema(Standard Schema or JSON Schema), which is consistent with the rest of the migration. However, the HOC does not appear to perform any conversion or validation against this schema—it just passes the config intouseTamboInteractable.
That means callers can now pass arbitrary JSON Schema objects and the type system won’t help them keep propsSchema in sync with the wrapped component’s actual props. This was already somewhat true with ZodTypeAny, but broadening the accepted shape while continuing to treat it as an opaque value increases the risk that malformed or mismatched schemas get attached to interactables without any early feedback.
Given that InteractableConfig is a public surface for users, it’s worth clarifying (in code comments or types) whether JSON Schema is really supported here, or if this should be limited to Standard Schema validators only (and then converted downstream).
Summary of changes
Overview
- Migrated multiple packages from
zoddefault import to versioned imports (zod/v3in backend,zod/v4in React SDK and docs) to support side‑by‑side Zod 3 & Zod 4. - Introduced a new Standard Schema abstraction in the React SDK (
src/util/schema.ts) and a JSON Schema validation helper (src/util/validate-schema.ts), replacing Zod‑specific logic. - Updated the Tambo React component/tool model (
component-metadata.ts) and registry utilities (registry.ts,generate-component.ts,testing/tools.ts) to work withStandardSchemaV1 | JSONSchema7instead of Zod-only schemas. - Added comprehensive schema compatibility tests (
tambo-registry-schema-compat.test.tsx,schema.test.ts,validate-schema.test.ts) and removed the old Zod‑specific validation tests. - Updated package metadata (
react-sdk/package.json,packages/backend/package.json,packages/core/package.json, rootpackage-lock.json) to depend on@standard-community/standard-json,@standard-schema/spec, and newerzod-to-json-schema, while making Zod/Z2JS optional peers in the React SDK. - Adjusted various providers and tests to use the new Zod 4 function API and Standard Schema‑based validation rather than direct Zod AST inspection.
|
#1446 (comment) Updated the Zod function helpers in Changes
Net effect: we only poke at Verification# Lint (monorepo via Turbo)
$ npm run lint
# ✅ Passed (no errors). Existing warnings remain, including:
# - jsdoc/check-alignment and jsdoc/require-jsdoc in schema/validate-schema
# that were already present on this branch.
# Type-check (monorepo)
$ npm run check-types
# ✅ Passed across all packages, including @tambo-ai/react.
# Tests (monorepo)
$ npm test
# ✅ All Jest and Node test suites passed (no new failures in react-sdk).Self-review items not applied (kept as follow-ups to avoid widening this change):
|
93bc72a to
93ecc3d
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The migration largely implements the Standard Schema abstraction correctly, but a few areas introduce risk: props validation in renderComponentIntoMessage now assumes .parse on any Standard Schema and can crash for non-Zod vendors; TamboTool’s public type has lost its generic coupling between tool and toolSchema, weakening compile-time safety. Tool and component schema serialization is stricter but still conflates different failure modes and sometimes swallows conversion errors, and registry parameter extraction now unconditionally collapses all schemas into a single args object, which may be a breaking surface change. Several helper and stub implementations (tool-caller, TamboStubRegistryProvider) add unnecessary indirection or tight coupling that could be simplified for better maintainability.
Additional notes (7)
-
Maintainability |
react-sdk/src/mcp/tambo-mcp-provider.tsx:364-371
TamboMcpProvidernow falls back to({} as JSONSchemaLite)whentool.inputSchemais falsy. That silently converts a missing MCP tool schema into an empty object schema, which will: -
Mislead downstream consumers about the true shape of the tool parameters.
-
Potentially hide integration bugs where MCP tools failed to advertise an input schema.
Given that this is an error path, masking it with {} (and a TODO comment) is not ideal. Callers will have no indication that the MCP server did not provide a usable schema.
-
Maintainability |
react-sdk/src/model/component-metadata.ts:71-71
TamboToolBasehas been split into several variants (TamboToolZod3Function,TamboToolZod4Function,TamboToolStandardSchema,TamboToolJSONSchema, etc.) anddefineTamboTooloverloads are introduced to recover type inference. However, the exportedTamboTooltype is now a non-generic base withtool: (...args: any[]) => anyandtoolSchema: unknown, which: -
Throws away compile-time coupling between
toolvalue-level types andtoolSchemadescriptions. -
Forces downstream consumers (including
mapTamboToolToContextTool) to treat everything asunknownand rely on runtime guards.
This is a regression from the previous generic TamboTool<Args, Returns> design, which statically enforced that the function signature matched the Zod schema. You’ve reintroduced some inference for defineTamboTool, but the public TamboTool remains untyped.
- Maintainability |
react-sdk/src/providers/tambo-registry-provider.tsx:125-152
RegisterToolFnoverloads correctly distinguish between Zod 4, Zod 3, Standard Schema, JSON Schema, and the fallbackTamboTooltype. However, the implementation signature is stillconst registerTool: RegisterToolFn = useCallback((tool: TamboTool, warnOnOverwrite = true) => { ... }), which completely erases the generics and specialised tool shapes at runtime.
This has two consequences:
- Inside
registerTool,toolis typed as the wideTamboToolunion, so you can’t leverage the richer schema typing fromdefineTamboToolwithout additional narrowing. - At call sites that import
RegisterToolFn, the overloads help inference, but downstream functions likemapTamboToolToContextToolare still operating on the erased shape and rely on runtime detection (isStandardSchema,looksLikeJSONSchema).
Given the complexity of the overloads, this erasure undermines some of the value you’re trying to gain from them.
-
Maintainability |
react-sdk/src/testing/tools.ts:11-11
serializeRegistrynow usessafeSchemaToJsonSchemafor Standard Schema props and tools, but it ignores the possibility that conversion failed and just returns whateversafeSchemaToJsonSchemaproduced. SincesafeSchemaToJsonSchemacurrently returns a plainJSONSchema7 | undefinedin this file, tools or components whose schemas fail conversion will be serialized withschema: undefined, which is a confusing artifact in snapshot-based tests and hides the underlying issue. -
Maintainability |
react-sdk/src/testing/tools.ts:1-1
serializeRegistryhas been significantly changed: instead of introspecting Zod function schemas and emitting one parameter per argument, it now wraps everything into a singleargsparameter whoseschemais either the Standard Schema–derived JSON Schema or the JSON Schema passed through.
This aligns with the new runtime behaviour of getParametersFromToolSchema, but it is a breaking change for any consumers (tests, tooling, or visualization code) that expected positional parameter names like param1, param2, etc. The existing tests in this file don’t exercise or assert on that structural difference, so an inadvertent future regression here would be easy to miss.
- Performance |
react-sdk/src/util/tool-caller.ts:78-81
findToolnow wrapsregistryTool.toolin anasyncarrow function that simply awaits the original call:
getComponentContext: async (...args) => await registryTool.tool(...args)Since tool can already return either a value or a promise (MaybeAsync), the previous direct reference (getComponentContext: registryTool.tool) would have worked fine; wrapping it adds an extra microtask and obscures the original type shape without a clear benefit. It could also interfere with this binding if any tools rely on it (unlikely but possible).
- Maintainability |
react-sdk/src/providers/tambo-stubs.tsx:100-104
TamboStubRegistryProvider’s props type has been replaced withPropsWithChildren<TamboRegistryContext>. However,TamboRegistryContextincludes runtime-only fields likeregisterMcpServer,registerMcpServers, andonCallUnregisteredToolthat the stub previously didn’t accept. In the JSX usage below you’ve added dummymcpServerInfos,registerMcpServer, andregisterMcpServersprops, butonCallUnregisteredToolis still missing.
More importantly, coupling the stub’s props directly to the full context type makes tests brittle: any future addition to TamboRegistryContext will break all TamboStubProvider call sites. The original explicit prop list was more stable and focused on what tests actually need.
Summary of changes
Overview
This diff migrates multiple parts of the backend and React SDK from direct Zod 3 usage and zod-to-json-schema to a Standard Schema–centric design with JSON Schema conversion.
Key changes include:
- Backend: switches imports from
zodtozod/v3and bumpszod-to-json-schemato^3.25.0inpackages/backendandpackages/core. - React SDK:
- Adds
@standard-community/standard-jsonand@standard-schema/specdependencies, plus versionedzodpeer/dev dependencies with optionalzod-to-json-schemafor Zod 3. - Introduces
src/util/schema.tswith Standard Schema detection, Zod 3/4 guards, JSON Schema heuristics (looksLikeJSONSchema), and conversion helpers (schemaToJsonSchema,safeSchemaToJsonSchema). - Replaces
validate-zod-schemawithvalidate-schema, operating on JSON Schema derived via Standard Schema, and forbids record-like types usingadditionalPropertiesdetection. - Refactors
TamboTool/TamboComponentmodel to support multiple schema forms, addsdefineTamboTooland inference helpers, and introduces several specializedTamboTool*variants. - Updates
tambo-registry-providerandregistry.tsto use the new schema utilities, register tools/components with Standard Schema or JSON Schema, and extract function args for Zod 3/4. - Updates tests and docs to use
zod/v4, adds compatibility suites (tambo-registry-schema-compat.test.tsx,schema.test.ts,validate-schema.test.ts), and adjusts testing helpers (serializeRegistry) to work via Standard Schema JSON conversion. - Minor behavioral tweaks: tool calling wraps
toolin an async function; MCP provider now falls back to{}JSON schema wheninputSchemais missing; stubs and HOCs updated to use new types.
- Adds
- Root
package-lock.jsonis regenerated to reflect new packages, peer flags, and updatedzod-to-json-schema/zodtopology.
| schema: TamboTool["toolSchema"], | ||
| ): ParameterSpec[] => { | ||
| if (isJsonSchema(schema)) { | ||
| // Handle JSON Schema - wrap as a single "args" parameter | ||
| if (looksLikeJSONSchema(schema)) { | ||
| const jsonSchema = schema; | ||
| return [ | ||
| { | ||
| name: "args", | ||
| type: "object", | ||
| description: schema.description ?? "", | ||
| description: jsonSchema.description ?? "", | ||
| isRequired: true, | ||
| schema: schema, | ||
| schema: jsonSchema, | ||
| }, | ||
| ]; | ||
| } | ||
|
|
||
| const parameters: z.ZodTuple = schema.parameters(); | ||
| return parameters.items.map((param, index): ParameterSpec => { | ||
| const name = `param${index + 1}`; | ||
| const type = getZodBaseType(param); | ||
| const description = param.description ?? ""; | ||
| const isRequired = !param.isOptional(); | ||
| const schema = zodToJsonSchema(param); | ||
|
|
||
| return { | ||
| name, | ||
| type, | ||
| description, | ||
| isRequired, | ||
| schema, | ||
| }; | ||
| }); | ||
| }; | ||
|
|
||
| const getZodBaseType = (schema: z.ZodTypeAny): string => { | ||
| const typeName = schema._def.typeName; | ||
| switch (typeName) { | ||
| case "ZodString": | ||
| return "string"; | ||
| case "ZodNumber": | ||
| return "number"; | ||
| case "ZodBoolean": | ||
| return "boolean"; | ||
| case "ZodArray": | ||
| return "array"; | ||
| case "ZodEnum": | ||
| return "enum"; | ||
| case "ZodDate": | ||
| return "date"; | ||
| case "ZodObject": | ||
| return "object"; | ||
| default: | ||
| console.warn("falling back to string for", typeName); | ||
| return "string"; | ||
| // Handle Standard Schema validators | ||
| if (isStandardSchema(schema)) { | ||
| // Check for Zod function schemas - extract args before converting | ||
| if (isZod4FunctionSchema(schema) || isZod3FunctionSchema(schema)) { | ||
| const argsSchema = getZodFunctionArgs(schema); | ||
| if (argsSchema && isStandardSchema(argsSchema)) { | ||
| const jsonSchema = schemaToJsonSchema(argsSchema); | ||
| return [ | ||
| { | ||
| name: "args", | ||
| type: "object", | ||
| description: jsonSchema.description ?? "", | ||
| isRequired: true, | ||
| schema: jsonSchema, | ||
| }, | ||
| ]; | ||
| } | ||
| } | ||
|
|
||
| // Non-function Standard Schema - convert directly | ||
| const jsonSchema = schemaToJsonSchema(schema); | ||
| return [ | ||
| { | ||
| name: "args", | ||
| type: "object", | ||
| description: jsonSchema.description ?? "", | ||
| isRequired: true, | ||
| schema: jsonSchema, | ||
| }, | ||
| ]; | ||
| } | ||
|
|
||
| // Fallback for unknown schema types | ||
| console.warn("Unknown tool schema type, returning empty parameters"); | ||
| return []; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
getParametersFromToolSchema now collapses all tool schemas (Standard Schema, Zod function, and JSON Schema) into a single "args" parameter containing the converted JSON Schema. This is consistent internally, but it is a semantic change from the previous behavior, where Zod function schemas produced one parameter per positional argument (param1, param2, etc.).
If any existing consumers (UI, backend, or tests) depended on the old per-argument structure, this is a breaking change in the tool surface, not just an implementation detail. The tests in this diff only assert that something is produced; they don’t assert the shape compared to the previous behavior.
Suggestion
If the new single-args shape is the intended long-term contract, it should be explicitly documented and tested as such, and any downstream consumers updated accordingly. At a minimum, add tests that:
- Verify Zod 3 function schemas with multiple args now produce a single
argsparameter whose schema is an object/tuple capturing all inputs. - Confirm JSON Schema-based tools are treated the same way.
If backward compatibility is desired, an alternative is to preserve per-argument parameter expansion for Zod function schemas, and only use the single-args pattern for non-function Standard Schema/JSON Schema. Reply with "@CharlieHelps yes please" if you’d like me to add tests clarifying and locking in the new args-only contract, or sketch a backward-compatible expansion strategy.
| import { Schema } from "../../util/schema"; | ||
| import { useTamboInteractable } from "../tambo-interactable-provider"; | ||
|
|
||
| export interface InteractableConfig { | ||
| componentName: string; | ||
| description: string; | ||
| propsSchema?: z.ZodTypeAny; | ||
| propsSchema?: Schema; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
with-tambo-interactable used to constrain propsSchema to z.ZodTypeAny, which at least encoded that this value was a validator rather than arbitrary data. Widening it to Schema (Standard Schema or JSON Schema) means callers can now pass raw JSON Schema here, but this HOC does not perform any conversion/validation and simply forwards the config into useTamboInteractable.
That combination is risky: the type suggests JSON Schema is acceptable, but there’s no clear contract around whether downstream code expects a validator vs. a schema object. It also deviates from the rest of the public surface, where TamboComponent.propsSchema is clearly documented as “schema describing the component’s props” and then converted centrally.
If this HOC is intended to mirror TamboComponent, it should either:
- Be restricted to
StandardSchemaV1-style validators only (and converted later), or - Clearly document that JSON Schema is accepted but never executed, so users don’t assume runtime validation like
zod.parse()will occur.
Suggestion
Consider tightening InteractableConfig.propsSchema back to a validator-only type and documenting that JSON Schema should go through the registry/component path instead. For example:
// Prefer a validator-only type here
import type { StandardSchemaV1 } from "@standard-schema/spec";
export interface InteractableConfig {
componentName: string;
description: string;
propsSchema?: StandardSchemaV1; // or a narrower alias re-exported from util/schema
}Then, if there’s a strong use case for JSON Schema on interactables, add an explicit propsJsonSchema?: JSONSchema7 field so the two code paths can be handled distinctly downstream.
Reply with "@CharlieHelps yes please" if you’d like me to draft that refactor and thread it through useTamboInteractable and callers.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The migration to Standard Schema and JSON Schema conversion is broadly on the right track, but several core paths now rely on loose heuristics, brittle Zod internals, and silent fallbacks that can hide schema misconfigurations. In particular, renderComponentIntoMessage, TamboTool typing, serializeRegistry, and getParametersFromToolSchema all deserve tightening to avoid lost validation and confusing behavior. Consolidating JSON Schema detection, hardening Zod function helpers, and making unknown schema shapes fail loudly will make the new abstraction safer and easier to debug.
Additional notes (6)
-
Maintainability |
react-sdk/src/util/generate-component.ts:27-37
Switching this check frominstanceof ZodTypetoisStandardSchema(registeredComponent.props)changes the semantics pretty significantly: -
registeredComponent.propsis now set bygetSerializedProps, which converts Standard Schema validators to JSON Schema objects. After registration,propsis no longer the original validator but the serialized schema. -
isStandardSchema(registeredComponent.props)will therefore befalsefor the normal registry flow, because the Standard Schema wrapper (~standard) has been stripped. That means this branch will almost never run in practice. -
When it does run (e.g. if someone bypasses the registry and sticks a raw validator onto
componentList), you cast to{ parse: (data: unknown) => unknown }and call.parsewithout checking the vendor or method name. Non‑Zod Standard Schema implementations are not required to exposeparse, so this can blow up at runtime.
Net effect: most components have silently lost props validation at render time, and the remaining code path is brittle.
To restore predictable behavior and avoid unsafe casts, you should either:
- Treat
registeredComponent.propsas always JSON Schema at this point and skip runtime parsing here (validation happens earlier), or - Thread both the validator and the serialized schema through the registry and add a dedicated
validatePropshelper that only calls.parseon known Zod schemas (usingisZodSchema) and leaves non‑Zod Standard Schema/JSON Schema alone.
Right now this logic is confusing and easy to misuse because props is sometimes a validator and sometimes a plain schema object.
- Maintainability |
react-sdk/src/model/component-metadata.ts:71-71
RegisterToolFnoverloads are a good step toward recovering type safety, but the concreteTamboTooltype has been reduced totool: (...args: any[]) => anyandtoolSchema: unknown. This discards the relationship between the overload variants and the publicTamboToolyou actually store and pass around.
As written:
- A user can construct a
TamboToolmanually (not viadefineTamboTool) with completely mismatchedtoolandtoolSchematypes and pass it intoregisterToolwithout compiler complaints. - Downstream utilities like
mapTamboToolToContextToolandtool-callercan no longer rely on value-level type information fromTamboTool, so you fall back toanyarguments and return values.
Given your goal of vendor‑agnostic schemas but strong typing for value‑level behavior, it would be more robust to make TamboTool itself generic over Args and Result with defaults, and have the schema field be structurally related but not strictly tied in the type system. defineTamboTool can then enforce alignment for the common cases while TamboTool remains a safer container type.
-
Maintainability |
react-sdk/src/testing/tools.ts:1-1
The newserializeRegistryimplementation centralizes schema conversion for tools and components, which is good, but there are a couple of behavior changes and edge cases worth tightening: -
All tools now serialize to a single
argsparameter instead of one parameter per argument. This is a breaking change from the previous Zod‑function-based behavior and may invalidate assumptions in any consumers that expectparam1,param2, etc. -
safeSchemaToJsonSchemacan returnundefinedon conversion failure, but you propagate that directly intoschemawithout surfacing the error. In snapshots or debugging output, anargsparameter withschema: undefinedis very hard to interpret. -
For non‑Standard Schema inputs, you unconditionally treat
toolSchemaas JSON Schema without validating it vialooksLikeJSONSchema, which is more permissive than the runtime registry code.
These differences between test serialization and runtime behavior can mask bugs or make tests pass when production would fail.
- Maintainability |
react-sdk/src/util/validate-schema.ts:147-185
You’ve reintroduced a localisStandardSchema/JSON Schema detection invalidate-schema.tsby checking fortypeof schema === "object" && "type" in schema, even though you already have a richerlooksLikeJSONSchemahelper inschema.ts. Keeping detection logic duplicated in different modules is brittle and increases the risk of divergent behavior.
For example, if you later tighten looksLikeJSONSchema but forget to update this check, assertNoRecordSchema may treat some objects as JSON Schema that the rest of the codebase does not, or vice versa.
- Maintainability |
react-sdk/src/util/schema.ts:141-193
isZod3FunctionSchema,isZod4FunctionSchema, andgetZodFunctionArgsinschema.tsare a central part of your Zod function handling now, but they still rely on undocumented internals like_zod.def.inputand_def.args. You’ve added some structure around them, which helps, but they are not fully type-safe and there’s no fallback when the expected properties are missing.
Given how much of the tool parameter logic now hangs off of these helpers, a small change in Zod’s internal representation could silently break argument extraction for function‑typed tools without any obvious error.
- Maintainability |
react-sdk/src/util/tool-caller.ts:78-81
WrappingregistryTool.toolin anasyncarrow here changes the call signature from “possibly sync or async” to “always returns a Promise”, even if the underlying implementation is synchronous. That’s generally fine, but it means the type-level relationship betweenTamboTool.toolandComponentContextTool.getComponentContextis now hidden behindanyand a coercing wrapper.
Given that you’re already using MaybeAsync in component-metadata.ts, it would be more consistent to type getComponentContext as returning a Promise and have the compiler enforce this contract, rather than forcing the async boundary in this helper.
Summary of changes
Summary of Changes
- Migrated backend and React SDK code to explicitly use
zod/v3andzod/v4entry points instead of unversionedzodimports. - Introduced a new schema utility module (
react-sdk/src/util/schema.ts) and tests (schema.test.ts) to support Standard Schema v1, vendor detection (Zod 3 & 4), JSON Schema heuristics, and conversion via@standard-community/standard-json. - Replaced Zod‑specific record validation (
assertNoZodRecord) with generalized JSON Schema–based validation (assertNoRecordSchema) and corresponding tests. - Refactored the Tambo tool and component model (
component-metadata.ts) to:- Support
StandardSchemaV1 | JSONSchema7(TamboSchema), - Add richer
TamboTool*variants (Zod3/Zod4/StandardSchema/JSONSchema), - Introduce
defineTamboToolhelper and inference utilities.
- Support
- Updated the registry, interactable providers, MCP provider, and testing utilities to:
- Use the new schema utilities and type guards,
- Treat tool and props schemas as Standard Schema or JSON Schema,
- Serialize tools/components via JSON Schema, including Zod function arg extraction.
- Tightened dependency metadata (especially around
zod,zod-to-json-schema, and@standard-community/standard-json) inpackage-lock.json,packages/backend/package.json,packages/core/package.json, andreact-sdk/package.json. - Updated README examples and tests to use
zod/v4and the new function schema API. - Removed legacy Zod‑specific validation files (
validate-zod-schema.tsand its tests`) and replaced them with Standard Schema–aware equivalents.
| export interface RegisterToolFn { | ||
| <Args extends $ZodFunctionIn, Returns extends $ZodType>( | ||
| tool: TamboToolZod4Function<Args, Returns>, | ||
| warnOnOverwrite?: boolean, | ||
| ): void; | ||
| <Args extends ZodTuple<any, any>, Returns extends ZodType>( | ||
| tool: TamboToolZod3Function<Args, Returns>, | ||
| warnOnOverwrite?: boolean, | ||
| ): void; | ||
| <Args extends StandardSchemaV1<any[]>, Returns extends StandardSchemaV1>( | ||
| tool: TamboToolStandardSchema<Args, Returns>, | ||
| warnOnOverwrite?: boolean, | ||
| ): void; | ||
| <Args extends unknown[], Returns = unknown>( | ||
| tool: TamboToolJSONSchema<Args, Returns>, | ||
| warnOnOverwrite?: boolean, | ||
| ): void; | ||
| // Fallback for pre-defined tools typed as TamboTool union | ||
| (tool: TamboTool, warnOnOverwrite?: boolean): void; | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
registerTool’s overloads and new TamboTool* variants are a good direction, but the implementation still treats tool.toolSchema as an opaque unknown and does not actually leverage the stronger generics to keep tool and toolSchema in sync.
Concretely:
- A caller can still supply a
toolwhose runtime signature doesn’t match the schema (e.g. wrong arg count or result type) and TypeScript will not complain unless they go throughdefineTamboToolcorrectly. - Even with
defineTamboTool, the registry happily stores tools that don’t conform to any of the variant-specific shapes, because the fallback overload(tool: TamboTool)accepts anyTamboToolBase.
This undermines part of the type-safety you’re trying to win back after relaxing TamboTool earlier, and makes it easier for mis-typed tools to slip into the registry unnoticed.
Suggestion
To better enforce the relationship between tool, its arguments, and toolSchema, restrict the public registration API to the typed variants and route all call sites through them.
For example:
- Export
RegisterToolFnanddefineTamboToolas the only supported ways to construct/register tools. - Make the fallback
(tool: TamboTool)overloadinternal(or remove it) so external callers cannot bypass the schema/arg coupling. - In your own code, update remaining
TamboToolliterals to go throughdefineTamboToolso type-checking kicks in at definition time.
If you’d like, I can go through the React SDK and convert the remaining bare TamboTool literals to defineTamboTool usages and tighten the overload set accordingly. Reply with @CharlieHelps yes please if you want that commit.
feat(react): add standard schema support
chore(react): update zod deps according to recommended service
fix(react): harden Zod function helpers with shared schema guard
Use the shared `isZodSchema` type guard from `schema.ts` in the Zod
function helpers to avoid blindly poking at internal properties on
unknown values.
- Import and use `isZodSchema` in `isZod4FunctionSchema`,
`isZod3FunctionSchema`, and `getZodFunctionArgs`
- Return early when the input does not look like a Zod schema
- Narrow internal access to `def` / `_def` only after the guard passes
This reduces the risk of runtime errors when tool schemas are not Zod
instances, which is increasingly important now that we support Standard
Schema and multiple schema shapes in the registry.
fix(react): tighten JSON schema detection
fix(react-sdk): treat any object with additionalProperties as record
Previously, `isRecordJsonSchema` only treated `object` schemas as records when they had `additionalProperties` and no explicit `properties`. This allowed mixed schemas (with both `properties` and `additionalProperties`) to slip through, even though they still represent dynamic-key records and are not supported by the Tambo backend.
This change:
- Updates `isRecordJsonSchema` to treat any `object` schema with `additionalProperties` defined as a schema object (not `true`/`false`) as a record, regardless of whether it also has `properties`.
- Adds a test to ensure schemas with both `properties` and `additionalProperties` are rejected with the existing record-type error message.
feat(react-sdk): add type safety/inference to registerTool function with TamboTool
fix(react-sdk): use versioned Zod function schema guards
apply changes from Charlie
remove complex type for TamboTool
remove type cast in favor of standard schema usage
feat(react-sdk): make tool schema extraction library-agnostic
- Add library-agnostic tuple detection via JSON Schema conversion
(supports Zod 3/4, Valibot, ArkType, and any Standard Schema library)
- Deprecate z.function() format with runtime warning
- Recommend {args: z.tuple([...]), returns: z.type()} format instead
- Remove TamboToolZod4Function and TamboToolStandardSchema types
- Add comprehensive tests for Zod 3, Zod 4, Valibot, and JSON Schema
- Update all production code and test fixtures to new format
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
4b70bd3 to
4cd8c4b
Compare
|
Review the following changes in direct dependencies. Learn more about Socket for GitHub.
|
Summary
zod-to-json-schemato@standard-community/standard-jsonfor vendor-agnostic Standard Schema to JSON Schema conversionzod-to-json-schemaKey Changes
src/util/schema.ts): Unified interface for Standard Schema and JSON Schema handlingvalidate-zod-schema.tstovalidate-schema.tswith Standard Schema supportTest plan
🤖 Generated with Claude Code