Skip to content

Add option to control nullish vs nullable behavior for optional fields #1414

@IONIN66

Description

@IONIN66

Describe the feature

Currently, the plugin generates .nullish() for optional GraphQL fields when using schema: "zodv4". However, in some projects, .nullable() is preferred over .nullish().

I'd like to propose a new configuration option that allows developers to control this behavior.

Proposed solution

Add a new config option nullishBehavior (or similar) that accepts:

  • "nullish" (default) — generates .nullish() for optional fields
  • "nullable" — generates .nullable() for optional fields
  • "optional" — generates .optional() for optional fields (only undefined, no null)

Example configuration:

generates:
  path/to/schemas.ts:
    plugins:
      - typescript-validation-schema
    config:
      schema: zodv4
      nullishBehavior: nullable  # or "nullish" (default), or "optional"

Current behavior (nullish):

// GraphQL: field: String
// Generated:
field: z.string().nullish()  // allows null | undefined

Desired behavior (nullable):

// GraphQL: field: String
// Generated:
field: z.string().nullable()  // allows null only

Use case:

Our project's GraphQL API treats optional fields as nullable but not undefined in the response. Using .nullish() generates types that are too permissive (string | null | undefined) and doesn't match our actual API contract (string | null).

Current workaround:

We're using a post-generation script to replace .nullish() with .nullable():

// scripts/fix-nullish.ts
import fs from "fs";
import path from "path";

const TARGET_FILE = "schemas.generated.ts";
const NULLISH_PATTERN = /\.nullish\(\)/g;

const schemaFile = process.argv
  .slice(2)
  .find((file) => file.includes(TARGET_FILE));

if (!schemaFile) {
  console.warn(`⏭️ ${TARGET_FILE} was not passed to the script`);
  process.exit(0);
}

const fullPath = path.resolve(process.cwd(), schemaFile);

if (!fs.existsSync(fullPath)) {
  console.error(`❌ File not found: ${fullPath}`);
  process.exit(1);
}

const content = fs.readFileSync(fullPath, "utf-8");
const updated = content.replace(NULLISH_PATTERN, ".nullable()");

if (content === updated) {
  console.warn(`ℹ️ No nullish() found to replace in ${schemaFile}`);
  process.exit(0);
}

fs.writeFileSync(fullPath, updated);
console.warn(`✅ Replaced nullish() with nullable() in ${schemaFile}`);
// codegen.ts
hooks: {
  afterOneFileWrite: ["npx tsx scripts/fix-nullish.ts"]
}

While this works, it would be much cleaner to have this as a built-in configuration option.

Additional context:

Thank you for considering this feature!

Metadata

Metadata

Assignees

No one assigned

    Labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions