Skip to content

Commit bba4ba8

Browse files
authored
Always checks for required properties, even if they are missing from properties (#620)
* Always checks for required properties, even if they are missing from properties * Fix based on comments
1 parent 2da69c1 commit bba4ba8

12 files changed

+1370
-1294
lines changed

src/transform/schema.ts

+18-2
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,19 @@ export function transformSchemaObjMap(obj: Record<string, any>, options: Transfo
3737
return output.replace(/\n+$/, "\n"); // replace repeat line endings with only one
3838
}
3939

40+
/** make sure all required fields exist **/
41+
export function addRequiredProps(properties: Record<string, any>, required: Set<string>): string[] {
42+
const missingRequired = [...required].filter((r: string) => !(r in properties));
43+
if (missingRequired.length == 0) {
44+
return [];
45+
}
46+
let output = "";
47+
for (const r of missingRequired) {
48+
output += `${r}: unknown;\n`;
49+
}
50+
return [`{\n${output}}`];
51+
}
52+
4053
/** transform anyOf */
4154
export function transformAnyOf(anyOf: any, options: TransformSchemaObjOptions): string {
4255
// filter out anyOf keys that only have a `required` key. #642
@@ -99,14 +112,16 @@ export function transformSchemaObj(node: any, options: TransformSchemaObjOptions
99112
}
100113
case "object": {
101114
const isAnyOfOrOneOfOrAllOf = "anyOf" in node || "oneOf" in node || "allOf" in node;
102-
115+
const missingRequired = addRequiredProps(node.properties || {}, node.required || []);
103116
// if empty object, then return generic map type
104117
if (
105118
!isAnyOfOrOneOfOrAllOf &&
106119
(!node.properties || !Object.keys(node.properties).length) &&
107120
!node.additionalProperties
108121
) {
109-
output += `{ ${readonly}[key: string]: any }`;
122+
const emptyObj = `{ ${readonly}[key: string]: unknown }`;
123+
124+
output += tsIntersectionOf([emptyObj, ...missingRequired]);
110125
break;
111126
}
112127

@@ -144,6 +159,7 @@ export function transformSchemaObj(node: any, options: TransformSchemaObjOptions
144159
...(node.anyOf ? [transformAnyOf(node.anyOf, options)] : []),
145160
...(node.oneOf ? [transformOneOf(node.oneOf, options)] : []),
146161
...(properties ? [`{\n${properties}\n}`] : []), // then properties (line breaks are important!)
162+
...missingRequired, // add required that are missing from properties
147163
...(additionalProperties ? [additionalProperties] : []), // then additional properties
148164
]);
149165

tests/schema.test.ts

+37-4
Original file line numberDiff line numberDiff line change
@@ -55,10 +55,10 @@ describe("SchemaObject", () => {
5555
);
5656

5757
// unknown
58-
expect(transform(objUnknown, { ...defaults })).toBe(`{ [key: string]: any }`);
58+
expect(transform(objUnknown, { ...defaults })).toBe(`{ [key: string]: unknown }`);
5959

6060
// empty
61-
expect(transform({}, { ...defaults })).toBe(`{ [key: string]: any }`);
61+
expect(transform({}, { ...defaults })).toBe(`{ [key: string]: unknown }`);
6262

6363
// nullable
6464
expect(transform(objNullable, { ...defaults })).toBe(`({\n"string"?: string;\n\n}) | null`);
@@ -73,8 +73,8 @@ describe("SchemaObject", () => {
7373
expect(transform(objStd, opts)).toBe(
7474
`{\nreadonly "object"?: {\nreadonly "string"?: string;\nreadonly "number"?: components["schemas"]["object_ref"];\n\n};\n\n}`
7575
);
76-
expect(transform(objUnknown, opts)).toBe(`{ readonly [key: string]: any }`);
77-
expect(transform({}, opts)).toBe(`{ readonly [key: string]: any }`);
76+
expect(transform(objUnknown, opts)).toBe(`{ readonly [key: string]: unknown }`);
77+
expect(transform({}, opts)).toBe(`{ readonly [key: string]: unknown }`);
7878
expect(transform(objNullable, opts)).toBe(`({\nreadonly "string"?: string;\n\n}) | null`);
7979
expect(transform(objRequired, opts)).toBe(`{\nreadonly "required": string;\nreadonly "optional"?: boolean;\n\n}`);
8080
});
@@ -368,6 +368,39 @@ describe("SchemaObject", () => {
368368
}"
369369
`);
370370
});
371+
it("empty object with required fields", () => {
372+
expect(
373+
transform(
374+
{
375+
type: "object",
376+
required: ["abc"],
377+
},
378+
{ ...defaults }
379+
)
380+
).toBe(`({ [key: string]: unknown }) & ({
381+
abc: unknown;
382+
})`);
383+
});
384+
});
385+
386+
it("object with missing required fields", () => {
387+
expect(
388+
transform(
389+
{
390+
type: "object",
391+
required: ["abc", "email"],
392+
properties: {
393+
email: { type: "string" },
394+
},
395+
},
396+
{ ...defaults }
397+
)
398+
).toBe(`({
399+
"email": string;
400+
401+
}) & ({
402+
abc: unknown;
403+
})`);
371404
});
372405

373406
describe("comments", () => {

tests/v2/expected/petstore.immutable.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -235,7 +235,7 @@ export interface operations {
235235
/** Additional data to pass to server */
236236
readonly additionalMetadata?: string;
237237
/** file to upload */
238-
readonly file?: { readonly [key: string]: any };
238+
readonly file?: { readonly [key: string]: unknown };
239239
};
240240
};
241241
readonly responses: {

tests/v2/expected/petstore.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -235,7 +235,7 @@ export interface operations {
235235
/** Additional data to pass to server */
236236
additionalMetadata?: string;
237237
/** file to upload */
238-
file?: { [key: string]: any };
238+
file?: { [key: string]: unknown };
239239
};
240240
};
241241
responses: {

0 commit comments

Comments
 (0)