Skip to content

Commit 9fa2708

Browse files
feat: add --x-nullable-as-nullable option to treat x-nullable the same as nullable
1 parent 4c88d9d commit 9fa2708

8 files changed

+132
-8
lines changed

bin/cli.js

+3
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ Options
2424
--content-never (optional) If supplied, an omitted reponse \`content\` property will be generated as \`never\` instead of \`unknown\`
2525
--additional-properties, -ap (optional) Allow arbitrary properties for all schema objects without "additionalProperties: false"
2626
--default-non-nullable (optional) If a schema object has a default value set, don’t mark it as nullable
27+
--x-nullable-as-nullable (optional) If a schema object has \`x-nullable\` set, treat it as nullable (like \`nullable\` in OpenAPI 3.0.x)
2728
--prettier-config, -c (optional) specify path to Prettier config file
2829
--raw-schema (optional) Parse as partial schema (raw components)
2930
--paths-enum, -pe (optional) Generate an enum containing all API paths.
@@ -48,6 +49,7 @@ const flags = parser(args, {
4849
array: ["header"],
4950
boolean: [
5051
"defaultNonNullable",
52+
"xNullableAsNullable",
5153
"immutableTypes",
5254
"contentNever",
5355
"rawSchema",
@@ -98,6 +100,7 @@ async function generateSchema(pathToSpec) {
98100
additionalProperties: flags.additionalProperties,
99101
auth: flags.auth,
100102
defaultNonNullable: flags.defaultNonNullable,
103+
xNullableAsNullable: flags.xNullableAsNullable,
101104
immutableTypes: flags.immutableTypes,
102105
prettierConfig: flags.prettierConfig,
103106
rawSchema: flags.rawSchema,

src/index.ts

+1
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ async function openapiTS(
4141
auth: options.auth,
4242
commentHeader: typeof options.commentHeader === "string" ? options.commentHeader : COMMENT_HEADER,
4343
defaultNonNullable: options.defaultNonNullable || false,
44+
xNullableAsNullable: options.xNullableAsNullable || false,
4445
formatter: options && typeof options.formatter === "function" ? options.formatter : undefined,
4546
immutableTypes: options.immutableTypes || false,
4647
contentNever: options.contentNever || false,

src/transform/schema.ts

+10-6
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ import {
1212
ParsedSimpleValue,
1313
} from "../utils.js";
1414

15-
interface TransformSchemaObjOptions extends GlobalContext {
15+
export interface TransformSchemaObjOptions extends GlobalContext {
1616
required: Set<string>;
1717
}
1818

@@ -32,7 +32,7 @@ export function transformSchemaObjMap(obj: Record<string, any>, options: Transfo
3232
const v = obj[k];
3333

3434
// 1. Add comment in jsdoc notation
35-
const comment = prepareComment(v);
35+
const comment = prepareComment(v, options);
3636
if (comment) output += comment;
3737

3838
// 2. name (with “?” if optional property)
@@ -86,6 +86,10 @@ export function transformOneOf(oneOf: any, options: TransformSchemaObjOptions):
8686
return tsUnionOf(oneOf.map((value: any) => transformSchemaObj(value, options)));
8787
}
8888

89+
export function isNodeNullable(node: any, options: TransformSchemaObjOptions): boolean {
90+
return node.nullable || (options.xNullableAsNullable && node["x-nullable"]);
91+
}
92+
8993
/** Convert schema object to TypeScript */
9094
export function transformSchemaObj(node: any, options: TransformSchemaObjOptions): string {
9195
const readonly = tsReadonly(options.immutableTypes);
@@ -96,7 +100,7 @@ export function transformSchemaObj(node: any, options: TransformSchemaObjOptions
96100
const overriddenType = options.formatter && options.formatter(node);
97101

98102
// open nullable
99-
if (node.nullable) {
103+
if (isNodeNullable(node, options)) {
100104
output += "(";
101105
}
102106

@@ -117,13 +121,13 @@ export function transformSchemaObj(node: any, options: TransformSchemaObjOptions
117121
break;
118122
}
119123
case "const": {
120-
output += parseSingleSimpleValue(node.const, node.nullable);
124+
output += parseSingleSimpleValue(node.const, isNodeNullable(node, options));
121125
break;
122126
}
123127
case "enum": {
124128
const items: Array<ParsedSimpleValue> = [];
125129
(node.enum as unknown[]).forEach((item) => {
126-
const value = parseSingleSimpleValue(item, node.nullable);
130+
const value = parseSingleSimpleValue(item, isNodeNullable(node, options));
127131
items.push(value);
128132
});
129133
output += tsUnionOf(items);
@@ -221,7 +225,7 @@ export function transformSchemaObj(node: any, options: TransformSchemaObjOptions
221225
}
222226

223227
// close nullable
224-
if (node.nullable) {
228+
if (isNodeNullable(node, options)) {
225229
output += ") | null";
226230
}
227231

src/types.ts

+3
Original file line numberDiff line numberDiff line change
@@ -130,6 +130,8 @@ export interface SwaggerToTSOptions {
130130
contentNever?: boolean;
131131
/** (optional) Treat schema objects with default values as non-nullable */
132132
defaultNonNullable?: boolean;
133+
/** (optional) Schemas with `x-nullable: true` should generate with `| null`, like `nullable` in OpenAPI 3.0.x */
134+
xNullableAsNullable?: boolean;
133135
/** (optional) Path to Prettier config */
134136
prettierConfig?: string;
135137
/** (optional) Parsing input document as raw schema rather than OpenAPI document */
@@ -180,6 +182,7 @@ export interface GlobalContext {
180182
auth?: string;
181183
commentHeader: string;
182184
defaultNonNullable: boolean;
185+
xNullableAsNullable: boolean;
183186
formatter?: SchemaFormatter;
184187
immutableTypes: boolean;
185188
contentNever: boolean;

src/utils.ts

+4-2
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import type { OpenAPI2, OpenAPI3, ReferenceObject } from "./types.js";
2+
import { isNodeNullable, type TransformSchemaObjOptions } from "./transform/schema.js";
23

34
type CommentObject = {
45
const?: boolean; // jsdoc without value
@@ -9,6 +10,7 @@ type CommentObject = {
910
example?: string; // jsdoc with value
1011
format?: string; // not jsdoc
1112
nullable?: boolean; // Node information
13+
"x-nullable"?: boolean; // Node information
1214
title?: string; // not jsdoc
1315
type: string; // Type of node
1416
};
@@ -27,7 +29,7 @@ const FS_RE = /\//g;
2729
* @see {comment} for output examples
2830
* @returns void if not comments or jsdoc format comment string
2931
*/
30-
export function prepareComment(v: CommentObject): string | void {
32+
export function prepareComment(v: CommentObject, options: TransformSchemaObjOptions): string | void {
3133
const commentsArray: Array<string> = [];
3234

3335
// * Not JSDOC tags: [title, format]
@@ -58,7 +60,7 @@ export function prepareComment(v: CommentObject): string | void {
5860

5961
// * JSDOC 'Enum' with type
6062
if (v.enum) {
61-
const canBeNull = v.nullable ? `|${null}` : "";
63+
const canBeNull = isNodeNullable(v, options) ? `|${null}` : "";
6264
commentsArray.push(`@enum {${v.type}${canBeNull}}`);
6365
}
6466

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
/**
2+
* This file was auto-generated by openapi-typescript.
3+
* Do not make direct changes to the file.
4+
*/
5+
6+
export interface paths {}
7+
8+
export interface definitions {
9+
MyType: string;
10+
/** @description Some value that has x-nullable set */
11+
MyTypeXNullable: string | null;
12+
/**
13+
* @description Enum with x-nullable
14+
* @enum {string|null}
15+
*/
16+
MyEnum: ("foo" | "bar") | null;
17+
}
18+
19+
export interface operations {}
20+
21+
export interface external {}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
/**
2+
* This file was auto-generated by openapi-typescript.
3+
* Do not make direct changes to the file.
4+
*/
5+
6+
export interface paths {}
7+
8+
export interface components {
9+
schemas: {
10+
MyType: string;
11+
/** @description Some value that has x-nullable set */
12+
MyTypeXNullable: string | null;
13+
/**
14+
* @description Enum with x-nullable
15+
* @enum {string|null}
16+
*/
17+
MyEnum: ("foo" | "bar") | null;
18+
};
19+
}
20+
21+
export interface operations {}
22+
23+
export interface external {}
+67
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
import { expect } from "chai";
2+
import fs from "fs";
3+
import eol from "eol";
4+
import openapiTS from "../../dist/index.js";
5+
6+
describe("x-nullable-as-nullable", () => {
7+
const cases = [
8+
{
9+
name: "swagger 2.0",
10+
expectedFile: "x-nullable-as-nullable.2.0.ts",
11+
schema: {
12+
swagger: "2.0",
13+
definitions: {
14+
MyType: {
15+
type: "string",
16+
},
17+
MyTypeXNullable: {
18+
type: "string",
19+
description: "Some value that has x-nullable set",
20+
"x-nullable": true,
21+
},
22+
MyEnum: {
23+
description: "Enum with x-nullable",
24+
type: "string",
25+
enum: ["foo", "bar"],
26+
"x-nullable": true,
27+
},
28+
},
29+
},
30+
},
31+
{
32+
name: "openapi 3.1",
33+
expectedFile: "x-nullable-as-nullable.3.1.ts",
34+
schema: {
35+
openapi: "3.1",
36+
components: {
37+
schemas: {
38+
MyType: {
39+
type: "string",
40+
},
41+
MyTypeXNullable: {
42+
type: "string",
43+
description: "Some value that has x-nullable set",
44+
"x-nullable": true,
45+
},
46+
MyEnum: {
47+
description: "Enum with x-nullable",
48+
type: "string",
49+
enum: ["foo", "bar"],
50+
"x-nullable": true,
51+
},
52+
},
53+
},
54+
},
55+
},
56+
];
57+
58+
cases.forEach(({ name, expectedFile, schema }) => {
59+
it(name, async () => {
60+
const generated = await openapiTS(schema, {
61+
xNullableAsNullable: true,
62+
});
63+
const expected = eol.lf(fs.readFileSync(new URL(`./expected/${expectedFile}`, import.meta.url), "utf8"));
64+
expect(generated).to.equal(expected);
65+
});
66+
});
67+
});

0 commit comments

Comments
 (0)