Skip to content

Commit 3043dc6

Browse files
authored
Merge pull request #22 from adrianhdezm/codex/generate-zod-schemas-and-typeddict-for-oneof-object
Handle oneOf during schema generation
2 parents 9c1162f + 1a8129a commit 3043dc6

5 files changed

Lines changed: 126 additions & 0 deletions

File tree

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

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,32 @@ export function convertToTypedDict(name: string, schema: OpenAPI.SchemaObject |
9292
return `${refName}`;
9393
}
9494

95+
if ('oneOf' in s && Array.isArray(s.oneOf)) {
96+
if (s.oneOf.length === 0) {
97+
typingImports.add('Any');
98+
return 'Any';
99+
}
100+
if (s.oneOf.length === 1) {
101+
return toType(s.oneOf[0] as any, className);
102+
}
103+
typingImports.add('Union');
104+
const parts = s.oneOf.map((sub, idx) => toType(sub as any, `${className}Option${idx}`)).join(', ');
105+
return `Union[${parts}]`;
106+
}
107+
108+
if ('anyOf' in s && Array.isArray(s.anyOf)) {
109+
if (s.anyOf.length === 0) {
110+
typingImports.add('Any');
111+
return 'Any';
112+
}
113+
if (s.anyOf.length === 1) {
114+
return toType(s.anyOf[0] as any, className);
115+
}
116+
typingImports.add('Union');
117+
const parts = s.anyOf.map((sub, idx) => toType(sub as any, `${className}Option${idx}`)).join(', ');
118+
return `Union[${parts}]`;
119+
}
120+
95121
if (s.enum) {
96122
typingImports.add('Literal');
97123
const values = s.enum.map((v) => JSON.stringify(v)).join(', ');

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

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,30 @@ export function convertSchema(schema: OpenAPI.SchemaObject | OpenAPI.ReferenceOb
2828
return expr;
2929
}
3030

31+
if ('oneOf' in s && Array.isArray(s.oneOf)) {
32+
const items = s.oneOf as Array<OpenAPI.SchemaObject | OpenAPI.ReferenceObject>;
33+
if (items.length === 0) {
34+
return 'z.any()';
35+
}
36+
if (items.length === 1) {
37+
return walk(items[0]!);
38+
}
39+
const parts = items.map((sub) => walk(sub)).join(', ');
40+
return `z.union([${parts}])`;
41+
}
42+
43+
if ('anyOf' in s && Array.isArray(s.anyOf)) {
44+
const items = s.anyOf as Array<OpenAPI.SchemaObject | OpenAPI.ReferenceObject>;
45+
if (items.length === 0) {
46+
return 'z.any()';
47+
}
48+
if (items.length === 1) {
49+
return walk(items[0]!);
50+
}
51+
const parts = items.map((sub) => walk(sub)).join(', ');
52+
return `z.union([${parts}])`;
53+
}
54+
3155
if (s.enum) {
3256
const values = s.enum.map((v) => JSON.stringify(v)).join(', ');
3357
return `z.enum([${values}])`;

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

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,3 +38,13 @@ class Place(TypedDict, total=False):
3838
name: Required[str]
3939
location: Required[PlaceLocation]"
4040
`;
41+
42+
exports[`generate-python-dict > handles oneOf with a single ref 1`] = `
43+
"from typing import TypedDict
44+
MessageSingle = A"
45+
`;
46+
47+
exports[`generate-python-dict > handles oneOf with refs 1`] = `
48+
"from typing import TypedDict, Union
49+
Message = Union[A, B]"
50+
`;

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

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,53 @@ describe('generate-python-dict', () => {
102102
expect(content).toMatchSnapshot();
103103
});
104104

105+
it('handles oneOf with refs', () => {
106+
const doc: OpenAPI.Document = {
107+
openapi: '3.1.0',
108+
info: { title: 't', version: '1' },
109+
paths: {},
110+
components: {
111+
schemas: {
112+
A: { type: 'object', properties: { id: { type: 'string' } } },
113+
B: { type: 'object', properties: { value: { type: 'number' } } },
114+
Message: { oneOf: [{ $ref: '#/components/schemas/A' }, { $ref: '#/components/schemas/B' }] }
115+
}
116+
}
117+
};
118+
const schemas = extractSchemas(doc, null);
119+
const { definition, typingImports } = convertToTypedDict('Message', schemas.Message as OpenAPI.SchemaObject);
120+
const typingLine = `from typing import ${Array.from(typingImports).join(', ')}`;
121+
const content = [typingLine, '', definition, ''].filter(Boolean).join('\n');
122+
const tmp = path.join(__dirname, 'tmp_oneof.py');
123+
fs.writeFileSync(tmp, content);
124+
child_process.execSync(`python3 -m py_compile ${tmp}`);
125+
fs.unlinkSync(tmp);
126+
expect(content).toMatchSnapshot();
127+
});
128+
129+
it('handles oneOf with a single ref', () => {
130+
const doc: OpenAPI.Document = {
131+
openapi: '3.1.0',
132+
info: { title: 't', version: '1' },
133+
paths: {},
134+
components: {
135+
schemas: {
136+
A: { type: 'object', properties: { id: { type: 'string' } } },
137+
MessageSingle: { oneOf: [{ $ref: '#/components/schemas/A' }] }
138+
}
139+
}
140+
};
141+
const schemas = extractSchemas(doc, null);
142+
const { definition, typingImports } = convertToTypedDict('MessageSingle', schemas.MessageSingle as OpenAPI.SchemaObject);
143+
const typingLine = `from typing import ${Array.from(typingImports).join(', ')}`;
144+
const content = [typingLine, '', definition, ''].filter(Boolean).join('\n');
145+
const tmp = path.join(__dirname, 'tmp_oneof_single.py');
146+
fs.writeFileSync(tmp, content);
147+
child_process.execSync(`python3 -m py_compile ${tmp}`);
148+
fs.unlinkSync(tmp);
149+
expect(content).toMatchSnapshot();
150+
});
151+
105152
it('handles inline object properties', () => {
106153
const doc: OpenAPI.Document = {
107154
openapi: '3.1.0',

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

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,25 @@ describe('convertSchema', () => {
5353
expect(imports.has('Base')).toBe(true);
5454
});
5555

56+
it('handles oneOf with refs', () => {
57+
const schema = {
58+
oneOf: [{ $ref: '#/components/schemas/A' }, { $ref: '#/components/schemas/B' }]
59+
} as OpenAPI.SchemaObject;
60+
const { zodString, imports } = convertSchema(schema);
61+
expect(zodString).toBe('z.union([A, B])');
62+
expect(imports.has('A')).toBe(true);
63+
expect(imports.has('B')).toBe(true);
64+
});
65+
66+
it('handles oneOf with a single ref', () => {
67+
const schema = {
68+
oneOf: [{ $ref: '#/components/schemas/A' }]
69+
} as OpenAPI.SchemaObject;
70+
const { zodString, imports } = convertSchema(schema);
71+
expect(zodString).toBe('A');
72+
expect(imports.has('A')).toBe(true);
73+
});
74+
5675
it('converts inline object properties', () => {
5776
const schema = {
5877
type: 'object',

0 commit comments

Comments
 (0)