Skip to content

Commit 114d0e3

Browse files
author
nx-plugin-for-aws
committed
fix(open-api): hoist composite parameter schemas to components.schemas
FastAPI / Pydantic v2 encode `Optional[T]` parameters as inline composite schemas on the parameter (e.g. `{anyOf: [{type: string}, {type: null}], title: 'X-Client'}`). The hoisting pass walked request/response JSON schemas but not parameter schemas, so hey-api-openapi synthesised an anonymous composite named from the title (`XClient`, `Session`, …) without emitting a declaration, and every generated FastAPI client failed to compile: types.gen.ts: Cannot find name 'XClient' pyright: '"Session" is not defined' Extends the existing hoist to also move composite parameter schemas into `components.schemas` under `<OpId>Request<In><Name>`.
1 parent c99ebee commit 114d0e3

2 files changed

Lines changed: 105 additions & 0 deletions

File tree

packages/nx-plugin/src/open-api/ts-client/generator.fast-api.spec.ts

Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -975,4 +975,94 @@ describe('openApiTsClientGenerator - FastAPI', () => {
975975
],
976976
});
977977
});
978+
979+
it('should hoist inline composite parameter schemas so FastAPI `Optional[T]` params compile', async () => {
980+
// Regression: FastAPI / Pydantic v2 encode `Optional[T]` params as
981+
// `{anyOf: [T, {type: 'null'}], title: '<name>'}` inline on the
982+
// parameter. hey-api used to derive a type name from the title
983+
// (e.g. `XClient`, `Session`, `Tag`) but no declaration was emitted,
984+
// so the generated client failed to compile with:
985+
//
986+
// types.gen.ts: Cannot find name 'XClient'
987+
// client.gen.ts: Property 'XClient' does not exist on type '$IO'
988+
//
989+
// Existing parameter tests covered primitive schemas (`{type: string}`)
990+
// and the composite tests covered `components.schemas.*` — both of
991+
// which were hoisted. Composite parameter-level schemas were the
992+
// uncovered gap.
993+
const spec: Spec = {
994+
openapi: '3.1.0',
995+
info: { title, version: '1.0.0' },
996+
paths: {
997+
'/pets/{petId}': {
998+
get: {
999+
operationId: 'getPet',
1000+
parameters: [
1001+
{
1002+
name: 'petId',
1003+
in: 'path',
1004+
required: true,
1005+
schema: { type: 'integer' },
1006+
},
1007+
{
1008+
name: 'X-Client',
1009+
in: 'header',
1010+
schema: {
1011+
anyOf: [{ type: 'string' }, { type: 'null' }],
1012+
title: 'X-Client',
1013+
} as any,
1014+
},
1015+
{
1016+
name: 'session',
1017+
in: 'cookie',
1018+
schema: {
1019+
anyOf: [{ type: 'string' }, { type: 'null' }],
1020+
title: 'Session',
1021+
} as any,
1022+
},
1023+
{
1024+
name: 'tag',
1025+
in: 'query',
1026+
schema: {
1027+
anyOf: [
1028+
{ type: 'array', items: { type: 'string' } },
1029+
{ type: 'null' },
1030+
],
1031+
title: 'Tag',
1032+
} as any,
1033+
},
1034+
],
1035+
responses: {
1036+
'200': {
1037+
description: 'ok',
1038+
content: {
1039+
'application/json': { schema: { type: 'string' } },
1040+
},
1041+
},
1042+
},
1043+
},
1044+
},
1045+
},
1046+
};
1047+
tree.write('openapi.json', JSON.stringify(spec));
1048+
await openApiTsClientGenerator(tree, {
1049+
openApiSpecPath: 'openapi.json',
1050+
outputPath: 'src/generated',
1051+
});
1052+
1053+
// Must actually compile (the phantom composites used to break tsc).
1054+
validateTypeScript([
1055+
'src/generated/client.gen.ts',
1056+
'src/generated/types.gen.ts',
1057+
]);
1058+
1059+
const types = tree.read('src/generated/types.gen.ts', 'utf-8')!;
1060+
// The hoisted composite schemas get operation-scoped names.
1061+
expect(types).toMatch(/GetPetRequestHeaderXClient\s*=\s*string \| null/);
1062+
expect(types).toMatch(/GetPetRequestCookieSession\s*=\s*string \| null/);
1063+
expect(types).toMatch(/GetPetRequestQueryTag\s*=\s*Array<string> \| null/);
1064+
expect(types).toContain('xClient?: GetPetRequestHeaderXClient');
1065+
expect(types).toContain('session?: GetPetRequestCookieSession');
1066+
expect(types).toContain('tag?: GetPetRequestQueryTag');
1067+
});
9781068
});

packages/nx-plugin/src/open-api/utils/normalise.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -389,6 +389,21 @@ export const normaliseOpenApiSpecForCodeGen = (inSpec: Spec): Spec => {
389389
};
390390
}
391391
}
392+
393+
// Hoist parameter schemas
394+
(operation.parameters ?? []).forEach((p) => {
395+
const param = resolveIfRef(spec, p);
396+
const paramSchema = param?.schema;
397+
if (
398+
paramSchema &&
399+
!isRef(paramSchema) &&
400+
isCompositeSchema(paramSchema as OpenAPIV3.SchemaObject)
401+
) {
402+
const schemaName = `${upperFirst(deduplicatedOpId)}Request${upperFirst(param.in)}${upperFirst(camelCase(param.name))}`;
403+
spec.components!.schemas![schemaName] = paramSchema;
404+
param.schema = { $ref: `#/components/schemas/${schemaName}` };
405+
}
406+
});
392407
}
393408
}),
394409
);

0 commit comments

Comments
 (0)