Skip to content

Commit d6c2f4d

Browse files
committed
feat(utils): ✨ Support typed records for additionalProperties
1 parent 3043dc6 commit d6c2f4d

5 files changed

Lines changed: 69 additions & 0 deletions

File tree

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

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -142,6 +142,10 @@ export function convertToTypedDict(name: string, schema: OpenAPI.SchemaObject |
142142
buildClass(className, s);
143143
return className;
144144
}
145+
if (s.additionalProperties && typeof s.additionalProperties === 'object') {
146+
const valType = toType(s.additionalProperties as any, `${className}Additional`);
147+
return `dict[str, ${valType}]`;
148+
}
145149
typingImports.add('Any');
146150
return 'dict[str, Any]';
147151
default:

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

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,10 @@ export function convertSchema(schema: OpenAPI.SchemaObject | OpenAPI.ReferenceOb
7070
return `z.array(${walk(s.items)})`;
7171
case 'object':
7272
default: {
73+
const hasProps = s.properties && Object.keys(s.properties).length > 0;
74+
if (!hasProps && typeof s.additionalProperties === 'object') {
75+
return `z.record(${walk(s.additionalProperties as any)})`;
76+
}
7377
const props = s.properties ?? {};
7478
const required = new Set(s.required ?? []);
7579
const fields = Object.entries(props).map(([key, value]) => {

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

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,13 @@ class Place(TypedDict, total=False):
3939
location: Required[PlaceLocation]"
4040
`;
4141

42+
exports[`generate-python-dict > handles objects with additional properties 1`] = `
43+
"from typing import TypedDict
44+
class Config(TypedDict, total=False):
45+
logit_bias: dict[str, int]
46+
metadata: dict[str, str]"
47+
`;
48+
4249
exports[`generate-python-dict > handles oneOf with a single ref 1`] = `
4350
"from typing import TypedDict
4451
MessageSingle = A"

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

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -185,6 +185,42 @@ describe('generate-python-dict', () => {
185185
expect(content).toMatchSnapshot();
186186
});
187187

188+
it('handles objects with additional properties', () => {
189+
const doc: OpenAPI.Document = {
190+
openapi: '3.1.0',
191+
info: { title: 't', version: '1' },
192+
paths: {},
193+
components: {
194+
schemas: {
195+
Config: {
196+
type: 'object',
197+
properties: {
198+
logit_bias: {
199+
type: 'object',
200+
nullable: true,
201+
additionalProperties: { type: 'integer' }
202+
},
203+
metadata: {
204+
type: 'object',
205+
nullable: true,
206+
additionalProperties: { type: 'string' }
207+
}
208+
}
209+
}
210+
}
211+
}
212+
};
213+
const schemas = extractSchemas(doc, null);
214+
const { definition, typingImports } = convertToTypedDict('Config', schemas.Config as OpenAPI.SchemaObject);
215+
const typingLine = `from typing import ${Array.from(typingImports).join(', ')}`;
216+
const content = [typingLine, '', definition, ''].filter(Boolean).join('\n');
217+
const tmp = path.join(__dirname, 'tmp_additional.py');
218+
fs.writeFileSync(tmp, content);
219+
child_process.execSync(`python3 -m py_compile ${tmp}`);
220+
fs.unlinkSync(tmp);
221+
expect(content).toMatchSnapshot();
222+
});
223+
188224
it('filters schemas by path prefixes', () => {
189225
const doc: OpenAPI.Document = {
190226
openapi: '3.1.0',

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

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,4 +90,22 @@ describe('convertSchema', () => {
9090
const { zodString } = convertSchema(schema);
9191
expect(zodString).toBe('z.object({\n data: z.object({\n id: z.string(),\n flag: z.boolean().optional()\n})\n})');
9292
});
93+
94+
it('converts objects with additional properties', () => {
95+
const schema = {
96+
type: 'object',
97+
properties: {
98+
logit_bias: {
99+
type: 'object',
100+
additionalProperties: { type: 'integer' }
101+
},
102+
metadata: {
103+
type: 'object',
104+
additionalProperties: { type: 'string' }
105+
}
106+
}
107+
} as OpenAPI.SchemaObject;
108+
const { zodString } = convertSchema(schema);
109+
expect(zodString).toBe('z.object({\n logit_bias: z.record(z.number()).optional(),\n metadata: z.record(z.string()).optional()\n})');
110+
});
93111
});

0 commit comments

Comments
 (0)