Skip to content

Commit 1dc521e

Browse files
fix(zod): parser generation when content-type contains charset precision
1 parent 729c5b8 commit 1dc521e

2 files changed

Lines changed: 125 additions & 9 deletions

File tree

packages/zod/src/index.ts

Lines changed: 12 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1335,13 +1335,18 @@ const parseBodyAndResponse = ({
13351335
| OpenApiRequestBodyObject;
13361336

13371337
// Only handle JSON and form-data; other content types (e.g., application/octet-stream)
1338-
// are skipped - unclear if this is correct behavior for root-level binary/text bodies
1339-
const jsonMedia = resolvedRef.content?.['application/json'];
1340-
const formDataMedia = resolvedRef.content?.['multipart/form-data'];
1341-
const [contentType, mediaType] = jsonMedia
1342-
? (['application/json', jsonMedia] as const)
1343-
: formDataMedia
1344-
? (['multipart/form-data', formDataMedia] as const)
1338+
// are skipped - unclear if this is correct behavior for root-level binary/text bodies.
1339+
const contentEntries = Object.entries(resolvedRef.content ?? {});
1340+
const jsonContent = contentEntries.find(([contentType]) =>
1341+
contentType.startsWith('application/json'),
1342+
);
1343+
const formDataContent = contentEntries.find(([contentType]) =>
1344+
contentType.startsWith('multipart/form-data'),
1345+
);
1346+
const [contentType, mediaType] = jsonContent
1347+
? (['application/json', jsonContent[1]] as const)
1348+
: formDataContent
1349+
? (['multipart/form-data', formDataContent[1]] as const)
13451350
: [undefined, undefined];
13461351

13471352
const schema = mediaType?.schema;
@@ -1419,7 +1424,6 @@ const parseBodyAndResponse = ({
14191424
isArray: false,
14201425
};
14211426
};
1422-
14231427
const getSingleResponse = (
14241428
responses:
14251429
| Record<string, OpenApiResponseObject | OpenApiReferenceObject | undefined>
@@ -1431,7 +1435,6 @@ const getSingleResponse = (
14311435

14321436
return responses['200'] ?? responses['2XX'] ?? responses['2xx'];
14331437
};
1434-
14351438
/* eslint-enable @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-argument, @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-unsafe-call */
14361439

14371440
export const parseParameters = ({

packages/zod/src/zod.test.ts

Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6273,6 +6273,119 @@ describe('generateZod (content type handling - parity with res-req-types.test.ts
62736273
})
62746274
})
62756275
6276+
`);
6277+
});
6278+
6279+
it('content type with charset precision: comprehensive content type handling', async () => {
6280+
// Matches type gen test structure in res-req-types.test.ts
6281+
const schema = {
6282+
pathRoute: '/upload-form',
6283+
context: {
6284+
spec: {
6285+
paths: {
6286+
'/upload-form': {
6287+
post: {
6288+
operationId: 'uploadForm',
6289+
requestBody: {
6290+
required: true,
6291+
content: {
6292+
'multipart/form-data; charset=utf-8': {
6293+
schema: {
6294+
type: 'object',
6295+
properties: {
6296+
encBinary: { type: 'string' },
6297+
encText: { type: 'string' },
6298+
cmtBinary: {
6299+
type: 'string',
6300+
contentMediaType: 'image/png',
6301+
},
6302+
cmtText: {
6303+
type: 'string',
6304+
contentMediaType: 'application/xml',
6305+
},
6306+
encOverride: {
6307+
type: 'string',
6308+
contentMediaType: 'image/png',
6309+
},
6310+
formatBinary: { type: 'string', format: 'binary' },
6311+
base64Field: {
6312+
type: 'string',
6313+
contentMediaType: 'image/png',
6314+
contentEncoding: 'base64',
6315+
},
6316+
metadata: {
6317+
type: 'object',
6318+
properties: { name: { type: 'string' } },
6319+
},
6320+
},
6321+
required: [
6322+
'encBinary',
6323+
'encText',
6324+
'cmtBinary',
6325+
'cmtText',
6326+
'encOverride',
6327+
'formatBinary',
6328+
'base64Field',
6329+
'metadata',
6330+
],
6331+
},
6332+
encoding: {
6333+
encBinary: { contentType: 'image/png' },
6334+
encText: { contentType: 'text/plain' },
6335+
encOverride: { contentType: 'text/csv' },
6336+
metadata: { contentType: 'application/json' },
6337+
},
6338+
},
6339+
},
6340+
},
6341+
responses: {
6342+
'200': {
6343+
content: {
6344+
'application/json; charset=utf-8': {
6345+
schema: { type: 'string' },
6346+
},
6347+
},
6348+
},
6349+
},
6350+
},
6351+
},
6352+
},
6353+
},
6354+
output: { override: { zod: { generateEachHttpStatus: false } } },
6355+
},
6356+
} as unknown as GeneratorOptions;
6357+
const result = await generateZod(
6358+
{
6359+
pathRoute: '/upload-form',
6360+
verb: 'post',
6361+
operationName: 'uploadForm',
6362+
override: zodOverride,
6363+
} as unknown as Parameters<typeof generateZod>[0],
6364+
schema,
6365+
testOutput,
6366+
);
6367+
// encBinary: encoding image/png → File
6368+
// encText: encoding text/plain → File | string
6369+
// cmtBinary: contentMediaType image/png → File
6370+
// cmtText: contentMediaType application/xml → File | string
6371+
// encOverride: encoding text/csv overrides contentMediaType image/png → File | string
6372+
// formatBinary: format: binary → File (same as instanceof check)
6373+
// base64Field: contentEncoding base64 → stays string
6374+
// metadata: object → object schema
6375+
expect(result.implementation)
6376+
.toBe(`export const UploadFormBody = zod.object({
6377+
"encBinary": zod.instanceof(File),
6378+
"encText": zod.instanceof(File).or(zod.string()),
6379+
"cmtBinary": zod.instanceof(File),
6380+
"cmtText": zod.instanceof(File).or(zod.string()),
6381+
"encOverride": zod.instanceof(File).or(zod.string()),
6382+
"formatBinary": zod.instanceof(File),
6383+
"base64Field": zod.string(),
6384+
"metadata": zod.object({
6385+
"name": zod.string().optional()
6386+
})
6387+
})
6388+
62766389
`);
62776390
});
62786391
});

0 commit comments

Comments
 (0)