Skip to content

Commit e2d8541

Browse files
authored
fix(openapi-typescript): include location in error messages (#1999)
1 parent 35c576c commit e2d8541

File tree

4 files changed

+60
-30
lines changed

4 files changed

+60
-30
lines changed

.changeset/three-bobcats-mate.md

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"openapi-typescript": patch
3+
---
4+
5+
Improved error messages to contain locations.

packages/openapi-typescript/src/lib/redoc.ts

+22-29
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import {
66
Source,
77
type Document,
88
lintDocument,
9+
type NormalizedProblem,
910
} from "@redocly/openapi-core";
1011
import { performance } from "node:perf_hooks";
1112
import { Readable } from "node:stream";
@@ -81,6 +82,25 @@ export async function parseSchema(schema: unknown, { absoluteRef, resolver }: Pa
8182
throw new Error(`Expected string, object, or Buffer. Got ${Array.isArray(schema) ? "Array" : typeof schema}`);
8283
}
8384

85+
function _processProblems(problems: NormalizedProblem[], options: { silent: boolean }) {
86+
if (problems.length) {
87+
let errorMessage: string | undefined = undefined;
88+
for (const problem of problems) {
89+
const problemLocation = problem.location?.[0].pointer;
90+
const problemMessage = problemLocation ? `${problem.message} at ${problemLocation}` : problem.message;
91+
if (problem.severity === "error") {
92+
errorMessage = problemMessage;
93+
error(problemMessage);
94+
} else {
95+
warn(problemMessage, options.silent);
96+
}
97+
}
98+
if (errorMessage) {
99+
throw new Error(errorMessage);
100+
}
101+
}
102+
}
103+
84104
/**
85105
* Validate an OpenAPI schema and flatten into a single schema using Redocly CLI
86106
*/
@@ -127,20 +147,7 @@ export async function validateAndBundle(
127147
config: options.redoc.styleguide,
128148
externalRefResolver: resolver,
129149
});
130-
if (problems.length) {
131-
let errorMessage: string | undefined = undefined;
132-
for (const problem of problems) {
133-
if (problem.severity === "error") {
134-
errorMessage = problem.message;
135-
error(problem.message);
136-
} else {
137-
warn(problem.message, options.silent);
138-
}
139-
}
140-
if (errorMessage) {
141-
throw new Error(errorMessage);
142-
}
143-
}
150+
_processProblems(problems, options);
144151
debug("Linted schema", "lint", performance.now() - redocLintT);
145152

146153
// 3. bundle
@@ -150,21 +157,7 @@ export async function validateAndBundle(
150157
dereference: false,
151158
doc: document,
152159
});
153-
if (bundled.problems.length) {
154-
let errorMessage: string | undefined = undefined;
155-
for (const problem of bundled.problems) {
156-
if (problem.severity === "error") {
157-
errorMessage = problem.message;
158-
error(problem.message);
159-
throw new Error(problem.message);
160-
} else {
161-
warn(problem.message, options.silent);
162-
}
163-
}
164-
if (errorMessage) {
165-
throw new Error(errorMessage);
166-
}
167-
}
160+
_processProblems(bundled.problems, options);
168161
debug("Bundled schema", "bundle", performance.now() - redocBundleT);
169162

170163
return bundled.bundle.parsed;

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

+3-1
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,9 @@ export function transformSchemaObjectWithComposition(
6666
}
6767
// for any other unexpected type, throw error
6868
if (Array.isArray(schemaObject) || typeof schemaObject !== "object") {
69-
throw new Error(`Expected SchemaObject, received ${Array.isArray(schemaObject) ? "Array" : typeof schemaObject}`);
69+
throw new Error(
70+
`Expected SchemaObject, received ${Array.isArray(schemaObject) ? "Array" : typeof schemaObject} at ${options.path}`,
71+
);
7072
}
7173

7274
/**

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

+30
Original file line numberDiff line numberDiff line change
@@ -19,4 +19,34 @@ describe("Invalid schemas", () => {
1919
test("Other missing required fields", async () => {
2020
await expect(() => openapiTS({} as any)).rejects.toThrowError("Unsupported schema format, expected `openapi: 3.x`");
2121
});
22+
23+
test("Unresolved $ref error messages", async () => {
24+
const consoleErrorSpy = vi.spyOn(console, "error").mockImplementation(() => {});
25+
26+
await expect(() =>
27+
openapiTS({
28+
openapi: "3.1",
29+
info: { title: "test", version: "1.0" },
30+
components: {
31+
schemas: {
32+
Pet: {
33+
type: "object",
34+
properties: {
35+
category: { $ref: "#/components/schemas/NonExistingSchema" },
36+
type: { $ref: "#/components/schemas/AnotherSchema" },
37+
},
38+
},
39+
},
40+
},
41+
}),
42+
).rejects.toThrowError("Can't resolve $ref at #/components/schemas/Pet/properties/type");
43+
44+
expect(consoleErrorSpy).toHaveBeenCalledTimes(2);
45+
expect(consoleErrorSpy).toHaveBeenCalledWith(
46+
expect.stringContaining("Can't resolve $ref at #/components/schemas/Pet/properties/category"),
47+
);
48+
expect(consoleErrorSpy).toHaveBeenCalledWith(
49+
expect.stringContaining("Can't resolve $ref at #/components/schemas/Pet/properties/type"),
50+
);
51+
});
2252
});

0 commit comments

Comments
 (0)