Skip to content
5 changes: 5 additions & 0 deletions .changeset/giant-hats-work.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"swagger-typescript-api": patch
---

Fixed incorrect null handling for nullable objects with nullable properties (#533)
26 changes: 11 additions & 15 deletions src/schema-parser/schema-utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -151,24 +151,20 @@ export class SchemaUtils {

isNullMissingInType = (schema, type) => {
const { nullable, type: schemaType } = schema || {};
if (
!(
nullable ||
!!get(schema, "x-nullable") ||
schemaType === this.config.Ts.Keyword.Null
) ||
typeof type !== "string"
) {
return false;
}
const isSchemaMarkedNullable =
nullable ||
!!get(schema, "x-nullable") ||
schemaType === this.config.Ts.Keyword.Null;

if (!isSchemaMarkedNullable) return false;
if (typeof type !== "string") return false;

const nullKeyword = this.config.Ts.Keyword.Null;
const lastLine = type.trimEnd().split("\n").pop() ?? type;
const hasRootLevelNull = new RegExp(
`(^|\\||\\()\\s*${nullKeyword}\\s*(\\||\\)|$)`,

@cubic-dev-ai cubic-dev-ai Bot May 7, 2026

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1: Regex incorrectly matches null inside parenthesized sub-expressions, causing nullable arrays of nullable elements to lose their root-level | null. For a nullable array with nullable items, the type (string | null)[] would not get | null appended because the regex matches | null) inside the group.

Consider anchoring the parenthesis check so it only matches when the parenthesized group constitutes the entire type (e.g., ^\(.*\)$), or using a more targeted approach that doesn't match nested groups followed by array/generic suffixes.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At src/schema-parser/schema-utils.ts, line 164:

<comment>Regex incorrectly matches null inside parenthesized sub-expressions, causing nullable arrays of nullable elements to lose their root-level `| null`. For a nullable array with nullable items, the type `(string | null)[]` would not get `| null` appended because the regex matches `| null)` inside the group.

Consider anchoring the parenthesis check so it only matches when the parenthesized group constitutes the entire type (e.g., `^\(.*\)$`), or using a more targeted approach that doesn't match nested groups followed by array/generic suffixes.</comment>

<file context>
@@ -161,7 +161,7 @@ export class SchemaUtils {
     const nullKeyword = this.config.Ts.Keyword.Null;
     const hasRootLevelNull = new RegExp(
-      `(^|\\|)\\s*${nullKeyword}\\s*(\\||$)`,
+      `(^|\\||\\()\\s*${nullKeyword}\\s*(\\||\\)|$)`,
     ).test(type);
 
</file context>
Fix with Cubic

).test(type);
Comment on lines +163 to +165

@cubic-dev-ai cubic-dev-ai Bot Apr 29, 2026

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2: The regex check treats nested | null | as root-level null, so nullable parent object types can still be skipped when a child property union contains null in the middle.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At src/schema-parser/schema-utils.ts, line 102:

<comment>The regex check treats nested `| null |` as root-level null, so nullable parent object types can still be skipped when a child property union contains `null` in the middle.</comment>

<file context>
@@ -85,14 +85,25 @@ export class SchemaUtils {
+    // Match null bounded by pipes and/or start/end of string
+    // This avoids false positives from nested nullable properties like { prop: string | null }
+    const nullKeyword = this.config.Ts.Keyword.Null;
+    const hasRootLevelNull = new RegExp(
+      `(^|\\|)\\s*${nullKeyword}\\s*(\\||$)`,
+    ).test(type);
</file context>
Suggested change
const hasRootLevelNull = new RegExp(
`(^|\\|)\\s*${nullKeyword}\\s*(\\||$)`,
).test(type);
const hasRootLevelNull = (() => {
let depth = 0;
let token = "";
for (const char of type) {
if (char === "{" || char === "(" || char === "[" || char === "<") depth++;
if (char === "}" || char === ")" || char === "]" || char === ">") {
depth = Math.max(depth - 1, 0);
}
if (char === "|" && depth === 0) {
if (token.trim() === nullKeyword) return true;
token = "";
continue;
}
token += char;
}
return token.trim() === nullKeyword;
})();
Fix with Cubic


return (
!lastLine.includes(` ${nullKeyword}`) &&
!lastLine.includes(`${nullKeyword} `)
);
return !hasRootLevelNull;
};

safeAddNullToType = (schema, type) => {
Expand Down
Loading
Loading