这是indexloc提供的服务,不要输入任何密码
Skip to content

Valibot: correctly support format: int64 #2344

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

Merged
merged 11 commits into from
Jul 24, 2025
Merged

Conversation

Daschi1
Copy link
Contributor

@Daschi1 Daschi1 commented Jul 21, 2025

Resolves #2194

Copy link

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

Copy link

changeset-bot bot commented Jul 21, 2025

🦋 Changeset detected

Latest commit: fb7b724

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 1 package
Name Type
@hey-api/openapi-ts Patch

Not sure what this means? Click here to learn what changesets are.

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

Copy link

vercel bot commented Jul 21, 2025

The latest updates on your projects. Learn more about Vercel for Git ↗︎

Name Status Preview Comments Updated (UTC)
hey-api-docs ✅ Ready (Inspect) Visit Preview 💬 Add feedback Jul 24, 2025 9:48am

@Daschi1
Copy link
Contributor Author

Daschi1 commented Jul 21, 2025

TODO

  • Figure out why v.union is generated with newlines despite multiLine: false
  • Work through previous integer generation code and decide how to change it (if at all) to work with the new integer format logic
  • Look at failing test snapshots to decide if they can be updated or if further code changes are necessary
  • Figure out how to integrate actual unit tests for generated validation behavior

@Daschi1
Copy link
Contributor Author

Daschi1 commented Jul 21, 2025

@mrlubos I got it to work and have a first draft, but I have a few questions:

  1. The createArrayLiteralExpression multiLine parameter is resolved as

    multiLine || (!Array.isArray(elements[0]) && typeof elements[0] === 'object')

    instead of just using the passed value. I assume it is intended and fine that v.union definitions generate like this:

    v.union([
      v.number(),
      v.string(),
      v.bigint()
    ])

    rather than as a one liner v.union([v.number(), v.string(), v.bigint()])?

  2. In the Valibot plugin.ts numberTypeToValibotSchema function what is this code path for?

    if (typeof schema.const === 'number') {
      // TODO: parser - handle bigint constants
      const expression = compiler.callExpression({
        functionName: compiler.propertyAccessExpression({
          expression: identifiers.v,
          name: identifiers.schemas.literal,
        }),
        parameters: [compiler.ots.number(schema.const)],
      });
      return expression;
    }

    I am not clear on its purpose. If const in the schema means the property must equal that value, why generate code for it here and what does the TODO refer to?

  3. Unrelated to this issue but an observation: in the 3.1.x spec schema-const.yaml test snapshot and schema-const/valibot.gen.ts, there is no literal check generated for bigint. Is that something I should address here or is it a separate bug? Could it relate to the TODO above? Also, is there a reason the other checks like min / max are generated with BigInt(123) instead of using the literal 123n?

  4. I would like to implement a Vitest suite to validate the generated Valibot pipelines (for example to check that each scenario accepts and rejects the correct values). Where in the project structure would be the best place for those tests? Are there any examples already present I can follow in terms of code style, etc. ...?

@mrlubos
Copy link
Member

mrlubos commented Jul 22, 2025

Hey!

  1. That looks fine to me. Sometimes it's a pain to get right with TypeScript Compiler API, but it might be a legitimate bug too. I wouldn't worry about it, it's not a big deal.
  2. The current logic wouldn't correctly generate output for bigint values such as 123n, there's a similar TODO in the Zod plugin. const is a way in JSON Schema to restrict a value to a single value. If it's defined, we know no other value will be legal and we don't have to deal with other constraints – x is either equal to const or its validation should fail.
  3. That's correct, it's related to point 2. If you handle it, that would be certainly appreciated! As for your last question, I'd need to see an example to understand, can't tell if there's a good reason for that.
  4. I believe there are no existing examples. That's a noble aspiration which I fully support though, maybe openapi-ts-tests/test/<new_folder>? I imagine we'd want to do something similar for each plugin and these tests would be isolated per plugin, unlike snapshots?

@Daschi1
Copy link
Contributor Author

Daschi1 commented Jul 22, 2025

Hey @mrlubos, thanks again for your feedback! A few follow-ups:

  1. I just want to clarify this, because it is caused by the following code in openapi-ts/packages/openapi-ts/src/compiler/types.ts at lines 536–537. The createArrayLiteralExpression function has this logic:
const expression = ts.factory.createArrayLiteralExpression(
  elements
    .map((value) => (isTsNode(value) ? value : toExpression({ value })))
    .filter(isType<ts.Expression>),
  // multiline if array contains objects
  multiLine ||
    (!Array.isArray(elements[0]) && typeof elements[0] === 'object'),
);

The key issue is in the condition:

multiLine || (!Array.isArray(elements[0]) && typeof elements[0] === 'object')

This forces multiLine to be true if the first element in the array is an object. In my case, the array elements are TypeScript CallExpression nodes (like v.number(), v.string(), v.bigint()), which are JavaScript objects. Therefore typeof elements[0] === 'object' evaluates to true, overriding the explicit multiLine: false setting:

const unionExpression = compiler.callExpression({
  functionName: compiler.propertyAccessExpression({
    expression: identifiers.v,
    name: identifiers.schemas.union,
  }),
  parameters: [
    compiler.arrayLiteralExpression({
      elements: [
        compiler.callExpression({
          functionName: compiler.propertyAccessExpression({
            expression: identifiers.v,
            name: identifiers.schemas.number,
          }),
        }),
        compiler.callExpression({
          functionName: compiler.propertyAccessExpression({
            expression: identifiers.v,
            name: identifiers.schemas.string,
          }),
        }),
        compiler.callExpression({
          functionName: compiler.propertyAccessExpression({
            expression: identifiers.v,
            name: identifiers.schemas.bigint,
          }),
        }),
      ],
      multiLine: false,
    }),
  ],
});

This is why multiLine: false is ignored or overwritten. When I remove the second clause, everything is generated correctly (i.e., respecting the passed multiLine value). I wanted to clarify if this behavior is intended or might be an unintended side effect. If it is fine to leave it as is, I will not pursue it further.

  1. Thank you for the explanation. I overlooked the typeof check in that if condition, and it makes perfect sense now. Regarding the TODO, I will try to handle it accordingly by supporting bigint constants (for example v.literal(123456789n)).

  2. In JavaScript, you can create a BigInt either via BigInt(123456789) or by using the n literal suffix (123456789n). The literal suffix is like a shorthand for calling the constructor (not quite a shorthand, but rather built-in JS grammar). I used the n suffix for the generated min/max validations, but currently your code uses the BigInt constructor (as seen in the validator-bigint-min-max test snapshot). Excerpt:

export const vFoo = v.object({
  foo: v.optional(
    v.pipe(
      v.union([
        v.number(),
        v.string(),
        v.bigint()
      ]),
      v.transform(x => BigInt(x)),
      v.minValue(-9223372036854775808n, 'Invalid value: Expected int64 to be >= -2^63'),
      v.maxValue(9223372036854775807n, 'Invalid value: Expected int64 to be <= 2^63-1'),
      v.minValue(BigInt(0)),
      v.maxValue(BigInt(100))
    )
  )
});

You can see my logic for format validation, then the additional schema min/max limitation. I want to ask if one style is preferred over the other, if a mix is acceptable, or if we should standardize on one.

  1. I think grouping tests by plugin makes sense. I will get started with some accept/reject Vitest cases and see where it takes me.

@mrlubos
Copy link
Member

mrlubos commented Jul 22, 2025

  1. I say leave it. That compiler stuff should be refactored at some point. But if you're curious why, I think it's a leftover from when this function accepted only JavaScript objects. Later we probably added AST node support and that broke that condition. You can see above that line we explicitly check for node type.

  2. I prefer BigInt for consistency. I'd expect a code formatter to swap these for the other syntax if they're truly equivalent and people prefer that syntax

Copy link

pkg-pr-new bot commented Jul 22, 2025

Open in StackBlitz

npm i https://pkg.pr.new/hey-api/openapi-ts/@hey-api/nuxt@2344
npm i https://pkg.pr.new/hey-api/openapi-ts/@hey-api/openapi-ts@2344
npm i https://pkg.pr.new/hey-api/openapi-ts/@hey-api/vite-plugin@2344

commit: fb7b724

Copy link

codecov bot commented Jul 22, 2025

Codecov Report

Attention: Patch coverage is 27.15517% with 169 lines in your changes missing coverage. Please review.

Project coverage is 22.52%. Comparing base (08f219e) to head (fb7b724).
Report is 12 commits behind head on main.

Files with missing lines Patch % Lines
packages/openapi-ts/src/plugins/valibot/plugin.ts 0.67% 147 Missing ⚠️
...s/openapi-ts/src/plugins/valibot/number-helpers.ts 73.80% 22 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main    #2344      +/-   ##
==========================================
+ Coverage   22.47%   22.52%   +0.05%     
==========================================
  Files         323      324       +1     
  Lines       31662    31857     +195     
  Branches     1232     1232              
==========================================
+ Hits         7115     7177      +62     
- Misses      24538    24671     +133     
  Partials        9        9              
Flag Coverage Δ
unittests 22.52% <27.15%> (+0.05%) ⬆️

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

☔ View full report in Codecov by Sentry.
📢 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.

@mrlubos
Copy link
Member

mrlubos commented Jul 23, 2025

@Daschi1 I moved tests around to accommodate both Zod 4 and Zod 3. Whatever was in packages/openapi-ts-tests is now in packages/openapi-ts-tests/main with the exception of specs which moved from packages/openapi-ts-tests/test/spec to packages/openapi-ts-tests/specs. Hopefully it's straightforward to pull the latest changes!

@Daschi1
Copy link
Contributor Author

Daschi1 commented Jul 23, 2025

@Daschi1 I moved tests around to accommodate both Zod 4 and Zod 3. Whatever was in packages/openapi-ts-tests is now in packages/openapi-ts-tests/main with the exception of specs which moved from packages/openapi-ts-tests/test/spec to packages/openapi-ts-tests/specs. Hopefully it's straightforward to pull the latest changes!

I just finished implementing the tests, thank you for the heads up! I'll resolve the merge conflicts later today and let you know once it's ready for review.

@Daschi1
Copy link
Contributor Author

Daschi1 commented Jul 23, 2025

@Daschi1 I moved tests around to accommodate both Zod 4 and Zod 3. Whatever was in packages/openapi-ts-tests is now in packages/openapi-ts-tests/main with the exception of specs which moved from packages/openapi-ts-tests/test/spec to packages/openapi-ts-tests/specs. Hopefully it's straightforward to pull the latest changes!

Hi @mrlubos,

I tried to integrate the recent changes over the last few hours, but honestly I struggled a lot, mainly due to my limited Git experience with large merges. I made some updates and now there are no merge conflicts with main, but I'm not sure if all my changes were applied correctly.

The PR now shows over 3,000 changed files because of the directory refactor, and I don't know if that is expected. Also, some tests that pull schemas are failing with 404 errors. I suspect it's because those files were moved, but I'm not sure if that is on my end or if the tests need adjustment.

I'm out of ideas and would really appreciate any guidance or assistance you can offer. I apologize if I overcomplicated this merge. My last option might be to close this PR and open a new one based on the latest code changes where I manually reimplement all my changes, so I can be sure everything is in the right place.

Thank you for your patience and any help you can provide!

@mrlubos
Copy link
Member

mrlubos commented Jul 24, 2025

Sorry for that! If resolving conflicts is a pain, I'd roll back your branch to commit aa5b7db1adab52b096dcad5ae2acef06bdf5b6ec so you get a cleaner diff.

From there, I'd start with a new branch based off main and move files over from your branch with git restore --source=<your-branch> <files>. That will give you a clean diff on top of the latest.

The only files you won't be able to move that way will probably be the changes to tests and specs. However, you should be able to re-apply those changes manually, I think it was just a few files.

Do NOT move snapshots over, they'll only be getting in the way. Instead, re-run pnpm openapi-ts-tests test:update once you've moved the source files over and built the library and that will give you the correct snapshots without causing merge conflicts.

@mrlubos
Copy link
Member

mrlubos commented Jul 24, 2025

You also added a commit feaf4d83b9c56448faf09a8ffefd13aa9d00c112 later, that one should be easily movable too

Copy link
Member

Choose a reason for hiding this comment

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

Did you generate this file with an LLM?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yes, for the final implementation I used Sonnet 4. I first wrote a few tests myself to establish the style and specified the conditions I wanted it to cover, then had it generate the rest based on that. I reviewed the result afterwards and it looked good to me. Did you notice anything that seems off?

@@ -253,6 +253,74 @@ const nullTypeToValibotSchema = (_props: {
return expression;
};

// Integer format ranges and properties
Copy link
Member

Choose a reason for hiding this comment

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

What do you think about moving these helpers and constants into their own file?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Sure, that seems reasonable. plugin.ts is already quite large. Where would you prefer I put them? In the same directory or somewhere more central?

Copy link
Member

Choose a reason for hiding this comment

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

Same directory is fine, thanks!

Copy link
Contributor Author

Choose a reason for hiding this comment

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

d573e06
I extracted them into their own file number-helpers.ts. Does this look good to you?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Do you think I should update the formats.test.ts do use the helper as well?

@mrlubos
Copy link
Member

mrlubos commented Jul 24, 2025

@Daschi1 CI tests are failing but your pull request might be okay. Stuff has moved around yesterday and I didn't fix the CI yet

@mrlubos mrlubos mentioned this pull request Jul 24, 2025
@Daschi1 Daschi1 marked this pull request as ready for review July 24, 2025 06:28
@Daschi1
Copy link
Contributor Author

Daschi1 commented Jul 24, 2025

@Daschi1 CI tests are failing but your pull request might be okay. Stuff has moved around yesterday and I didn't fix the CI yet

Yeah I noticed, some tests are failing because they attempt to download specs that no longer exist, which causes a 404. For example:

test/index.test.ts > index > downloads and parses v2 without issues
Generating from https://raw.githubusercontent.com/hey-api/openapi-ts/main/packages/openapi-ts-tests/test/spec/v2.json
Error: Request failed with status 404: Not Found

test/index.test.ts > index > downloads and parses v3 without issues
Generating from https://raw.githubusercontent.com/hey-api/openapi-ts/main/packages/openapi-ts-tests/test/spec/v3.json
Error: Request failed with status 404: Not Found

@Daschi1
Copy link
Contributor Author

Daschi1 commented Jul 24, 2025

Sorry for that! If resolving conflicts is a pain, I'd roll back your branch to commit aa5b7db1adab52b096dcad5ae2acef06bdf5b6ec so you get a cleaner diff.

From there, I'd start with a new branch based off main and move files over from your branch with git restore --source=<your-branch> <files>. That will give you a clean diff on top of the latest.

The only files you won't be able to move that way will probably be the changes to tests and specs. However, you should be able to re-apply those changes manually, I think it was just a few files.

Do NOT move snapshots over, they'll only be getting in the way. Instead, re-run pnpm openapi-ts-tests test:update once you've moved the source files over and built the library and that will give you the correct snapshots without causing merge conflicts.

Thank you so much for that! That helped tremendously and the git restore command is definitely handy, I didn't know about it before. Everything should now be up to date with my changes, so I've marked the PR as ready for review. I am looking forward to your feedback, and if you have any questions or need adjustments I am happy to accommodate to get this merged :)

it('should accept exact const value', () => {
const result = v.safeParse(generatedSchemas.vNumberNoFormat, 42.5);
expect(result.success).toBe(true);
if (result.success) {
Copy link
Member

Choose a reason for hiding this comment

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

I think this is a redundant condition in these test files. If the previous line fails, this should never execute. And even if it does, it doesn't matter since the previous one is already failing

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yes you are right. I am validating something that would essentially only test if Valibot itself works correctly rather than our generated plugin output. I will remove the redundant checks.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

a7df1f2
I removed the redundant checks from const-values.test.ts and also min-max-constraints.test.ts. In formats.test.ts I am specifically checking for the error message defined in plugin.ts and also verifying the result type to ensure the BigInt conversions are happening as expected. I think this is fine to keep, but let me know if you would prefer me to change that as well.

Copy link
Member

Choose a reason for hiding this comment

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

The assertions themselves are fine, but the if conditions are redundant for the same reason I mentioned above. It makes the file unnecessarily large and harder to read

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Oh, I see now. I misunderstood earlier and thought you were referring to the assertions rather than the if clauses themselves. I have removed the if conditions and kept the assertions: 4147808

@mrlubos mrlubos changed the title Valibot: correctly support format: int64 Valibot: correctly support format: int64 Jul 24, 2025
Copy link
Member

@mrlubos mrlubos left a comment

Choose a reason for hiding this comment

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

Looks good! There are a few remaining if conditions, can be merged after those are cleaned up

@Daschi1
Copy link
Contributor Author

Daschi1 commented Jul 24, 2025

Looks good! There are a few remaining if conditions, can be merged after those are cleaned up

Oops, I had to undo some changes locally earlier and it looks like I was a bit too generous with my ctrl‑z :D
I pushed a fix now.

Copy link
Member

@mrlubos mrlubos left a comment

Choose a reason for hiding this comment

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

This looks good @Daschi1, thank you! Let's see what the people say once they use it 🛳️

@mrlubos mrlubos merged commit d780a69 into hey-api:main Jul 24, 2025
17 checks passed
@github-actions github-actions bot mentioned this pull request Jul 24, 2025
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.

Valibot: correctly support format: int64
2 participants