-
-
Notifications
You must be signed in to change notification settings - Fork 204
Description
Description:
A problem arises when an OpenAPI definition has format: int64
, as is the case for the Repository
object in the /user/repositories
endpoint. One of the relevant variables is:
"size": {
"type": "integer",
"format": "int64",
"x-go-name": "Size"
}
The Valibot plugin currently generates:
size: v.optional(v.bigint()),
However, this will throw the following error:
{
kind: 'schema',
type: 'bigint',
input: 328,
expected: 'bigint',
received: '328',
message: 'Invalid type: Expected bigint but received 328',
requirement: undefined,
path: [Array],
issues: undefined,
lang: undefined,
abortEarly: undefined,
abortPipeEarly: undefined
},
The API actually returns (only relevant part):
// ...
"mirror": false,
"size": 328,
"language": "Swift",
// ...
I think the problem comes from the fact that Valibot expects an actual BigInt
value (for example 328n
), which is not possible since JSON disallows that suffix on numeric literals (hence the n
will never be there). See https://valibot.dev/api/bigint/
Background
Looking at the OpenAPI spec section for data types (https://spec.openapis.org/oas/latest.html#data-types), it states that format: int64
is considered to be a
signed 64 bits (a.k.a long)
The linked Format Registry (https://spec.openapis.org/registry/format/) permits int64
values to be either JSON numbers or strings. Further, the OpenAPI spec says that type: integer
is a valid substitution for type: number
, essentially giving three data types (number
, integer
, and string
) for which the int64
format specifier can apply.
Note: the spec itself only allows number
(and, by consequence, integer
) as a type. Only in the registry is string
defined as an extension. The registry also specifies:
The existence of a format in this registry DOES NOT require tools to implement it.
Technically, to be fully OpenAPI spec compliant, support for quoted strings is optional. However, in my opinion it is necessary to implement this extension, since as a code generator for OpenAPI I would expect it to work with a variety of APIs, some of which might follow this extended format registry. The tradeoff to implement this in this context is minimal, as the example below will show.
Proposed solution
A Valibot pipeline should be generated that handles all scenarios allowed by the OpenAPI spec and registry. My proposal is:
// Define signed 64-bit range constants
const INT64_MIN = BigInt('-9223372036854775808');
const INT64_MAX = BigInt('9223372036854775807');
const Int64 = v.pipe(
// 1) accept a JSON number, a string, or a BigInt literal
v.union([ v.number(), v.string(), v.bigint() ]),
// 2) convert into a JS bigint
v.transform((x) => BigInt(x)),
// 3) enforce the signed 64-bit range
v.minValue(INT64_MIN, 'Invalid value: Expected int64 to be >= -2^63'),
v.maxValue(INT64_MAX, 'Invalid value: Expected int64 to be <= 2^63-1')
);
Writing a quick Vitest to validate the pipeline looks like this:
import * as v from 'valibot';
import {describe, expect, it} from 'vitest';
// Define the Int64 (signed long) constants
const INT64_MIN = BigInt('-9223372036854775808');
const INT64_MAX = BigInt('9223372036854775807');
const Int64 = v.pipe(
// 1) accept either a JSON number, a string, or a bigint
// bigint is needed to also validate JSON that has values with the 'n' BigInt literal
v.union([v.number(), v.string(), v.bigint()]),
// 2) convert into a JS bigint
v.transform((x) => BigInt(x)),
// 3) enforce the signed 64-bit range
v.minValue(INT64_MIN, 'Invalid value: Expected int64 to be >= -2^63'),
v.maxValue(INT64_MAX, 'Invalid value: Expected int64 to be <= 2^63-1')
);
// Create a test schema with an Int64 field
const TestSchema = v.object({
test: Int64
});
// Tests for the Int64 Validator
describe('Int64 Validator', () => {
describe('Valid values', () => {
describe('Within range', () => {
it('should validate a regular number', () => {
const input = {test: 123456789};
const result = v.parse(TestSchema, input);
expect(result.test).toBe(BigInt('123456789'));
});
it('should validate a string number', () => {
const input = {test: "987654321"};
const result = v.parse(TestSchema, input);
expect(result.test).toBe(BigInt('987654321'));
});
it('should validate a BigInt with n literal', () => {
const input = {test: 123456789n};
const result = v.parse(TestSchema, input);
expect(result.test).toBe(BigInt('123456789'));
});
});
describe('Boundary values', () => {
describe('Maximum values', () => {
it('should validate the maximum valid value as string', () => {
const input = {test: "9223372036854775807"};
const result = v.parse(TestSchema, input);
expect(result.test).toBe(BigInt('9223372036854775807'));
});
it('should validate a large number (close to maximum)', () => {
// Note: JavaScript numbers can't accurately represent values this large,
// but we'll use a slightly smaller number that can be accurately represented
const input = {test: 9007199254740991}; // Number.MAX_SAFE_INTEGER
const result = v.parse(TestSchema, input);
expect(result.test).toBe(BigInt('9007199254740991'));
});
it('should validate the maximum valid value as BigInt with n literal', () => {
const input = {test: 9223372036854775807n};
const result = v.parse(TestSchema, input);
expect(result.test).toBe(BigInt('9223372036854775807'));
});
});
describe('Minimum values', () => {
it('should validate the minimum valid value as string', () => {
const input = {test: "-9223372036854775808"};
const result = v.parse(TestSchema, input);
expect(result.test).toBe(BigInt('-9223372036854775808'));
});
it('should validate a small number (close to minimum)', () => {
// Note: JavaScript numbers can't accurately represent values this large,
// but we'll use a slightly larger number that can be accurately represented
const input = {test: -9007199254740991}; // -Number.MAX_SAFE_INTEGER
const result = v.parse(TestSchema, input);
expect(result.test).toBe(BigInt('-9007199254740991'));
});
it('should validate the minimum valid value as BigInt with n literal', () => {
const input = {test: -9223372036854775808n};
const result = v.parse(TestSchema, input);
expect(result.test).toBe(BigInt('-9223372036854775808'));
});
});
});
});
describe('Invalid values', () => {
describe('Above maximum', () => {
it('should reject a string value exceeding maximum', () => {
const input = {test: "9223372036854775808"};
expect(() => v.parse(TestSchema, input)).toThrowError(/Invalid value: Expected int64 to be <= 2\^63-1/);
});
it('should reject a BigInt with n literal exceeding maximum', () => {
const input = {test: 9223372036854775808n};
expect(() => v.parse(TestSchema, input)).toThrowError(/Invalid value: Expected int64 to be <= 2\^63-1/);
});
});
describe('Below minimum', () => {
it('should reject a string value below minimum', () => {
const input = {test: "-9223372036854775809"};
expect(() => v.parse(TestSchema, input)).toThrowError(/Invalid value: Expected int64 to be >= -2\^63/);
});
it('should reject a BigInt with n literal below minimum', () => {
const input = {test: -9223372036854775809n};
expect(() => v.parse(TestSchema, input)).toThrowError(/Invalid value: Expected int64 to be >= -2\^63/);
});
});
describe('Non-parsable values', () => {
it('should reject a non-numeric string', () => {
const input = {test: "not-a-number"};
expect(() => v.parse(TestSchema, input)).toThrowError(/Cannot convert/);
});
});
});
});
Running this confirms that the Valibot pipeline validates the input according to the OpenAPI specification and our earlier identified requirements for the int64
format.
Reproducible example or configuration
Client generation:
import { createClient, UserConfig } from '@hey-api/openapi-ts';
const userConfig: UserConfig = {
input: 'https://code.forgejo.org/swagger.v1.json',
output: 'src/client',
plugins: [
{ name: '@hey-api/client-fetch', bundle: true, baseUrl: 'https://code.forgejo.org' },
{ name: '@hey-api/typescript', enums: 'javascript' },
{ name: '@hey-api/sdk', validator: true, transformer: true },
{ name: 'valibot' },
{ name: '@hey-api/transformers', dates: true }
],
logs: { file: false, level: 'debug' },
dryRun: false
};
createClient(userConfig)
.then(() => console.log('Client generation completed successfully'))
.catch((error) => console.error('Failed to generate client:', error));
Runtime test:
import { userCurrentListRepos } from './dist/client';
async function testForgejoClient() {
console.log('Starting Forgejo API test...');
const token = process.env.FORGEJO_TOKEN;
const response = await userCurrentListRepos({
headers: { Authorization: `token ${token}` },
query: { limit: 1 }
});
console.log(`Found ${response.data?.length} repositories. Size of first repo: ${response.data?.[0].size}`);
}
testForgejoClient()
.then(() => console.log('Test completed successfully!'))
.catch((error) => {
console.error('Test failed:', error.message);
process.exit(1);
});
OpenAPI specification (optional)
https://code.forgejo.org/swagger.v1.json
System information (optional)
@hey-api/openapi-ts
version 0.73.0valibot
version 1.1.0- OS: Windows 11
- Node.js: v22.16.0
- TypeScript: 5.8.3