You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
isProvablyObjectShapedRoot does not recognize $defs/$ref to typeless object-shaped schemas (e.g. named discriminatedUnion) #3094
In commit 5820f796 (#3081), isProvablyObjectShapedRoot in packages/core-internal/src/util/standardSchema.ts was updated to follow a same-document $ref into $defs / definitions, checking target?.type === 'object'.
When a $defs / definitions entry is a typeless object-shaped schema like z.discriminatedUnion('kind', [z.object(...), z.object(...)]).meta({ id: 'Result' }) (or a union/intersection of objects, or an object schema with properties that omits type: 'object'), target.type === 'object' returns false and wraps structuredContent in { result: ... }, even though the identical schema written inline without .meta({ id }) returns true.
Should isProvablyObjectShapedRoot also check isProvablyObjectShapedRoot(target, {}) when resolving a $ref target?
The answer to your question is yes, and the reason is a naming invariant rather than a preference: the same schema must get the same answer whether or not it carries an id.
Right now the two paths disagree. The inline call runs the full predicate, while the $ref branch only tests target.type === 'object', so any referenced schema that is object-shaped without that keyword flips the result. z.discriminatedUnion(...).meta({ id }) is the loudest case, but a referenced z.object() with properties and no type, and any allOf composition of objects, land on the same side of the branch. That is exactly the class the predicate exists to resolve, so recursing on the target with the same function is the consistent fix.
Two things to get right when you do it. Recursing through $defs needs a visited set keyed on the pointer, because a self-referential schema will otherwise loop, and the $ref cycle case is common in generated schemas. And since the visible effect of the boolean is the { result: ... } wrapper, the regression test should assert on the shape of structuredContent rather than on the helper's return value: build one schema inline and the identical schema named through $defs, then assert the two calls produce equal structured content. That test fails today, passes after the change, and stays honest if the predicate is refactored again later.
Worth deciding explicitly whether a referenced non-object, such as a named primitive, should stay false, since the same recursion then needs a deliberate terminal case rather than an implicit fallthrough. Stating it in the doc comment prevents the next reader from assuming the $ref branch mirrors the inline path, which is the assumption that produced this report.
Activity
Ben-Home commentedon Oct 28, 2026
The answer to your question is yes, and the reason is a naming invariant rather than a preference: the same schema must get the same answer whether or not it carries an
id.Right now the two paths disagree. The inline call runs the full predicate, while the
$refbranch only teststarget.type === 'object', so any referenced schema that is object-shaped without that keyword flips the result.z.discriminatedUnion(...).meta({ id })is the loudest case, but a referencedz.object()withpropertiesand notype, and anyallOfcomposition of objects, land on the same side of the branch. That is exactly the class the predicate exists to resolve, so recursing on the target with the same function is the consistent fix.Two things to get right when you do it. Recursing through
$defsneeds a visited set keyed on the pointer, because a self-referential schema will otherwise loop, and the$refcycle case is common in generated schemas. And since the visible effect of the boolean is the{ result: ... }wrapper, the regression test should assert on the shape ofstructuredContentrather than on the helper's return value: build one schema inline and the identical schema named through$defs, then assert the two calls produce equal structured content. That test fails today, passes after the change, and stays honest if the predicate is refactored again later.Worth deciding explicitly whether a referenced non-object, such as a named primitive, should stay false, since the same recursion then needs a deliberate terminal case rather than an implicit fallthrough. Stating it in the doc comment prevents the next reader from assuming the
$refbranch mirrors the inline path, which is the assumption that produced this report.