Skip to content

Commit e3617c0

Browse files
committed
Support JSONSchema $defs
1 parent c5b6ed8 commit e3617c0

File tree

10 files changed

+202
-56
lines changed

10 files changed

+202
-56
lines changed

packages/openapi-typescript/src/index.ts

+6-33
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import type { GlobalContext, OpenAPI3, OpenAPITSOptions, ParameterObject, SchemaObject, Subschema } from "./types.js";
1+
import type { GlobalContext, OpenAPI3, OpenAPITSOptions, SchemaObject, Subschema } from "./types.js";
22
import type { Readable } from "node:stream";
33
import { URL } from "node:url";
44
import load, { resolveSchema, VIRTUAL_JSON_URL } from "./load.js";
@@ -10,8 +10,9 @@ import transformParameterObjectArray from "./transform/parameter-object-array.js
1010
import transformRequestBodyObject from "./transform/request-body-object.js";
1111
import transformResponseObject from "./transform/response-object.js";
1212
import transformSchemaObject from "./transform/schema-object.js";
13+
import transformSchemaObjectMap from "./transform/schema-object-map.js";
1314
import { error, escObjKey, getDefaultFetch, getEntries, getSchemaObjectComment, indent } from "./utils.js";
14-
import transformPathItemObject, { Method } from "./transform/path-item-object.js";
15+
1516
export * from "./types.js"; // expose all types to consumers
1617

1718
const EMPTY_OBJECT_RE = /^\s*\{?\s*\}?\s*$/;
@@ -184,43 +185,15 @@ async function openapiTS(schema: string | URL | OpenAPI3 | Readable, options: Op
184185
break;
185186
}
186187
case "RequestBodyObject": {
187-
subschemaOutput = transformRequestBodyObject(subschema.schema, { path, ctx: { ...ctx, indentLv } });
188+
subschemaOutput = `${transformRequestBodyObject(subschema.schema, { path, ctx: { ...ctx, indentLv } })};`;
188189
break;
189190
}
190191
case "ResponseObject": {
191-
subschemaOutput = transformResponseObject(subschema.schema, { path, ctx: { ...ctx, indentLv } });
192+
subschemaOutput = `${transformResponseObject(subschema.schema, { path, ctx: { ...ctx, indentLv } })};`;
192193
break;
193194
}
194195
case "SchemaMap": {
195-
subschemaOutput += "{\n";
196-
indentLv++;
197-
198-
outer: for (const [name, schemaObject] of getEntries(subschema.schema!)) {
199-
if (!schemaObject || typeof schemaObject !== "object") continue;
200-
const c = getSchemaObjectComment(schemaObject as SchemaObject, indentLv);
201-
if (c) subschemaOutput += indent(c, indentLv);
202-
203-
// Test for Path Item Object
204-
if (!("type" in schemaObject) && !("$ref" in schemaObject)) {
205-
for (const method of ["get", "put", "post", "delete", "options", "head", "patch", "trace"] as Method[]) {
206-
if (method in schemaObject) {
207-
subschemaOutput += indent(`${escObjKey(name)}: ${transformPathItemObject(schemaObject, { path: `${path}${name}`, ctx: { ...ctx, indentLv } })};\n`, indentLv);
208-
continue outer;
209-
}
210-
}
211-
}
212-
// Test for Parameter
213-
if ("in" in schemaObject) {
214-
subschemaOutput += indent(`${escObjKey(name)}: ${transformParameterObject(schemaObject as ParameterObject, { path: `${path}${name}`, ctx: { ...ctx, indentLv } })};\n`, indentLv);
215-
continue;
216-
}
217-
218-
// Otherwise, this is a Schema Object
219-
subschemaOutput += indent(`${escObjKey(name)}: ${transformSchemaObject(schemaObject, { path: `${path}${name}`, ctx: { ...ctx, indentLv } })};\n`, indentLv);
220-
}
221-
222-
indentLv--;
223-
subschemaOutput += indent("};", indentLv);
196+
subschemaOutput = `${transformSchemaObjectMap(subschema.schema, { path, ctx: { ...ctx, indentLv } })};`;
224197
break;
225198
}
226199
case "SchemaObject": {

packages/openapi-typescript/src/transform/components-object.ts

+3-15
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import transformParameterObject from "./parameter-object.js";
55
import transformPathItemObject from "./path-item-object.js";
66
import transformRequestBodyObject from "./request-body-object.js";
77
import transformResponseObject from "./response-object.js";
8+
import transformSchemaObjectMap from "./schema-object-map.js";
89
import transformSchemaObject from "./schema-object.js";
910

1011
export default function transformComponentsObject(components: ComponentsObject, ctx: GlobalContext): string {
@@ -14,21 +15,8 @@ export default function transformComponentsObject(components: ComponentsObject,
1415

1516
// schemas
1617
if (components.schemas) {
17-
output.push(indent("schemas: {", indentLv));
18-
indentLv++;
19-
for (const [name, schemaObject] of getEntries(components.schemas, ctx.alphabetize, ctx.excludeDeprecated)) {
20-
const c = getSchemaObjectComment(schemaObject, indentLv);
21-
if (c) output.push(indent(c, indentLv));
22-
let key = escObjKey(name);
23-
if (ctx.immutableTypes || schemaObject.readOnly) key = tsReadonly(key);
24-
const schemaType = transformSchemaObject(schemaObject, {
25-
path: `#/components/schemas/${name}`,
26-
ctx: { ...ctx, indentLv: indentLv },
27-
});
28-
output.push(indent(`${key}: ${schemaType};`, indentLv));
29-
}
30-
indentLv--;
31-
output.push(indent("};", indentLv));
18+
const schemas = transformSchemaObjectMap(components.schemas, { path: "#/components/schemas/", ctx: { ...ctx, indentLv } });
19+
output.push(indent(`schemas: ${schemas};`, indentLv));
3220
} else {
3321
output.push(indent("schemas: never;", indentLv));
3422
}

packages/openapi-typescript/src/transform/index.ts

+5
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import type { GlobalContext, OpenAPI3 } from "../types.js";
22
import transformComponentsObject from "./components-object.js";
33
import transformPathsObject from "./paths-object.js";
4+
import transformSchemaObjectMap from "./schema-object-map.js";
45
import transformWebhooksObject from "./webhooks-object.js";
56

67
/** transform top-level schema */
@@ -21,5 +22,9 @@ export function transformSchema(schema: OpenAPI3, ctx: GlobalContext): Record<st
2122
if (schema.components) output.components = transformComponentsObject(schema.components, ctx);
2223
else output.components = "";
2324

25+
// $defs
26+
if (schema.$defs) output.$defs = transformSchemaObjectMap(schema.$defs, { path: "$defs/", ctx });
27+
else output.$defs = "";
28+
2429
return output;
2530
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
import type { GlobalContext, ParameterObject, SchemaObject } from "../types.js";
2+
import { escObjKey, getEntries, getSchemaObjectComment, indent, tsReadonly } from "../utils.js";
3+
import transformParameterObject from "./parameter-object.js";
4+
import transformPathItemObject, { Method } from "./path-item-object.js";
5+
import transformSchemaObject from "./schema-object.js";
6+
7+
export interface TransformSchemaMapOptions {
8+
/** The full ID for this object (mostly used in error messages) */
9+
path: string;
10+
/** Shared context */
11+
ctx: GlobalContext;
12+
}
13+
14+
export default function transformSchemaObjectMap(schemaObjMap: Record<string, SchemaObject>, { path, ctx }: TransformSchemaMapOptions): string {
15+
let { indentLv } = ctx;
16+
const output: string[] = ["{"];
17+
indentLv++;
18+
outer: for (const [name, schemaObject] of getEntries(schemaObjMap, ctx.alphabetize, ctx.excludeDeprecated)) {
19+
if (!schemaObject || typeof schemaObject !== "object") continue;
20+
const c = getSchemaObjectComment(schemaObject as SchemaObject, indentLv);
21+
if (c) output.push(indent(c, indentLv));
22+
let key = escObjKey(name);
23+
if (ctx.immutableTypes || schemaObject.readOnly) key = tsReadonly(key);
24+
25+
// Test for Path Item Object
26+
if (!("type" in schemaObject) && !("$ref" in schemaObject)) {
27+
for (const method of ["get", "put", "post", "delete", "options", "head", "patch", "trace"] as Method[]) {
28+
if (method in schemaObject) {
29+
output.push(indent(`${key}: ${transformPathItemObject(schemaObject, { path: `${path}${name}`, ctx: { ...ctx, indentLv } })};`, indentLv));
30+
continue outer;
31+
}
32+
}
33+
}
34+
35+
// Test for Parameter
36+
if ("in" in schemaObject) {
37+
output.push(indent(`${key}: ${transformParameterObject(schemaObject as ParameterObject, { path: `${path}${name}`, ctx: { ...ctx, indentLv } })};`, indentLv));
38+
continue;
39+
}
40+
41+
// Otherwise, this is a Schema Object
42+
output.push(indent(`${key}: ${transformSchemaObject(schemaObject, { path: `${path}${name}`, ctx: { ...ctx, indentLv } })};`, indentLv));
43+
}
44+
indentLv--;
45+
output.push(indent("}", indentLv));
46+
return output.join("\n");
47+
}

packages/openapi-typescript/src/transform/schema-object.ts

+9-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import type { GlobalContext, ReferenceObject, SchemaObject } from "../types.js";
22
import { escObjKey, escStr, getEntries, getSchemaObjectComment, indent, parseRef, tsArrayOf, tsIntersectionOf, tsOmit, tsOneOf, tsOptionalProperty, tsReadonly, tsTupleOf, tsUnionOf, tsWithRequired } from "../utils.js";
3+
import transformSchemaObjectMap from "./schema-object-map.js";
34

45
// There’s just no getting around some really complex type intersections that TS
56
// has trouble following
@@ -163,7 +164,11 @@ export function defaultSchemaObjectTransform(schemaObject: SchemaObject | Refere
163164

164165
// core type: properties + additionalProperties
165166
const coreType: string[] = [];
166-
if (("properties" in schemaObject && schemaObject.properties && Object.keys(schemaObject.properties).length) || ("additionalProperties" in schemaObject && schemaObject.additionalProperties)) {
167+
if (
168+
("properties" in schemaObject && schemaObject.properties && Object.keys(schemaObject.properties).length) ||
169+
("additionalProperties" in schemaObject && schemaObject.additionalProperties) ||
170+
("$defs" in schemaObject && schemaObject.$defs)
171+
) {
167172
indentLv++;
168173
for (const [k, v] of getEntries(schemaObject.properties ?? {}, ctx.alphabetize, ctx.excludeDeprecated)) {
169174
const c = getSchemaObjectComment(v, indentLv);
@@ -198,6 +203,9 @@ export function defaultSchemaObjectTransform(schemaObject: SchemaObject | Refere
198203
coreType.push(indent(`[key: string]: ${addlType ? addlType : "unknown"};`, indentLv));
199204
}
200205
}
206+
if (schemaObject.$defs) {
207+
coreType.push(indent(`$defs: ${transformSchemaObjectMap(schemaObject.$defs, { path: `${path}$defs/`, ctx: { ...ctx, indentLv } })};`, indentLv));
208+
}
201209
indentLv--;
202210
}
203211

packages/openapi-typescript/src/types.ts

+4
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ export interface OpenAPI3 extends Extensable {
3434
tags?: TagObject[];
3535
/** Additional external documentation. */
3636
externalDocs?: ExternalDocumentationObject;
37+
$defs?: $defs;
3738
}
3839

3940
/**
@@ -495,6 +496,7 @@ export interface ObjectSubtype {
495496
allOf?: (SchemaObject | ReferenceObject)[];
496497
anyOf?: (SchemaObject | ReferenceObject)[];
497498
enum?: (SchemaObject | ReferenceObject)[];
499+
$defs?: $defs;
498500
}
499501

500502
/**
@@ -707,6 +709,8 @@ export interface GlobalContext {
707709
excludeDeprecated: boolean;
708710
}
709711

712+
export type $defs = Record<string, SchemaObject>;
713+
710714
// Fetch is available in the global scope starting with Node v18.
711715
// However, @types/node does not have it yet available.
712716
// GitHub issue: https://github.com/DefinitelyTyped/DefinitelyTyped/issues/60924
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
RemoteObject:
2+
type: object
3+
properties:
4+
ownProperty:
5+
type: boolean
6+
$defs:
7+
remoteDef:
8+
type: string
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
openapi: 3.1.0
2+
components:
3+
schemas:
4+
Object:
5+
type: object
6+
properties:
7+
rootDef:
8+
$ref: '#/$defs/StringType'
9+
nestedDef:
10+
$ref: '#/components/schemas/OtherObject/$defs/nestedDef'
11+
remoteDef:
12+
$ref: '#/components/schemas/RemoteDefs/$defs/remoteDef'
13+
$defs:
14+
hasDefs:
15+
type: boolean
16+
ArrayOfDefs:
17+
type: array
18+
items:
19+
$ref: '#/$defs/StringType'
20+
OtherObject:
21+
type: object
22+
$defs:
23+
nestedDef:
24+
type: boolean
25+
RemoteDefs:
26+
type: object
27+
$defs:
28+
remoteDef:
29+
$ref: './_jsonschema-remote-obj.yaml#/RemoteObject/$defs/remoteDef'
30+
$defs:
31+
StringType:
32+
type: string

packages/openapi-typescript/test/index.test.ts

+57
Original file line numberDiff line numberDiff line change
@@ -813,6 +813,63 @@ export type external = Record<string, never>;
813813
export type operations = Record<string, never>;
814814
`);
815815
});
816+
817+
describe("JSONSchema", () => {
818+
test.only("$defs are respected", async () => {
819+
const generated = await openapiTS(new URL("./fixtures/jsonschema-defs.yaml", import.meta.url));
820+
expect(generated).toBe(`${BOILERPLATE}
821+
export type paths = Record<string, never>;
822+
823+
export type webhooks = Record<string, never>;
824+
825+
export interface components {
826+
schemas: {
827+
Object: {
828+
rootDef?: $defs["StringType"];
829+
nestedDef?: components["schemas"]["OtherObject"]["$defs"]["nestedDef"];
830+
remoteDef?: components["schemas"]["RemoteDefs"]["$defs"]["remoteDef"];
831+
$defs: {
832+
hasDefs: boolean;
833+
};
834+
};
835+
ArrayOfDefs: $defs["StringType"][];
836+
OtherObject: {
837+
$defs: {
838+
nestedDef: boolean;
839+
};
840+
};
841+
RemoteDefs: {
842+
$defs: {
843+
remoteDef: external["_jsonschema-remote-obj.yaml"]["RemoteObject"]["$defs"]["remoteDef"];
844+
};
845+
};
846+
};
847+
responses: never;
848+
parameters: never;
849+
requestBodies: never;
850+
headers: never;
851+
pathItems: never;
852+
}
853+
854+
export interface $defs {
855+
StringType: string;
856+
}
857+
858+
export interface external {
859+
"_jsonschema-remote-obj.yaml": {
860+
RemoteObject: {
861+
ownProperty?: boolean;
862+
$defs: {
863+
remoteDef: string;
864+
};
865+
};
866+
};
867+
}
868+
869+
export type operations = Record<string, never>;
870+
`);
871+
});
872+
});
816873
});
817874

818875
describe("options", () => {

packages/openapi-typescript/test/schema-object.test.ts

+31-7
Original file line numberDiff line numberDiff line change
@@ -209,7 +209,7 @@ describe("Schema Object", () => {
209209
type: "object",
210210
properties: { property: { type: "boolean" } },
211211
additionalProperties: { type: "string" },
212-
required: ["property"]
212+
required: ["property"],
213213
};
214214
const generated = transformSchemaObject(schema, options);
215215
expect(generated).toBe(`{
@@ -221,9 +221,9 @@ describe("Schema Object", () => {
221221
test("additionalProperties with partly required properties", () => {
222222
const schema: SchemaObject = {
223223
type: "object",
224-
properties: { property: { type: "boolean" }, property2: { type: "boolean" }},
224+
properties: { property: { type: "boolean" }, property2: { type: "boolean" } },
225225
additionalProperties: { type: "string" },
226-
required: ["property"]
226+
required: ["property"],
227227
};
228228
const generated = transformSchemaObject(schema, options);
229229
expect(generated).toBe(`{
@@ -369,6 +369,11 @@ describe("Schema Object", () => {
369369
expect(generated).toBe(`null | "blue" | "green" | "yellow"`);
370370
});
371371
});
372+
373+
test("unknown", () => {
374+
const generated = transformSchemaObject({}, options);
375+
expect(generated).toBe(`unknown`);
376+
});
372377
});
373378

374379
describe("schema composition", () => {
@@ -793,14 +798,33 @@ describe("Schema Object", () => {
793798
});
794799
});
795800

796-
test("unknown", () => {
797-
const generated = transformSchemaObject({}, options);
798-
expect(generated).toBe(`unknown`);
801+
describe("JSONSchema", () => {
802+
test("$defs are kept (for types that can hold them)", () => {
803+
const generated = transformSchemaObject(
804+
{
805+
type: "object",
806+
properties: {
807+
foo: { type: "string" },
808+
},
809+
$defs: {
810+
defEnum: { type: "string", enum: ["one", "two", "three"] },
811+
},
812+
},
813+
options,
814+
);
815+
expect(generated).toBe(`{
816+
foo?: string;
817+
$defs: {
818+
/** @enum {string} */
819+
defEnum: "one" | "two" | "three";
820+
};
821+
}`);
822+
});
799823
});
800824
});
801825

802826
describe("ReferenceObject", () => {
803-
it("x-* properties are ignored", () => {
827+
test("x-* properties are ignored", () => {
804828
expect(
805829
transformSchemaObject(
806830
{

0 commit comments

Comments
 (0)