Resolve OpenAPI 3.1 reference examples correctly (#592)

This commit is contained in:
Gregory Schier
2026-08-19 22:57:07 -07:00
committed by GitHub
parent 95b1beffcf
commit 2c4e49b8f6
2 changed files with 612 additions and 46 deletions
@@ -66,6 +66,410 @@ describe("importer-openapi", () => {
]);
});
test("Imports OpenAPI 3.1 schema reference siblings and examples", async () => {
const imported = await convertOpenApi(
JSON.stringify({
openapi: "3.1.0",
info: { title: "Reference Examples", version: "1.0.0" },
paths: {
"/sibling": {
post: {
requestBody: {
content: {
"application/json": {
schema: {
$ref: "#/components/schemas/Message",
example: { text: "overridden by sibling" },
},
},
},
},
responses: {},
},
},
"/example-ref": {
post: {
requestBody: {
content: {
"application/json": {
examples: { sample: { $ref: "#/components/examples/Message" } },
},
},
},
responses: {},
},
},
"/schema-values": {
post: {
requestBody: {
content: {
"application/json": {
schema: {
type: "object",
properties: {
fromExamples: { type: "string", examples: ["first", "second"] },
fromConst: { const: "fixed" },
},
},
},
},
},
responses: {},
},
},
"/sibling-form": {
post: {
requestBody: {
content: {
"multipart/form-data": {
schema: {
$ref: "#/components/schemas/MessageForm",
required: ["extra"],
properties: { extra: { type: "string", default: "sibling" } },
},
},
},
},
responses: {},
},
},
},
components: {
schemas: {
Message: { type: "object", properties: { text: { default: "base" } } },
MessageForm: {
$ref: "#/components/schemas/BaseMessageForm",
required: ["middle"],
properties: {
middle: { type: "string", default: "intermediate" },
optional: { type: "string", default: "optional" },
},
},
BaseMessageForm: {
type: "object",
required: ["base"],
properties: { base: { type: "string", default: "referenced" } },
},
},
examples: {
Message: { value: { text: "resolved example" } },
},
},
}),
);
expect(imported?.resources.httpRequests.map((request) => request.body)).toEqual([
{ text: JSON.stringify({ text: "overridden by sibling" }, null, 2) },
{ text: JSON.stringify({ text: "resolved example" }, null, 2) },
{ text: JSON.stringify({ fromExamples: "first", fromConst: "fixed" }, null, 2) },
{
form: [
{ enabled: true, name: "base", value: "referenced" },
{ enabled: true, name: "middle", value: "intermediate" },
{ enabled: false, name: "optional", value: "optional" },
{ enabled: true, name: "extra", value: "sibling" },
],
},
]);
});
test("Merges colliding and composed schema properties", async () => {
const imported = await convertOpenApi(
JSON.stringify({
openapi: "3.1.0",
info: { title: "Composed Schema Examples", version: "1.0.0" },
paths: {
"/colliding-property": {
post: {
requestBody: {
content: {
"application/xml": {
schema: {
$ref: "#/components/schemas/BasePayload",
properties: {
shared: {
xml: { name: "renamed" },
properties: {
local: { type: "string", default: "sibling" },
},
},
},
},
},
},
},
responses: {},
},
},
"/composition-siblings": {
post: {
requestBody: {
content: {
"application/json": {
schema: {
allOf: [
{
type: "object",
properties: {
shared: {
type: "object",
properties: {
fromBranch: { type: "string", default: "branch" },
},
},
branchOnly: { type: "string", default: "branch" },
},
},
],
properties: {
shared: {
type: "object",
properties: {
fromSibling: { type: "string", default: "sibling" },
},
},
siblingOnly: { type: "string", default: "sibling" },
},
},
},
},
},
responses: {},
},
},
"/composition-form": {
post: {
requestBody: {
content: {
"multipart/form-data": {
schema: {
$ref: "#/components/schemas/ComposedForm",
required: ["siblingField"],
properties: {
siblingField: { type: "string", default: "sibling" },
},
},
},
},
},
responses: {},
},
},
},
components: {
schemas: {
Shared: {
type: "object",
xml: { namespace: "urn:shared", prefix: "s" },
properties: {
inherited: { type: "string", default: "base" },
},
},
BasePayload: {
type: "object",
xml: { name: "payload" },
properties: {
shared: {
$ref: "#/components/schemas/Shared",
xml: { name: "base-shared" },
},
},
},
ComposedForm: {
allOf: [
{
type: "object",
required: ["baseField"],
properties: {
baseField: { type: "string", default: "base" },
},
},
],
},
},
},
}),
);
expect(imported?.resources.httpRequests.map((request) => request.body)).toEqual([
{
text:
'<payload><s:renamed xmlns:s="urn:shared">' +
"<inherited>base</inherited><local>sibling</local>" +
"</s:renamed></payload>",
},
{
text: JSON.stringify(
{
shared: { fromBranch: "branch", fromSibling: "sibling" },
branchOnly: "branch",
siblingOnly: "sibling",
},
null,
2,
),
},
{
form: [
{ enabled: true, name: "baseField", value: "base" },
{ enabled: true, name: "siblingField", value: "sibling" },
],
},
]);
});
test("Stops circular schema references when generating examples", async () => {
const imported = await convertOpenApi(
JSON.stringify({
openapi: "3.1.0",
info: { title: "Circular References", version: "1.0.0" },
paths: {
"/nodes": {
post: {
requestBody: {
content: {
"application/json": { schema: { $ref: "#/components/schemas/Node" } },
},
},
responses: {},
},
},
},
components: {
schemas: {
Node: {
type: "object",
properties: {
name: { type: "string", example: "root" },
child: {
$ref: "#/components/schemas/Node",
required: ["relationship"],
properties: {
relationship: { type: "string", example: "nested" },
},
},
},
},
},
},
}),
);
expect(imported?.resources.httpRequests[0]?.body).toEqual({
text: JSON.stringify({ name: "root", child: { relationship: "nested" } }, null, 2),
});
});
test("Bounds deeply colliding schema merges", async () => {
const depth = 12_000;
const nestedSchema = (leaf: string, levels: number) =>
'{"type":"object","properties":{"next":'.repeat(levels) + leaf + "}}".repeat(levels);
const baseSchema = nestedSchema('{"type":"string","default":"base"}', depth);
const siblingProperty = nestedSchema('{"type":"string","example":"sibling"}', depth - 1);
const imported = await convertOpenApi(
'{"openapi":"3.1.0","info":{"title":"Deep Merge","version":"1.0.0"},' +
'"paths":{"/deep":{"post":{"requestBody":{"content":{"application/json":' +
'{"schema":{"$ref":"#/components/schemas/DeepBase","properties":{"next":' +
siblingProperty +
'}}}}},"responses":{}}}},"components":{"schemas":{"DeepBase":' +
baseSchema +
"}}}",
);
expect(imported?.resources.httpRequests[0]?.body).toEqual({
text: JSON.stringify(
{
next: {
next: {
next: {
next: {
next: {
next: {
next: {
next: {
next: {},
},
},
},
},
},
},
},
},
},
null,
2,
),
});
});
test("Bounds deeply nested inline allOf schemas", async () => {
const depth = 12_000;
const schema = '{"allOf":['.repeat(depth) + '{"type":"string","example":"leaf"}' + "]}".repeat(depth);
const imported = await convertOpenApi(
'{"openapi":"3.1.0","info":{"title":"Deep allOf","version":"1.0.0"},' +
'"paths":{"/deep":{"post":{"requestBody":{"content":{"application/json":{"schema":' +
schema +
'}}},"responses":{}}}}}',
);
expect(imported?.resources.httpRequests[0]?.body).toEqual({
text: JSON.stringify({}, null, 2),
});
});
test("Resolves long local reference chains without truncating schemas", async () => {
const depth = 12_000;
const schemas: Record<string, unknown> = {
[`Ref${depth}`]: {
type: "object",
properties: { target: { type: "string", example: "reached" } },
},
};
for (let index = depth - 1; index >= 0; index--) {
schemas[`Ref${index}`] = {
$ref: `#/components/schemas/Ref${index + 1}`,
...(index === 1
? { properties: { middle: { type: "string", example: "sibling" } } }
: {}),
};
}
const imported = await convertOpenApi(
JSON.stringify({
openapi: "3.1.0",
info: { title: "Long Reference Chain", version: "1.0.0" },
paths: {
"/long-ref": {
post: {
requestBody: {
content: {
"application/json": {
schema: {
$ref: "#/components/schemas/Ref0",
properties: { outer: { type: "string", example: "request" } },
},
},
},
},
responses: {},
},
},
},
components: { schemas },
}),
);
expect(imported?.resources.httpRequests[0]?.body).toEqual({
text: JSON.stringify(
{ target: "reached", middle: "sibling", outer: "request" },
null,
2,
),
});
});
test("Imports requests directly from OpenAPI details", async () => {
const imported = await convertOpenApi(
JSON.stringify({