Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Improve metadata tracking across child-parent relationships#5578

Merged
colinhacks merged 1 commit intomainfrom
inherit-meta-across-refien
Dec 30, 2025
Merged

Improve metadata tracking across child-parent relationships#5578
colinhacks merged 1 commit intomainfrom
inherit-meta-across-refien

Conversation

@colinhacks
Copy link
Owner

@colinhacks colinhacks commented Dec 30, 2025

Fixes #5578

Copy link
Contributor

@pullfrog pullfrog bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Low urgency — The approach addresses a real pain point (metadata lost when .meta() precedes refinements like .min()). The implementation is sound, though I have some concerns about edge cases in the deduplication logic and whether the override behavior change is intentional.

The play.ts file changes appear to be debug scaffolding that should not be merged.

Pullfrog  | Fix all ➔Fix 👍s ➔View workflow runpullfrog.com𝕏

@@ -1,6 +1,19 @@
import * as z from "zod/v4";
import * as z from "./packages/zod/src/index.js";
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This looks like debug/exploration code that shouldn't be committed. Consider reverting this file or adding it to .gitignore.

if (refSchema.$ref) {
for (const key in schema) {
if (key === "$ref" || key === "allOf") continue;
if (key in refSeen.def! && JSON.stringify(schema[key]) === JSON.stringify(refSeen.def![key])) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

JSON.stringify comparison for equality is O(n) and can be slow for deeply nested schemas. It's also order-dependent for object properties. If this path is hot, consider a shallow key comparison or structural equality check. That said, this is finalization code that runs once per schema, so it may be acceptable.

jsonSchema: schema,
path: seen.path ?? [],
});
ctx.override({
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The !seen.isParent guard was removed, doubling the overrideCount in tests. Is this intentional? If users were relying on overrides not being called for parent schemas (e.g., to avoid double-processing), this is a breaking change.

// restore child's own properties (child wins)
Object.assign(schema, _cached);

const isParentRef = (zodSchema as any)._zod.parent === ref;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Using (zodSchema as any)._zod.parent bypasses type safety. Since zodSchema is typed as schemas.$ZodType, consider adding parent to the $ZodType interface if it's a supported property, or use a type guard.

Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR improves metadata tracking across child-parent schema relationships in Zod's JSON Schema generation. The main issue addressed is that metadata (like id and custom properties) was being lost when schemas were chained together, particularly when .meta() was called before constraint methods like .min().

Key Changes:

  • Enabled parent tracking in schema cloning by passing { parent: true } when .check() is called
  • Separated parent tracking from ref tracking in the JSON Schema generation process to handle cases where processors set a different ref than the parent chain
  • Added logic to propagate $ref from extracted parent schemas to their children

Reviewed changes

Copilot reviewed 6 out of 6 changed files in this pull request and generated 2 comments.

Show a summary per file
File Description
play.ts Test file demonstrating the metadata ordering issue being fixed
packages/zod/src/v4/mini/schemas.ts Added { parent: true } parameter to clone calls in .check() method
packages/zod/src/v4/classic/schemas.ts Added { parent: true } parameter to clone calls in .check() method
packages/zod/src/v4/core/to-json-schema.ts Core changes: added parent field to Seen interface, moved parent processing after processor execution, enhanced flattening logic to handle parent-child ref propagation
packages/zod/src/v4/core/json-schema-processors.ts Optimized file processor to avoid duplicating common properties in anyOf branches
packages/zod/src/v4/classic/tests/to-json-schema.test.ts Updated test expectations for override count (now runs on all schemas), added test for wrapper with id, updated file processor test expectations

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +413 to +416
if (refSchema.$ref) {
for (const key in schema) {
if (key === "$ref" || key === "allOf") continue;
if (key in refSeen.def! && JSON.stringify(schema[key]) === JSON.stringify(refSeen.def![key])) {
Copy link

Copilot AI Dec 30, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Potential runtime error: The non-null assertion on refSeen.def! is unsafe. This code path is reached when refSchema.$ref exists (line 413). However, $ref can be set on a schema in two ways: (1) during extractToDef which also sets def, or (2) at line 431 which propagates $ref from a parent without setting def.

If schema A's parent B was extracted (has $ref and def), and A itself was not extracted, then line 431 sets A.schema.$ref = B.schema.$ref but A.def remains undefined. If another schema C references A, when processing C's ref (A) at line 413-420, the condition refSchema.$ref will be true, but refSeen.def will be undefined, causing a runtime error.

Consider checking if refSeen.def exists before accessing it, or ensuring that when $ref is propagated at line 431, the def is also set appropriately.

Suggested change
if (refSchema.$ref) {
for (const key in schema) {
if (key === "$ref" || key === "allOf") continue;
if (key in refSeen.def! && JSON.stringify(schema[key]) === JSON.stringify(refSeen.def![key])) {
if (refSchema.$ref && refSeen.def) {
for (const key in schema) {
if (key === "$ref" || key === "allOf") continue;
if (key in refSeen.def && JSON.stringify(schema[key]) === JSON.stringify(refSeen.def[key])) {

Copilot uses AI. Check for mistakes.
Copy link

@brunolemos brunolemos Jan 18, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@colinhacks I'm getting this exact error when trying to upgrade from 4.2.1 to 4.3.5:

node_modules/.pnpm/[email protected]/node_modules/zod/v4/core/to-json-schema.cjs:267
                    if (key in refSeen.def && JSON.stringify(schema[key]) === JSON.stringify(refSeen.def[key])) {
                            ^
TypeError: Cannot use 'in' operator to search for 'id' in undefined
    at flattenRef (node_modules/.pnpm/[email protected]/node_modules/zod/v4/core/to-json-schema.cjs:267:29)

Maybe it should have the safety checks suggested by copilot?
I can confirm adding refSeen.def && in the beginning of line 416 fixed the issue for me.

PR: #5644

});

expect(overrideCount).toBe(6);
expect(overrideCount).toBe(12);
Copy link

Copilot AI Dec 30, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The override count expectation changed from 6 to 12. This change reflects that the override function now runs on parent schemas as well, which were previously skipped (by checking !seen.isParent). This doubles the count because each schema in the chain is now visited. While this aligns with the code change in the finalize function (removing the isParent check), consider adding a comment explaining why this number doubled to make the test more maintainable and document the expected behavior.

Copilot uses AI. Check for mistakes.
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.

2 participants