[ZOD] Incorrect schema creation for binary files
Description
When generating a zod schema to verify the body of the data being sent, binary data is checked as attachments: z.array(z.string()).optional() instead z.array(z.instanceof(Blob)).optional()
while typescript types are generated as
attachments?:Array<Blob|File>
They are not compatible with each other
Reproducible example or configuration
No response
OpenAPI specification (optional)
"attachments": {
"description": "Прикреплённые файлы",
"type": "array",
"items": {
"type": "string",
"format": "binary"
}
}
System information (optional)
No response
This seems to be a duplicate of https://github.com/hey-api/openapi-ts/issues/2446
@shadone as you're browsing through this issue, do you have any proposal how to resolve, considering the various scenarios we need to cover?
any update on this issue?
@sahand-sn a workaround is to create post process for binary file to convert the z.string() to z.instanceof(File) and run post process after "openapi-ts": "openapi-ts && tsx lib/plugins/zod-binary-converter/post-process.ts"
Just ensure that the binary schema name matches the one in the pre-process (file, files in this example):
schema: {
type: 'object',
properties: {
file: {
type: 'string',
format: 'binary',
},
},
},
schema: {
type: 'object',
properties: {
files: {
type: 'array',
items: {
type: 'string',
format: 'binary',
},
},
},
},
//post-process.ts
import { existsSync, readFileSync, writeFileSync } from 'fs';
import { join, dirname } from 'path';
import { fileURLToPath } from 'url';
/**
* Post-process the generated zod.gen.ts file to replace z.string() with z.instanceof(File)
* for binary file/files fields.
*/
function postProcessZodFile(zodFilePath: string, debug: boolean = false): void {
if (!existsSync(zodFilePath)) {
if (debug) {
console.log(`[zod-binary-converter] File does not exist: ${zodFilePath}`);
}
return;
}
if (debug) {
console.log(`[zod-binary-converter] Processing file: ${zodFilePath}`);
}
// Read the file content
let content: string = readFileSync(zodFilePath, 'utf-8');
let modified: boolean = false;
// Reset regex lastIndex
const patterns: Array<{ name: string; regex: RegExp; replacement: string }> = [
{
name: 'file: z.string()',
regex: /(\s+file:\s+)z\.string\(\)/g,
replacement: '$1z.instanceof(File)'
},
{
name: 'files: z.array(z.string())',
regex: /(\s+files:\s+)z\.array\(z\.string\(\)\)/g,
replacement: '$1z.array(z.instanceof(File))'
},
{
name: 'file: z.optional(z.string())',
regex: /(\s+file:\s+)z\.optional\(z\.string\(\)\)/g,
replacement: '$1z.optional(z.instanceof(File))'
},
{
name: 'file: z.union([z.string(), ...])',
regex: /(\s+file:\s+z\.union\(\[)z\.string\(\)/g,
replacement: '$1z.instanceof(File)'
},
{
name: 'files: z.optional(z.array(z.string()))',
regex: /(\s+files:\s+)z\.optional\(z\.array\(z\.string\(\)\)\)/g,
replacement: '$1z.optional(z.array(z.instanceof(File)))'
}
];
patterns.forEach(({ name, regex, replacement }) => {
const originalContent: string = content;
content = content.replace(regex, replacement);
if (content !== originalContent) {
modified = true;
if (debug) {
console.log(`[zod-binary-converter] Applied pattern: ${name}`);
}
}
});
if (modified) {
// Write the modified content back
writeFileSync(zodFilePath, content, 'utf-8');
console.log('[zod-binary-converter] File transformation complete - binary fields converted to z.instanceof(File)');
} else {
if (debug) {
console.log('[zod-binary-converter] No modifications needed');
}
}
}
// Run if called directly
if (import.meta.url === `file://${process.argv[1]}`) {
const __filename: string = fileURLToPath(import.meta.url);
const __dirname: string = dirname(__filename);
const zodFilePath: string = join(
__dirname,
'../../client/zod.gen.ts'
);
const debug: boolean = process.argv.includes('--debug');
postProcessZodFile(zodFilePath, debug);
}
export { postProcessZodFile };