Skip to content

fix(valibot): emit acyclic schema refs before dependents#4077

Open
yslpn wants to merge 1 commit into
hey-api:mainfrom
yslpn:fix/valibot-acyclic-ref-order
Open

fix(valibot): emit acyclic schema refs before dependents#4077
yslpn wants to merge 1 commit into
hey-api:mainfrom
yslpn:fix/valibot-acyclic-ref-order

Conversation

@yslpn

@yslpn yslpn commented Jun 16, 2026

Copy link
Copy Markdown

Fix Valibot acyclic reference ordering

Summary

  • Emit acyclic Valibot schema references before the schema that uses them.
  • Keep cyclic and self-referential schemas lazy, while avoiding v.lazy() for forward references that can be safely emitted first.
  • Add a 3.1.x Valibot fixture where a parent schema is declared before the child schema it references.

This prevents generated validators from becoming v.GenericSchema solely because a referenced schema appears later in the OpenAPI document.

Related issue

Related to #4027.

Testing

  • pnpm exec vitest run packages/openapi-ts-tests/valibot/v1/test/3.1.x.test.ts
  • pnpm exec tsc --noEmit --pretty false --project packages/openapi-ts/tsconfig.json
  • pnpm exec tsc --noEmit --pretty false --project packages/openapi-ts-tests/valibot/v1/tsconfig.json

@bolt-new-by-stackblitz

Copy link
Copy Markdown

Review PR in StackBlitz Codeflow Run & review this pull request in StackBlitz Codeflow.

@changeset-bot

changeset-bot Bot commented Jun 16, 2026

Copy link
Copy Markdown

⚠️ No Changeset found

Latest commit: dc0bc41

Merging this PR will not cause a version bump for any packages. If these changes should not result in a new version, you're good to go. If these changes should result in a version bump, you need to add a changeset.

This PR includes no changesets

When changesets are added to this PR, you'll see the packages that this PR includes changesets for and the associated semver types

Click here to learn what changesets are, and how to add one.

Click here if you're a maintainer who wants to add a changeset to this PR

@vercel

vercel Bot commented Jun 16, 2026

Copy link
Copy Markdown

@yslpn is attempting to deploy a commit to the Hey API Team on Vercel.

A member of the Team first needs to authorize it.

@dosubot dosubot Bot added the size:M This PR changes 30-99 lines, ignoring generated files. label Jun 16, 2026
@mrlubos

mrlubos commented Jun 16, 2026

Copy link
Copy Markdown
Member

@yslpn did you consider any other approaches?

@codecov

codecov Bot commented Jun 16, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 0% with 25 lines in your changes missing coverage. Please review.
✅ Project coverage is 38.74%. Comparing base (9f539eb) to head (dc0bc41).

Files with missing lines Patch % Lines
...ges/openapi-ts/src/plugins/valibot/v1/processor.ts 0.00% 18 Missing and 4 partials ⚠️
...kages/openapi-ts/src/plugins/valibot/v1/visitor.ts 0.00% 3 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main    #4077      +/-   ##
==========================================
- Coverage   38.78%   38.74%   -0.05%     
==========================================
  Files         627      627              
  Lines       21897    21920      +23     
  Branches     6408     6414       +6     
==========================================
  Hits         8492     8492              
- Misses      10921    10940      +19     
- Partials     2484     2488       +4     
Flag Coverage Δ
unittests 38.74% <0.00%> (-0.05%) ⬇️

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@pullfrog pullfrog Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

✅ No new issues found.

Reviewed changes — emits acyclic Valibot schema refs before their dependents, eliminating unnecessary v.lazy() wrappers and v.GenericSchema type annotations when a referenced schema appears later in the document.

  • Add isCyclicReference cycle detection in the processor using transitiveDependencies from the spec graph, plus a direct self-reference check
  • Add ensureReferenceRegistered that recursively emits acyclic target schemas before the referencing schema is fully walked
  • Thread ensureReferenceRegistered into the visitor via VisitorConfig, called by reference() before the isRegistered / v.lazy() fallback
  • Normalize $ref via normalizeJsonPointer in both the visitor query and processor lookup, fixing a latent mismatch where un-normalized refs (with escaped chars) could bypass symbol registration checks
  • Add test fixture (validators-acyclic-ref-order.yaml) with Parent declared before Child and a snapshot confirming vChild emits first and vParent references it directly

ℹ️ Nitpicks

  • Zod has the same forward-reference issue and resourceId mismatch. packages/openapi-ts/src/plugins/zod/v4/visitor.ts:244 (and v3/mini) uses raw $ref without normalizeJsonPointer and lacks any acyclic-forward-emission path — the Zod plugin could benefit from the same treatment in a follow-up.

  • isCyclicReference falls back to false when transitiveDependencies is absent. processor.ts:28 chains through ?. and ?? false, so a missing graph silently treats all non-self refs as acyclic. In practice the graph is always built for OAS specs, but if it were absent, mutual refs would hit the throw on line 66 instead of falling through to v.lazy(). A comment or guarding this behind graph presence could help future readers, though it's not a risk today.

Pullfrog  | View workflow run | Using DeepSeek Pro (free via Pullfrog for OSS) | 𝕏

@pkg-pr-new

pkg-pr-new Bot commented Jun 16, 2026

Copy link
Copy Markdown

Open in StackBlitz

@hey-api/codegen-core

npm i https://pkg.pr.new/@hey-api/codegen-core@4077

@hey-api/json-schema-ref-parser

npm i https://pkg.pr.new/@hey-api/json-schema-ref-parser@4077

@hey-api/nuxt

npm i https://pkg.pr.new/@hey-api/nuxt@4077

@hey-api/openapi-ts

npm i https://pkg.pr.new/@hey-api/openapi-ts@4077

@hey-api/shared

npm i https://pkg.pr.new/@hey-api/shared@4077

@hey-api/spec-types

npm i https://pkg.pr.new/@hey-api/spec-types@4077

@hey-api/types

npm i https://pkg.pr.new/@hey-api/types@4077

@hey-api/vite-plugin

npm i https://pkg.pr.new/@hey-api/vite-plugin@4077

commit: dc0bc41

@yslpn

yslpn commented Jun 16, 2026

Copy link
Copy Markdown
Author

@yslpn did you consider any other approaches?

@mrlubos Yes, I initially thought about properly typing all the lazy schemes. It turned out to be quite complicated. I had to change a lot of files, and it's not ready yet, so I'm not sure it can be merged.

The current fix removes lazy for cases where it's not needed. In my example, there's no cyclic dependency or anything similar. So this can be considered a point fix for my case.

But the lazy typing issue can't be resolved yet #4027. I've been developing a new recursive scheme for valibot for some time open-circle/valibot#1504

Perhaps after its implementation, the fix can be simplified.

@mrlubos

mrlubos commented Jun 16, 2026

Copy link
Copy Markdown
Member

I initially thought about properly typing all the lazy schemes. It turned out to be quite complicated.

Indeed!

The current fix removes lazy for cases where it's not needed. In my example, there's no cyclic dependency or anything similar. So this can be considered a point fix for my case.

My main concern with this is that it introduces a bespoke pattern into a single plugin. The problem seems like something that should be recurring though? Even Pullfrog pointed out Zod which would be my first question too. But the same issue should exist with Faker and many other plugins. Do you agree? If so, I'd be looking to introduce this as a pattern or API other plugins could implement. It would make refactoring and understanding code much easier

@yslpn

yslpn commented Jun 16, 2026

Copy link
Copy Markdown
Author

I initially thought about properly typing all the lazy schemes. It turned out to be quite complicated.

Indeed!

The current fix removes lazy for cases where it's not needed. In my example, there's no cyclic dependency or anything similar. So this can be considered a point fix for my case.

My main concern with this is that it introduces a bespoke pattern into a single plugin. The problem seems like something that should be recurring though? Even Pullfrog pointed out Zod which would be my first question too. But the same issue should exist with Faker and many other plugins. Do you agree? If so, I'd be looking to introduce this as a pattern or API other plugins could implement. It would make refactoring and understanding code much easier

Yes, I agree. This should be a shared pattern/API rather than a Valibot-only solution.

I kept this scoped because I was focused on the concrete issue I hit, but I’m not familiar enough with the broader plugin architecture to do that refactor properly.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size:M This PR changes 30-99 lines, ignoring generated files.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants