Skip to content

Commit 8199976

Browse files
committed
feat: Under-specified objects are Record<string, unknown>
1 parent 9cd050d commit 8199976

12 files changed

+60
-1
lines changed

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -149,6 +149,7 @@ The following flags can be appended to the CLI command.
149149
| `--http-method` | `-m` | `GET` | Provide the HTTP Verb/Method for fetching a schema from a remote URL |
150150
| `--immutable-types` | | `false` | Generates immutable types (readonly properties and readonly array) |
151151
| `--additional-properties` | | `false` | Allow arbitrary properties for all schema objects without `additionalProperties: false` |
152+
| `--empty-objects-unknown` | | `false` | Allow arbitrary properties for schema objects with no specified properties, and no specified `additionalProperties` |
152153
| `--default-non-nullable` | | `false` | Treat schema objects with default values as non-nullable |
153154
| `--export-type` | `-t` | `false` | Export `type` instead of `interface` |
154155
| `--path-params-as-types` | | `false` | Allow dynamic string lookups on the `paths` object |

bin/cli.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ Options
2222
--export-type, -t (optional) Export "type" instead of "interface"
2323
--immutable-types (optional) Generates immutable types (readonly properties and readonly array)
2424
--additional-properties (optional) Allow arbitrary properties for all schema objects without "additionalProperties: false"
25+
--empty-objects-unknown (optional) Allow arbitrary properties for schema objects with no specified properties, and no specified "additionalProperties"
2526
--default-non-nullable (optional) If a schema object has a default value set, don’t mark it as nullable
2627
--support-array-length (optional) Generate tuples using array minItems / maxItems
2728
--path-params-as-types (optional) Substitute path parameter names with their respective types

src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@ async function openapiTS(
4646
transform: options && typeof options.transform === "function" ? options.transform : undefined,
4747
postTransform: options && typeof options.postTransform === "function" ? options.postTransform : undefined,
4848
immutableTypes: options.immutableTypes || false,
49+
emptyObjectsUnknown: options.emptyObjectsUnknown || false,
4950
indentLv: 0,
5051
operations: {},
5152
pathParamsAsTypes: options.pathParamsAsTypes || false,

src/transform/schema-object.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -255,5 +255,8 @@ export function defaultSchemaObjectTransform(
255255
// nullable (3.0)
256256
if (schemaObject.nullable) finalType = tsUnionOf(finalType || "Record<string, unknown>", "null");
257257

258-
return finalType || "Record<string, never>"; // if no type could be generated, fall back to “empty object” type
258+
if (finalType) return finalType;
259+
260+
// if no type could be generated, fall back to “empty object” type
261+
return ctx.emptyObjectsUnknown ? "Record<string, unknown>" : "Record<string, never>";
259262
}

src/types.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -567,6 +567,8 @@ export interface OpenAPITSOptions {
567567
alphabetize?: boolean;
568568
/** Specify auth if using openapi-typescript to fetch URL */
569569
auth?: string;
570+
/** Allow schema objects with no specified properties to have additional properties if not expressly forbidden? (default: false) */
571+
emptyObjectsUnknown?: boolean;
570572
/** Specify current working directory (cwd) to resolve remote schemas on disk (not needed for remote URL schemas) */
571573
cwd?: URL;
572574
/** Should schema objects with a default value not be considered optional? */
@@ -632,6 +634,7 @@ export type Subschema =
632634
export interface GlobalContext {
633635
additionalProperties: boolean;
634636
alphabetize: boolean;
637+
emptyObjectsUnknown: boolean;
635638
defaultNonNullable: boolean;
636639
discriminators: { [$ref: string]: DiscriminatorObject };
637640
transform: OpenAPITSOptions["transform"];

test/components-object.test.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import transformComponentsObject from "../src/transform/components-object.js";
44
const options: GlobalContext = {
55
additionalProperties: false,
66
alphabetize: false,
7+
emptyObjectsUnknown: false,
78
defaultNonNullable: false,
89
discriminators: {},
910
immutableTypes: false,

test/header-object.test.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ const options: TransformHeaderObjectOptions = {
66
ctx: {
77
additionalProperties: false,
88
alphabetize: false,
9+
emptyObjectsUnknown: false,
910
defaultNonNullable: false,
1011
discriminators: {},
1112
immutableTypes: false,

test/load.test.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,7 @@ async function load(
105105
urlCache: new Set(),
106106
fetch: vi.fn(unidiciFetch),
107107
additionalProperties: false,
108+
emptyObjectsUnknown: false,
108109
alphabetize: false,
109110
defaultNonNullable: false,
110111
discriminators: {},

test/path-item-object.test.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ const options: TransformPathItemObjectOptions = {
66
ctx: {
77
additionalProperties: false,
88
alphabetize: false,
9+
emptyObjectsUnknown: false,
910
defaultNonNullable: false,
1011
discriminators: {},
1112
immutableTypes: false,

test/paths-object.test.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import transformPathsObject from "../src/transform/paths-object.js";
44
const options: GlobalContext = {
55
additionalProperties: false,
66
alphabetize: false,
7+
emptyObjectsUnknown: false,
78
defaultNonNullable: false,
89
discriminators: {},
910
immutableTypes: false,

test/request-body-object.test.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ const options: TransformRequestBodyObjectOptions = {
66
ctx: {
77
additionalProperties: false,
88
alphabetize: false,
9+
emptyObjectsUnknown: false,
910
defaultNonNullable: false,
1011
discriminators: {},
1112
immutableTypes: false,

test/schema-object.test.ts

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ const options: TransformSchemaObjectOptions = {
66
ctx: {
77
additionalProperties: false,
88
alphabetize: false,
9+
emptyObjectsUnknown: false,
910
defaultNonNullable: false,
1011
discriminators: {},
1112
immutableTypes: false,
@@ -121,6 +122,25 @@ describe("Schema Object", () => {
121122
});
122123

123124
describe("object", () => {
125+
test("empty", () => {
126+
const schema: SchemaObject = {
127+
type: "object",
128+
};
129+
const generated = transformSchemaObject(schema, options);
130+
expect(generated).toBe(`Record<string, never>`);
131+
});
132+
133+
test("empty with emptyObjectsUnknown", () => {
134+
const schema: SchemaObject = {
135+
type: "object",
136+
};
137+
const generated = transformSchemaObject(schema, {
138+
...options,
139+
ctx: { ...options.ctx, emptyObjectsUnknown: true },
140+
});
141+
expect(generated).toBe(`Record<string, unknown>`);
142+
});
143+
124144
test("basic", () => {
125145
const schema: SchemaObject = {
126146
type: "object",
@@ -361,6 +381,30 @@ describe("Schema Object", () => {
361381
});
362382
});
363383

384+
describe("emptyObjectsUnknown", () => {
385+
describe("with object with no specified properties", () => {
386+
const schema: SchemaObject = {
387+
type: "object",
388+
};
389+
390+
test("true", () => {
391+
const generated = transformSchemaObject(schema, {
392+
...options,
393+
ctx: { ...options.ctx, emptyObjectsUnknown: true },
394+
});
395+
expect(generated).toBe(`Record<string, unknown>`);
396+
});
397+
398+
test("false", () => {
399+
const generated = transformSchemaObject(schema, {
400+
...options,
401+
ctx: { ...options.ctx, emptyObjectsUnknown: false },
402+
});
403+
expect(generated).toBe(`Record<string, never>`);
404+
});
405+
});
406+
});
407+
364408
describe("defaultNonNullable", () => {
365409
test("true", () => {
366410
const schema: SchemaObject = {

0 commit comments

Comments
 (0)