openapi-ts icon indicating copy to clipboard operation
openapi-ts copied to clipboard

[ZOD] Incorrect schema creation for binary files

Open kokojer opened this issue 9 months ago • 4 comments

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

kokojer avatar Jul 16 '25 08:07 kokojer

This seems to be a duplicate of https://github.com/hey-api/openapi-ts/issues/2446

shadone avatar Sep 13 '25 08:09 shadone

@shadone as you're browsing through this issue, do you have any proposal how to resolve, considering the various scenarios we need to cover?

mrlubos avatar Sep 13 '25 09:09 mrlubos

any update on this issue?

edward-meister avatar Nov 02 '25 10:11 edward-meister

@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 };

SofienRogue avatar Nov 03 '25 20:11 SofienRogue