Skip to content

Commit e526771

Browse files
authored
Merge pull request #19 from adrianhdezm/codex/add-tests-for-json-schema-allof-functionality
feat(utils): handle allOf in schema conversions
2 parents 81e6f5a + 1a10012 commit e526771

5 files changed

Lines changed: 88 additions & 7 deletions

File tree

src/utils/json-schema-to-typed-dict.ts

Lines changed: 33 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,29 @@ export interface TypedDictResult {
88
export function convertToTypedDict(name: string, schema: OpenAPI.SchemaObject | OpenAPI.ReferenceObject): TypedDictResult {
99
const typingImports = new Set<string>(['TypedDict']);
1010

11+
function flatten(s: OpenAPI.SchemaObject | OpenAPI.ReferenceObject): { bases: string[]; schema: OpenAPI.SchemaObject | null } {
12+
if ('$ref' in s) {
13+
const match = s.$ref.match(/^#\/components\/schemas\/(.+)$/);
14+
return { bases: [match?.[1] ?? s.$ref], schema: null };
15+
}
16+
17+
if ('allOf' in s && Array.isArray(s.allOf)) {
18+
const bases: string[] = [];
19+
const merged: OpenAPI.SchemaObject = { type: 'object', properties: {}, required: [] };
20+
for (const sub of s.allOf as Array<OpenAPI.SchemaObject | OpenAPI.ReferenceObject>) {
21+
const res = flatten(sub);
22+
bases.push(...res.bases);
23+
if (res.schema) {
24+
Object.assign(merged.properties!, res.schema.properties);
25+
merged.required = Array.from(new Set([...(merged.required ?? []), ...(res.schema.required ?? [])]));
26+
}
27+
}
28+
return { bases, schema: merged };
29+
}
30+
31+
return { bases: [], schema: s };
32+
}
33+
1134
function toType(s: OpenAPI.SchemaObject | OpenAPI.ReferenceObject): string {
1235
if ('$ref' in s) {
1336
const match = s.$ref.match(/^#\/components\/schemas\/(.+)$/);
@@ -43,10 +66,12 @@ export function convertToTypedDict(name: string, schema: OpenAPI.SchemaObject |
4366
}
4467
}
4568

69+
const { bases, schema: flat } = flatten(schema);
70+
4671
let definition = '';
47-
if (!('$ref' in schema) && schema.type === 'object') {
48-
const props = schema.properties ?? {};
49-
const required = new Set(schema.required ?? []);
72+
if (flat && flat.type === 'object') {
73+
const props = flat.properties ?? {};
74+
const required = new Set(flat.required ?? []);
5075
const fields: string[] = [];
5176
const attrLines: string[] = [];
5277
for (const [key, value] of Object.entries(props)) {
@@ -65,13 +90,14 @@ export function convertToTypedDict(name: string, schema: OpenAPI.SchemaObject |
6590
if (fields.length === 0) {
6691
fields.push(' pass');
6792
}
68-
const header = [`class ${name}(TypedDict, total=False):`];
93+
const baseList = bases.length > 0 ? `${bases.join(', ')}, ` : '';
94+
const header = [`class ${name}(${baseList}TypedDict, total=False):`];
6995
const docLines: string[] = [];
70-
if (schema.description) {
71-
docLines.push((schema.description as string).replace(/\n/g, ' '));
96+
if (flat.description) {
97+
docLines.push((flat.description as string).replace(/\n/g, ' '));
7298
}
7399
if (attrLines.length > 0) {
74-
if (schema.description) docLines.push('');
100+
if (flat.description) docLines.push('');
75101
docLines.push('Attributes:');
76102
for (const line of attrLines) {
77103
docLines.push(` ${line}`);

src/utils/json-schema-to-zod.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,18 @@ export function convertSchema(schema: OpenAPI.SchemaObject | OpenAPI.ReferenceOb
1616
return name;
1717
}
1818

19+
if ('allOf' in s && Array.isArray(s.allOf)) {
20+
const items = s.allOf as Array<OpenAPI.SchemaObject | OpenAPI.ReferenceObject>;
21+
if (items.length === 0) {
22+
return 'z.any()';
23+
}
24+
let expr = walk(items[0]!);
25+
for (const sub of items.slice(1)) {
26+
expr = `z.intersection(${expr}, ${walk(sub)})`;
27+
}
28+
return expr;
29+
}
30+
1931
if (s.enum) {
2032
const values = s.enum.map((v) => JSON.stringify(v)).join(', ');
2133
return `z.enum([${values}])`;

tests/__snapshots__/json-schema-to-typed-dict.spec.ts.snap

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,3 +21,9 @@ exports[`generate-python-dict > generates enums and nested arrays 1`] = `
2121
"from typing import TypedDict, List
2222
Wrapper = List[Status]"
2323
`;
24+
25+
exports[`generate-python-dict > handles allOf with refs and properties 1`] = `
26+
"from typing import TypedDict, Required
27+
class Derived(Base, TypedDict, total=False):
28+
extra: Required[str]"
29+
`;

tests/json-schema-to-typed-dict.spec.ts

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,34 @@ describe('generate-python-dict', () => {
7474
expect(definition).toMatchSnapshot();
7575
});
7676

77+
it('handles allOf with refs and properties', () => {
78+
const doc: OpenAPI.Document = {
79+
openapi: '3.1.0',
80+
info: { title: 't', version: '1' },
81+
paths: {},
82+
components: {
83+
schemas: {
84+
Base: { type: 'object', properties: { id: { type: 'string' } }, required: ['id'] },
85+
Derived: {
86+
allOf: [
87+
{ $ref: '#/components/schemas/Base' },
88+
{ type: 'object', properties: { extra: { type: 'string' } }, required: ['extra'] }
89+
]
90+
}
91+
}
92+
}
93+
};
94+
const schemas = extractSchemas(doc, null);
95+
const { definition, typingImports } = convertToTypedDict('Derived', schemas.Derived as OpenAPI.SchemaObject);
96+
const typingLine = `from typing import ${Array.from(typingImports).join(', ')}`;
97+
const content = [typingLine, '', definition, ''].filter(Boolean).join('\n');
98+
const tmp = path.join(__dirname, 'tmp_allof.py');
99+
fs.writeFileSync(tmp, content);
100+
child_process.execSync(`python3 -m py_compile ${tmp}`);
101+
fs.unlinkSync(tmp);
102+
expect(content).toMatchSnapshot();
103+
});
104+
77105
it('filters schemas by path prefixes', () => {
78106
const doc: OpenAPI.Document = {
79107
openapi: '3.1.0',

tests/json-schema-to-zod.spec.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,4 +43,13 @@ describe('convertSchema', () => {
4343
const { zodString } = convertSchema(schema);
4444
expect(zodString).toBe('z.object({\n id: z.string().meta({ description: "identifier" })\n})');
4545
});
46+
47+
it('handles allOf with refs and properties', () => {
48+
const schema = {
49+
allOf: [{ $ref: '#/components/schemas/Base' }, { type: 'object', properties: { extra: { type: 'string' } }, required: ['extra'] }]
50+
} as OpenAPI.SchemaObject;
51+
const { zodString, imports } = convertSchema(schema);
52+
expect(zodString).toBe('z.intersection(Base, z.object({\n extra: z.string()\n}))');
53+
expect(imports.has('Base')).toBe(true);
54+
});
4655
});

0 commit comments

Comments
 (0)