Skip to content

Commit ef6afaf

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

File tree

19 files changed

+220
-57
lines changed

19 files changed

+220
-57
lines changed

packages/openapi-fetch/examples/nextjs/lib/api/v1.d.ts

+2
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,8 @@ export interface components {
9898
pathItems: never;
9999
}
100100

101+
export type $defs = Record<string, never>;
102+
101103
export type external = Record<string, never>;
102104

103105
export interface operations {

packages/openapi-fetch/examples/react-query/src/lib/api/v1.d.ts

+2
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,8 @@ export interface components {
9898
pathItems: never;
9999
}
100100

101+
export type $defs = Record<string, never>;
102+
101103
export type external = Record<string, never>;
102104

103105
export interface operations {

packages/openapi-fetch/examples/sveltekit/src/lib/api/v1.d.ts

+2
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,8 @@ export interface components {
9898
pathItems: never;
9999
}
100100

101+
export type $defs = Record<string, never>;
102+
101103
export type external = Record<string, never>;
102104

103105
export interface operations {

packages/openapi-fetch/test/v1.d.ts

+2
Original file line numberDiff line numberDiff line change
@@ -422,6 +422,8 @@ export interface components {
422422
pathItems: never;
423423
}
424424

425+
export type $defs = Record<string, never>;
426+
425427
export type external = Record<string, never>;
426428

427429
export interface operations {

packages/openapi-typescript/examples/github-api-next.ts

+2
Original file line numberDiff line numberDiff line change
@@ -79583,6 +79583,8 @@ export interface components {
7958379583
pathItems: never;
7958479584
}
7958579585

79586+
export type $defs = Record<string, never>;
79587+
7958679588
export type external = Record<string, never>;
7958779589

7958879590
export interface operations {

packages/openapi-typescript/examples/github-api.ts

+2
Original file line numberDiff line numberDiff line change
@@ -81708,6 +81708,8 @@ export interface components {
8170881708
pathItems: never;
8170981709
}
8171081710

81711+
export type $defs = Record<string, never>;
81712+
8171181713
export type external = Record<string, never>;
8171281714

8171381715
export interface operations {

packages/openapi-typescript/examples/octokit-ghes-3.6-diff-to-api.ts

+2
Original file line numberDiff line numberDiff line change
@@ -5807,6 +5807,8 @@ export interface components {
58075807
pathItems: never;
58085808
}
58095809

5810+
export type $defs = Record<string, never>;
5811+
58105812
export type external = Record<string, never>;
58115813

58125814
export interface operations {

packages/openapi-typescript/examples/stripe-api.ts

+2
Original file line numberDiff line numberDiff line change
@@ -16173,6 +16173,8 @@ export interface components {
1617316173
pathItems: never;
1617416174
}
1617516175

16176+
export type $defs = Record<string, never>;
16177+
1617616178
export type external = Record<string, never>;
1617716179

1617816180
export interface operations {

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/load.ts

+2-1
Original file line numberDiff line numberDiff line change
@@ -428,12 +428,13 @@ function getHintFromResponseObject(path: (string | number)[], external: boolean)
428428
function getHintFromSchemaObject(path: (string | number)[], external: boolean): Subschema["hint"] {
429429
switch (path[0]) {
430430
case "allOf":
431+
return "SchemaMap";
431432
case "anyOf":
432433
case "oneOf":
433434
return getHintFromSchemaObject(path.slice(2), external); // skip array index at [1]
434435
}
435436
// if this is external, and the path is [filename, key], then the external schema is probably a SchemaMap
436-
if (path.length === 2 && external) {
437+
if (path.length >= 2 && external) {
437438
return "SchemaMap";
438439
}
439440
// otherwise, path length of 1 means partial schema is likely a SchemaObject (or it’s unknown, in which case assume 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

0 commit comments

Comments
 (0)