Skip to content

Commit 3d456a6

Browse files
committed
feat(schema): ✨ Enforce PascalCase for TypedDicts and camelCase for Zod
1 parent 6c99edc commit 3d456a6

8 files changed

Lines changed: 54 additions & 33 deletions

File tree

src/index.ts

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import { isValidOpenapiSchema } from './utils/validation.js';
1212
import { extractSchemas } from './utils/extract-schemas.js';
1313
import { convertSchema } from './utils/json-schema-to-zod.js';
1414
import { convertToTypedDict } from './utils/json-schema-to-typed-dict.js';
15+
import { toCamelCase, toPascalCase } from './utils/case.js';
1516
import { sortSchemas } from './utils/sort-schemas.js';
1617
import Handlebars from 'handlebars';
1718

@@ -95,12 +96,12 @@ program
9596
const template = Handlebars.compile(templateSource);
9697

9798
const ordered = sortSchemas(schemas);
98-
const schemaData = ordered.map((name) => {
99-
const schema = schemas[name]!;
99+
const schemaData = ordered.map((rawName) => {
100+
const schema = schemas[rawName]!;
100101
const { zodString } = convertSchema(schema);
101102
const desc = schema.description as string | undefined;
102103
return {
103-
schemaName: name,
104+
schemaName: toCamelCase(rawName),
104105
zodString,
105106
description: desc ? desc.replace(/\n/g, ' ') : undefined
106107
};
@@ -148,10 +149,10 @@ program
148149

149150
const typingImports = new Set<string>();
150151
const ordered = sortSchemas(schemas);
151-
const definitions = ordered.map((name) => {
152-
const res = convertToTypedDict(name, schemas[name]!);
152+
const definitions = ordered.map((rawName) => {
153+
const res = convertToTypedDict(rawName, schemas[rawName]!);
153154
res.typingImports.forEach((i) => typingImports.add(i));
154-
return { name, definition: res.definition };
155+
return { name: toPascalCase(rawName), definition: res.definition };
155156
});
156157

157158
const content = template({

src/utils/case.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
export function toPascalCase(str: string): string {
2+
return str
3+
.replace(/[^a-zA-Z0-9]+/g, ' ')
4+
.split(' ')
5+
.filter(Boolean)
6+
.map((p) => p.charAt(0).toUpperCase() + p.slice(1))
7+
.join('');
8+
}
9+
10+
export function toCamelCase(str: string): string {
11+
const pascal = toPascalCase(str);
12+
return pascal.charAt(0).toLowerCase() + pascal.slice(1);
13+
}

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

Lines changed: 7 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,27 +1,21 @@
11
import type { OpenAPIV3_1 as OpenAPI } from 'openapi-types';
2+
import { toPascalCase } from './case.js';
23

34
export interface TypedDictResult {
45
definition: string;
56
typingImports: Set<string>;
67
}
78

89
export function convertToTypedDict(name: string, schema: OpenAPI.SchemaObject | OpenAPI.ReferenceObject): TypedDictResult {
10+
name = toPascalCase(name);
911
const typingImports = new Set<string>(['TypedDict']);
1012
const extraDefs: string[] = [];
1113

12-
function toPascal(str: string): string {
13-
return str
14-
.replace(/[^a-zA-Z0-9]+/g, ' ')
15-
.split(' ')
16-
.filter(Boolean)
17-
.map((p) => p.charAt(0).toUpperCase() + p.slice(1))
18-
.join('');
19-
}
20-
2114
function flatten(s: OpenAPI.SchemaObject | OpenAPI.ReferenceObject): { bases: string[]; schema: OpenAPI.SchemaObject | null } {
2215
if ('$ref' in s) {
2316
const match = s.$ref.match(/^#\/components\/schemas\/(.+)$/);
24-
return { bases: [match?.[1] ?? s.$ref], schema: null };
17+
const refName = match?.[1] ?? s.$ref;
18+
return { bases: [toPascalCase(refName)], schema: null };
2519
}
2620

2721
if ('allOf' in s && Array.isArray(s.allOf)) {
@@ -51,7 +45,7 @@ export function convertToTypedDict(name: string, schema: OpenAPI.SchemaObject |
5145
const fields: string[] = [];
5246
const attrLines: string[] = [];
5347
for (const [key, value] of Object.entries(props)) {
54-
const typeStr = toType(value as any, `${className}${toPascal(key)}`);
48+
const typeStr = toType(value as any, `${className}${toPascalCase(key)}`);
5549
const desc = (value as any).description;
5650
if (required.has(key)) {
5751
typingImports.add('Required');
@@ -95,7 +89,7 @@ export function convertToTypedDict(name: string, schema: OpenAPI.SchemaObject |
9589
if ('$ref' in s) {
9690
const match = s.$ref.match(/^#\/components\/schemas\/(.+)$/);
9791
const refName = match?.[1] ?? s.$ref;
98-
return `${refName}`;
92+
return `${toPascalCase(refName)}`;
9993
}
10094

10195
if ('oneOf' in s && Array.isArray(s.oneOf)) {
@@ -169,7 +163,7 @@ export function convertToTypedDict(name: string, schema: OpenAPI.SchemaObject |
169163
const fields: string[] = [];
170164
const attrLines: string[] = [];
171165
for (const [key, value] of Object.entries(props)) {
172-
const typeStr = toType(value as any, `${name}${toPascal(key)}`);
166+
const typeStr = toType(value as any, `${name}${toPascalCase(key)}`);
173167
const desc = (value as any).description;
174168
if (required.has(key)) {
175169
typingImports.add('Required');

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

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import type { OpenAPIV3_1 as OpenAPI } from 'openapi-types';
2+
import { toCamelCase } from './case.js';
23

34
export interface ZodResult {
45
zodString: string;
@@ -11,7 +12,8 @@ export function convertSchema(schema: OpenAPI.SchemaObject | OpenAPI.ReferenceOb
1112
function walk(s: OpenAPI.SchemaObject | OpenAPI.ReferenceObject): string {
1213
if ('$ref' in s) {
1314
const match = s.$ref.match(/^#\/components\/schemas\/(.+)$/);
14-
const name = match?.[1] ?? s.$ref;
15+
const raw = match?.[1] ?? s.$ref;
16+
const name = toCamelCase(raw);
1517
imports.add(name);
1618
return name;
1719
}

tests/__snapshots__/generate-zod.spec.ts.snap

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ exports[`generate-zod > generates a simple object schema 1`] = `
55
66
import { z } from 'zod/v4';
77
8-
export const User = z.object({
8+
export const user = z.object({
99
id: z.string()
1010
});
1111
"
@@ -16,6 +16,6 @@ exports[`generate-zod > generates enums and nested arrays 1`] = `
1616
1717
import { z } from 'zod/v4';
1818
19-
export const Wrapper = z.array(Status);
19+
export const wrapper = z.array(status);
2020
"
2121
`;

tests/case.spec.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
import { describe, it, expect } from 'vitest';
2+
import { toPascalCase, toCamelCase } from '../src/utils/case';
3+
4+
describe('case utils', () => {
5+
it('converts to PascalCase', () => {
6+
expect(toPascalCase('my_schema-name')).toBe('MySchemaName');
7+
});
8+
it('converts to camelCase', () => {
9+
expect(toCamelCase('my_schema-name')).toBe('mySchemaName');
10+
});
11+
});

tests/generate-zod.spec.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ describe('generate-zod', () => {
2121
const schemas = extractSchemas(doc, null);
2222
const { zodString } = convertSchema(schemas.User as OpenAPI.SchemaObject);
2323
const content = schemaTemplate({
24-
schemas: [{ schemaName: 'User', zodString }]
24+
schemas: [{ schemaName: 'user', zodString }]
2525
});
2626
const result = ts.transpileModule(content, { compilerOptions: { module: ts.ModuleKind.ESNext } });
2727
expect(result.diagnostics?.length).toBe(0);
@@ -43,7 +43,7 @@ describe('generate-zod', () => {
4343
const schemas = extractSchemas(doc, null);
4444
const { zodString } = convertSchema(schemas.Wrapper as OpenAPI.SchemaObject);
4545
const content = schemaTemplate({
46-
schemas: [{ schemaName: 'Wrapper', zodString }]
46+
schemas: [{ schemaName: 'wrapper', zodString }]
4747
});
4848
const result = ts.transpileModule(content, { compilerOptions: { module: ts.ModuleKind.ESNext } });
4949
expect(result.diagnostics?.length).toBe(0);

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

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,8 @@ describe('convertSchema', () => {
1717
type: 'array',
1818
items: { $ref: '#/components/schemas/User' }
1919
} as OpenAPI.SchemaObject);
20-
expect(zodString).toBe('z.array(User)');
21-
expect(imports.has('User')).toBe(true);
20+
expect(zodString).toBe('z.array(user)');
21+
expect(imports.has('user')).toBe(true);
2222
});
2323

2424
it('converts objects with optional fields', () => {
@@ -49,27 +49,27 @@ describe('convertSchema', () => {
4949
allOf: [{ $ref: '#/components/schemas/Base' }, { type: 'object', properties: { extra: { type: 'string' } }, required: ['extra'] }]
5050
} as OpenAPI.SchemaObject;
5151
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);
52+
expect(zodString).toBe('z.intersection(base, z.object({\n extra: z.string()\n}))');
53+
expect(imports.has('base')).toBe(true);
5454
});
5555

5656
it('handles oneOf with refs', () => {
5757
const schema = {
5858
oneOf: [{ $ref: '#/components/schemas/A' }, { $ref: '#/components/schemas/B' }]
5959
} as OpenAPI.SchemaObject;
6060
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);
61+
expect(zodString).toBe('z.union([a, b])');
62+
expect(imports.has('a')).toBe(true);
63+
expect(imports.has('b')).toBe(true);
6464
});
6565

6666
it('handles oneOf with a single ref', () => {
6767
const schema = {
6868
oneOf: [{ $ref: '#/components/schemas/A' }]
6969
} as OpenAPI.SchemaObject;
7070
const { zodString, imports } = convertSchema(schema);
71-
expect(zodString).toBe('A');
72-
expect(imports.has('A')).toBe(true);
71+
expect(zodString).toBe('a');
72+
expect(imports.has('a')).toBe(true);
7373
});
7474

7575
it('converts inline object properties', () => {

0 commit comments

Comments
 (0)