Skip to content

Commit eff961b

Browse files
authored
fix: generate Blob for $ref schemas with format: binary (#3030)
* fix: generate Blob for contentMediaType: application/octet-stream in model types * fix: update generateZodValidationSchemaDefinition to use 'test' for context spec
1 parent 14c60e6 commit eff961b

File tree

4 files changed

+142
-0
lines changed

4 files changed

+142
-0
lines changed
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
import { describe, expect, it } from 'vitest';
2+
3+
import type { ContextSpec, OpenApiSchemaObject } from '../types';
4+
import { getScalar } from './scalar';
5+
6+
const context = {
7+
output: {
8+
override: {
9+
useDates: false,
10+
},
11+
},
12+
} as ContextSpec;
13+
14+
describe('getScalar (contentMediaType: application/octet-stream)', () => {
15+
it('contentMediaType: application/octet-stream without formDataContext → Blob', () => {
16+
// Simulates $ref-based schema after upgrader converts format: binary →
17+
// contentMediaType: application/octet-stream (Swagger 2.0 / OAS 3.0 → 3.1)
18+
const schema: OpenApiSchemaObject = {
19+
type: 'string',
20+
contentMediaType: 'application/octet-stream',
21+
};
22+
23+
const result = getScalar({ item: schema, name: 'file', context });
24+
25+
expect(result.value).toBe('Blob');
26+
});
27+
28+
it('contentEncoding: base64 with contentMediaType: application/octet-stream → string', () => {
29+
const schema: OpenApiSchemaObject = {
30+
type: 'string',
31+
contentMediaType: 'application/octet-stream',
32+
contentEncoding: 'base64',
33+
};
34+
35+
const result = getScalar({ item: schema, name: 'data', context });
36+
37+
expect(result.value).toBe('string');
38+
});
39+
40+
it('plain string without contentMediaType or format: binary → string', () => {
41+
const schema: OpenApiSchemaObject = {
42+
type: 'string',
43+
};
44+
45+
const result = getScalar({ item: schema, name: 'name', context });
46+
47+
expect(result.value).toBe('string');
48+
});
49+
50+
it('non-octet-stream contentMediaType without formDataContext → string', () => {
51+
// contentMediaType other than application/octet-stream is only handled
52+
// in the form-data context (formDataContext?.atPart)
53+
const schema: OpenApiSchemaObject = {
54+
type: 'string',
55+
contentMediaType: 'image/png',
56+
};
57+
58+
const result = getScalar({ item: schema, name: 'avatar', context });
59+
60+
expect(result.value).toBe('string');
61+
});
62+
});

packages/core/src/getters/scalar.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,8 @@ export function getScalar({
5050
const schemaConst = item.const as string | undefined;
5151
const schemaFormat = item.format as string | undefined;
5252
const schemaNullable = item.nullable as boolean | undefined;
53+
const schemaContentMediaType = item.contentMediaType as string | undefined;
54+
const schemaContentEncoding = item.contentEncoding as string | undefined;
5355

5456
const nullable =
5557
(isArray(schemaType) && schemaType.includes('null')) ||
@@ -172,6 +174,15 @@ export function getScalar({
172174
if (fileType) {
173175
value = fileType === 'binary' ? 'Blob' : 'Blob | string';
174176
}
177+
} else if (
178+
schemaContentMediaType === 'application/octet-stream' &&
179+
!schemaContentEncoding
180+
) {
181+
// The @scalar/openapi-parser upgrader converts format: binary to
182+
// contentMediaType: application/octet-stream when upgrading
183+
// Swagger 2.0 / OAS 3.0 → OAS 3.1. Treat it the same as
184+
// format: binary so $ref-based model types generate Blob.
185+
value = 'Blob';
175186
}
176187

177188
if (

packages/zod/src/index.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -508,6 +508,18 @@ export const generateZodValidationSchemaDefinition = (
508508
break;
509509
}
510510

511+
// The @scalar/openapi-parser upgrader converts format: binary to
512+
// contentMediaType: application/octet-stream when upgrading
513+
// Swagger 2.0 / OAS 3.0 → OAS 3.1. Treat it the same as
514+
// format: binary so $ref-based model types generate File validation.
515+
if (
516+
schema.contentMediaType === 'application/octet-stream' &&
517+
!schema.contentEncoding
518+
) {
519+
functions.push(['instanceof', 'File']);
520+
break;
521+
}
522+
511523
if (isZodV4) {
512524
if (
513525
![

packages/zod/src/zod.test.ts

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5154,3 +5154,60 @@ describe('zod split mode regressions', () => {
51545154
expect(parsed.zod).not.toContain('looseObject({');
51555155
});
51565156
});
5157+
5158+
describe('generateZodValidationSchemaDefinition (contentMediaType: application/octet-stream)', () => {
5159+
it('contentMediaType: application/octet-stream → instanceof File', () => {
5160+
const schema: OpenApiSchemaObject = {
5161+
type: 'string',
5162+
contentMediaType: 'application/octet-stream',
5163+
};
5164+
5165+
const result = generateZodValidationSchemaDefinition(
5166+
schema,
5167+
{
5168+
output: {
5169+
override: {
5170+
useDates: false,
5171+
},
5172+
},
5173+
} as ContextSpec,
5174+
'test',
5175+
true,
5176+
false,
5177+
{ required: true },
5178+
);
5179+
5180+
expect(result).toEqual({
5181+
functions: [['instanceof', 'File']],
5182+
consts: [],
5183+
});
5184+
});
5185+
5186+
it('contentEncoding: base64 with contentMediaType: application/octet-stream → string', () => {
5187+
const schema: OpenApiSchemaObject = {
5188+
type: 'string',
5189+
contentMediaType: 'application/octet-stream',
5190+
contentEncoding: 'base64',
5191+
};
5192+
5193+
const result = generateZodValidationSchemaDefinition(
5194+
schema,
5195+
{
5196+
output: {
5197+
override: {
5198+
useDates: false,
5199+
},
5200+
},
5201+
} as ContextSpec,
5202+
'test',
5203+
true,
5204+
false,
5205+
{ required: true },
5206+
);
5207+
5208+
expect(result).toEqual({
5209+
functions: [['string', undefined]],
5210+
consts: [],
5211+
});
5212+
});
5213+
});

0 commit comments

Comments
 (0)