Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Added file type #3866

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 29 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,7 @@
- [Cyclical objects](#cyclical-objects)
- [Promises](#promises)
- [Instanceof](#instanceof)
- [Files](#files)
- [Functions](#functions)
- [Preprocess](#preprocess)
- [Custom schemas](#custom-schemas)
Expand Down Expand Up @@ -1925,6 +1926,34 @@ TestSchema.parse(new Test()); // passes
TestSchema.parse(blob); // throws
```

## Files

This basic implementation validates the input type. As usual, you can set custom error messages.

```ts
const imageFile = z.file({
required_error: "file is required",
invalid_type_error: "This object must be a file",
});
```

You can validate the file's minimum and maximum size.

```ts
const imageFile = z.file().size({ min: 100000, max: 200000 });
```

You check again min and max size of the file.

```ts
// Validate MIME type for any image
const imageFileAny = z.file().mimeType("image/*");

// Validate specific MIME types
const imageFileJpegPng = z.file().mimeType("image/jpeg,image/png");
const imageFileJpegPngArray = z.file().mimeType(["image/jpeg", "image/png"]);
```

## Functions

Zod also lets you define "function schemas". This makes it easy to validate the inputs and outputs of a function without intermixing your validation code and "business logic".
Expand Down
29 changes: 29 additions & 0 deletions deno/lib/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,7 @@
- [Cyclical objects](#cyclical-objects)
- [Promises](#promises)
- [Instanceof](#instanceof)
- [Files](#files)
- [Functions](#functions)
- [Preprocess](#preprocess)
- [Custom schemas](#custom-schemas)
Expand Down Expand Up @@ -1925,6 +1926,34 @@ TestSchema.parse(new Test()); // passes
TestSchema.parse(blob); // throws
```

## Files

This basic implementation validates the input type. As usual, you can set custom error messages.

```ts
const imageFile = z.file({
required_error: "file is required",
invalid_type_error: "This object must be a file",
});
```

You can validate the file's minimum and maximum size.

```ts
const imageFile = z.file().size({ min: 100000, max: 200000 });
```

You check again min and max size of the file.

```ts
// Validate MIME type for any image
const imageFileAny = z.file().mimeType("image/*");

// Validate specific MIME types
const imageFileJpegPng = z.file().mimeType("image/jpeg,image/png");
const imageFileJpegPngArray = z.file().mimeType(["image/jpeg", "image/png"]);
```

## Functions

Zod also lets you define "function schemas". This makes it easy to validate the inputs and outputs of a function without intermixing your validation code and "business logic".
Expand Down
49 changes: 49 additions & 0 deletions deno/lib/__tests__/file.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
// @ts-ignore TS6133
import { expect } from "https://deno.land/x/[email protected]/mod.ts";
const test = Deno.test;

import * as z from "../index.ts";

class MockFile {
static create(size?: number, name?: string, mimeType?: string) {
name = name || "mock.txt";
size = size || 1024;
mimeType = mimeType || "plain/txt";

function range(count: number) {
let output = "";
for (let i = 0; i < count; i++) {
output += "a";
}
return output;
}

return new File([range(size)], name, { type: mimeType });
}
}

const basicSchema = z.file().size({ max: 50000, min: 1024 });
const imageSchema = z
.file()
.size({ min: 20000 })
.mimeType("image/*,image/jpeg,image/png", "Invalide mime type");

test("passing validations", () => {
expect(basicSchema.parse(MockFile.create(1024))).toBeInstanceOf(File);
expect(basicSchema.parse(MockFile.create(50000))).toBeInstanceOf(File);
expect(basicSchema.parse(MockFile.create(2014))).toBeInstanceOf(File);
expect(
imageSchema.parse(MockFile.create(20000, "mock.jpeg", "image/jpeg"))
).toBeInstanceOf(File);
});

test("failing validations", () => {
expect(() => basicSchema.parse(MockFile.create(1023))).toThrow();
expect(() => basicSchema.parse(MockFile.create(50001))).toThrow();
expect(() =>
imageSchema.parse(MockFile.create(20000, "mock.jpeg", "video/mp4"))
).toThrow();
expect(() => basicSchema.parse(undefined)).toThrow();
expect(() => basicSchema.parse({})).toThrow();
expect(() => basicSchema.parse([])).toThrow();
});
2 changes: 2 additions & 0 deletions deno/lib/__tests__/firstparty.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,8 @@ test("first party switch", () => {
break;
case z.ZodFirstPartyTypeKind.ZodSet:
break;
case z.ZodFirstPartyTypeKind.ZodFile:
break;
case z.ZodFirstPartyTypeKind.ZodFunction:
break;
case z.ZodFirstPartyTypeKind.ZodLazy:
Expand Down
4 changes: 4 additions & 0 deletions deno/lib/helpers/util.ts
Original file line number Diff line number Diff line change
Expand Up @@ -148,6 +148,7 @@ export const ZodParsedType = util.arrayToEnum([
"date",
"bigint",
"symbol",
"file",
"function",
"undefined",
"null",
Expand Down Expand Up @@ -189,6 +190,9 @@ export const getParsedType = (data: any): ZodParsedType => {
return ZodParsedType.symbol;

case "object":
if (data instanceof File) {
return ZodParsedType.file;
}
if (Array.isArray(data)) {
return ZodParsedType.array;
}
Expand Down
132 changes: 132 additions & 0 deletions deno/lib/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3847,6 +3847,134 @@ export class ZodSet<Value extends ZodTypeAny = ZodTypeAny> extends ZodType<
};
}

///////////////////////////////////////////
///////////////////////////////////////////
////////// //////////
////////// ZodFile //////////
////////// //////////
///////////////////////////////////////////
///////////////////////////////////////////
type SizeRange = { min?: number; max?: number };

export type ZodFileCheck =
| { kind: "size"; value: SizeRange; message?: string }
| { kind: "mime_type"; value: string | string[]; message?: string };

export interface ZodFileDef extends ZodTypeDef {
checks: ZodFileCheck[];
typeName: ZodFirstPartyTypeKind.ZodFile;
}

export class ZodFile extends ZodType<File, ZodFileDef, File> {
size(range: SizeRange, message?: errorUtil.ErrMessage) {
return this._addCheck({
kind: "size",
value: range,
message: errorUtil.toString(message),
});
}

mimeType(type: string | string[], message?: errorUtil.ErrMessage) {
return this._addCheck({
kind: "mime_type",
value: type,
message: errorUtil.toString(message),
});
}

_parse(input: ParseInput): ParseReturnType<this["_output"]> {
const parsedType = this._getType(input);

if (parsedType !== ZodParsedType.file) {
const ctx = this._getOrReturnCtx(input);
addIssueToContext(ctx, {
code: ZodIssueCode.invalid_type,
expected: ZodParsedType.file,
received: ctx.parsedType,
});
return INVALID;
}

const file = input.data as File;

const status = new ParseStatus();

for (const check of this._def.checks) {
const ctx = this._getOrReturnCtx(input);

if (
check.kind === "size" &&
check.value.min &&
check.value.min > file.size
) {
addIssueToContext(ctx, {
code: ZodIssueCode.too_small,
minimum: check.value.min,
type: ZodParsedType.number,
inclusive: true,
exact: false,
message: check.message,
});

status.dirty();
} else if (
check.kind === "size" &&
check.value.max &&
check.value.max < file.size
) {
addIssueToContext(ctx, {
code: ZodIssueCode.too_big,
maximum: check.value.max,
type: ZodParsedType.number,
inclusive: true,
exact: false,
message: check.message,
});

status.dirty();
} else if (check.kind === "mime_type") {
const mimeTypes = Array.isArray(check.value)
? check.value
: check.value.split(",").map((s) => s.trim());

const state = mimeTypes.reduce((state, currentMimeType) => {
const regex = new RegExp(currentMimeType, "gm");

return state || regex.test(file.type);
}, false);

if (!state) {
addIssueToContext(ctx, {
code: ZodIssueCode.invalid_type,
expected: ZodParsedType.object,
received: ctx.parsedType,
message: check.message,
});

status.dirty();
}
}
}

return { status: status.value, value: input.data };
}

_addCheck(check: ZodFileCheck) {
return new ZodFile({
...this._def,
checks: [...this._def.checks, check],
});
}

static create = (params?: RawCreateParams): ZodFile => {
return new ZodFile({
typeName: ZodFirstPartyTypeKind.ZodFile,
checks: [],
...processCreateParams(params),
});
};
}

///////////////////////////////////////////
///////////////////////////////////////////
////////// //////////
Expand Down Expand Up @@ -5117,6 +5245,7 @@ export const late = {
};

export enum ZodFirstPartyTypeKind {
ZodFile = "ZodFile",
ZodString = "ZodString",
ZodNumber = "ZodNumber",
ZodNaN = "ZodNaN",
Expand Down Expand Up @@ -5176,6 +5305,7 @@ export type ZodFirstPartySchemaTypes =
| ZodRecord<any, any>
| ZodMap<any>
| ZodSet<any>
| ZodFile
| ZodFunction<any, any>
| ZodLazy<any>
| ZodLiteral<any>
Expand Down Expand Up @@ -5227,6 +5357,7 @@ const tupleType = ZodTuple.create;
const recordType = ZodRecord.create;
const mapType = ZodMap.create;
const setType = ZodSet.create;
const fileType = ZodFile.create;
const functionType = ZodFunction.create;
const lazyType = ZodLazy.create;
const literalType = ZodLiteral.create;
Expand Down Expand Up @@ -5267,6 +5398,7 @@ export {
discriminatedUnionType as discriminatedUnion,
effectsType as effect,
enumType as enum,
fileType as file,
functionType as function,
instanceOfType as instanceof,
intersectionType as intersection,
Expand Down
48 changes: 48 additions & 0 deletions src/__tests__/file.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
// @ts-ignore TS6133
import { expect, test } from "@jest/globals";

import * as z from "../index";

class MockFile {
static create(size?: number, name?: string, mimeType?: string) {
name = name || "mock.txt";
size = size || 1024;
mimeType = mimeType || "plain/txt";

function range(count: number) {
let output = "";
for (let i = 0; i < count; i++) {
output += "a";
}
return output;
}

return new File([range(size)], name, { type: mimeType });
}
}

const basicSchema = z.file().size({ max: 50000, min: 1024 });
const imageSchema = z
.file()
.size({ min: 20000 })
.mimeType("image/*,image/jpeg,image/png", "Invalide mime type");

test("passing validations", () => {
expect(basicSchema.parse(MockFile.create(1024))).toBeInstanceOf(File);
expect(basicSchema.parse(MockFile.create(50000))).toBeInstanceOf(File);
expect(basicSchema.parse(MockFile.create(2014))).toBeInstanceOf(File);
expect(
imageSchema.parse(MockFile.create(20000, "mock.jpeg", "image/jpeg"))
).toBeInstanceOf(File);
});

test("failing validations", () => {
expect(() => basicSchema.parse(MockFile.create(1023))).toThrow();
expect(() => basicSchema.parse(MockFile.create(50001))).toThrow();
expect(() =>
imageSchema.parse(MockFile.create(20000, "mock.jpeg", "video/mp4"))
).toThrow();
expect(() => basicSchema.parse(undefined)).toThrow();
expect(() => basicSchema.parse({})).toThrow();
expect(() => basicSchema.parse([])).toThrow();
});
2 changes: 2 additions & 0 deletions src/__tests__/firstparty.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,8 @@ test("first party switch", () => {
break;
case z.ZodFirstPartyTypeKind.ZodSet:
break;
case z.ZodFirstPartyTypeKind.ZodFile:
break;
case z.ZodFirstPartyTypeKind.ZodFunction:
break;
case z.ZodFirstPartyTypeKind.ZodLazy:
Expand Down
Loading