Skip to content

Commit

Permalink
feat: added support for type negation with z.not() and .not() method …
Browse files Browse the repository at this point in the history
…in schema validation #2862
  • Loading branch information
Kumar06Lav committed Aug 14, 2024
1 parent 821d45b commit 61dc91e
Show file tree
Hide file tree
Showing 7 changed files with 446 additions and 2 deletions.
22 changes: 22 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,7 @@
- [Dates](#dates)
- [Zod enums](#zod-enums)
- [Native enums](#native-enums)
- [Not](#not)
- [Optionals](#optionals)
- [Nullables](#nullables)
- [Objects](#objects)
Expand Down Expand Up @@ -1133,6 +1134,27 @@ You can access the underlying object with the `.enum` property:
FruitEnum.enum.Apple; // "apple"
```

## Not

You can use `z.not()` to create a schema that rejects a specific type. This wraps the schema in a `ZodNot` instance and returns the result.

```ts
const schema = z.not(z.string());

schema.parse(1234); // => passes, as 1234 is not a string
schema.parse("hello"); // => throws an error, as "hello" is a string
```

For convenience, you can also call the `.not()` method on an existing schema.

```ts
const schema = z.string().email().not();

schema.parse(1234); // => passes, as 1234 is not a string or a valid email
schema.parse("hello"); // => passes, as "hello" is not a valid email
schema.parse("[email protected]"); // => throws an error, as "[email protected]" is a valid email
```

## Optionals

You can make any schema optional with `z.optional()`. This wraps the schema in a `ZodOptional` instance and returns the result.
Expand Down
22 changes: 22 additions & 0 deletions README_ZH.md
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@
- [.min/.max/.length](#minmaxlength)
- [Unions](#unions)
- [Discriminated unions](#discriminated-unions)
- [Not](#not)
- [Optionals](#optionals)
- [Nullables](#nullables)
- [Enums](#enums)
Expand Down Expand Up @@ -941,6 +942,27 @@ const B = z.discriminatedUnion("status", [
const AB = z.discriminatedUnion("status", [...A.options, ...B.options]);
```

## Not

你可以用 `z.not()` 来创建一个拒绝特定类型的模式。这个方法会将模式包装在 `ZodNot` 实例中并返回结果。

```ts
const schema = z.not(z.string());

schema.parse(1234); // => 通过,因为 1234 不是字符串
schema.parse("hello"); // => 抛出错误,因为 "hello" 是字符串
```

为了方便,你也可以在现有的模式上直接调用 `.not()` 方法。

```ts
const schema = z.string().email().not();

schema.parse(1234); // => 通过,因为 1234 不是字符串或有效的邮箱地址
schema.parse("hello"); // => 通过,因为 "hello" 不是有效的邮箱地址
schema.parse("[email protected]"); // => 抛出错误,因为 "[email protected]" 是一个有效的邮箱地址
```

## Optionals

你可以用`z.optional()`使任何模式成为可选:
Expand Down
27 changes: 25 additions & 2 deletions deno/lib/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,7 @@
- [Dates](#dates)
- [Zod enums](#zod-enums)
- [Native enums](#native-enums)
- [Not](#not)
- [Optionals](#optionals)
- [Nullables](#nullables)
- [Objects](#objects)
Expand Down Expand Up @@ -252,7 +253,7 @@ Sponsorship at any level is appreciated and encouraged. If you built a paid prod
<td align="center">
<p></p>
<p>
<a href="https://speakeasyapi.dev/?utm_source=zod+docs">
<a href="https://speakeasy.com/?utm_source=zod+docs">
<picture height="40px">
<source media="(prefers-color-scheme: dark)" srcset="https://github.com/colinhacks/zod/assets/3084745/b1d86601-c7fb-483c-9927-5dc24ce8b737">
<img alt="speakeasy" height="40px" src="https://github.com/colinhacks/zod/assets/3084745/647524a4-22bb-4199-be70-404207a5a2b5">
Expand All @@ -261,7 +262,7 @@ Sponsorship at any level is appreciated and encouraged. If you built a paid prod
<br />
SDKs & Terraform providers for your API
<br/>
<a href="https://speakeasyapi.dev/?utm_source=zod+docs" style="text-decoration:none;">speakeasyapi.dev</a>
<a href="https://speakeasy.com/?utm_source=zod+docs" style="text-decoration:none;">speakeasy.com</a>
</p>
<p></p>
</td>
Expand Down Expand Up @@ -490,6 +491,7 @@ There are a growing number of tools that are built atop or support Zod natively!
- [`zod-prisma`](https://github.com/CarterGrimmeisen/zod-prisma): Generate Zod schemas from your Prisma schema.
- [`Supervillain`](https://github.com/Southclaws/supervillain): Generate Zod schemas from your Go structs.
- [`prisma-zod-generator`](https://github.com/omar-dulaimi/prisma-zod-generator): Emit Zod schemas from your Prisma schema.
- [`drizzle-zod`](https://orm.drizzle.team/docs/zod): Emit Zod schemas from your Drizzle schema.
- [`prisma-trpc-generator`](https://github.com/omar-dulaimi/prisma-trpc-generator): Emit fully implemented tRPC routers and their validation schemas using Zod.
- [`zod-prisma-types`](https://github.com/chrishoermann/zod-prisma-types) Create Zod types from your Prisma models.
- [`quicktype`](https://app.quicktype.io/): Convert JSON objects and JSON schemas into Zod schemas.
Expand Down Expand Up @@ -1132,6 +1134,27 @@ You can access the underlying object with the `.enum` property:
FruitEnum.enum.Apple; // "apple"
```

## Not

You can use `z.not()` to create a schema that rejects a specific type. This wraps the schema in a `ZodNot` instance and returns the result.

```ts
const schema = z.not(z.string());

schema.parse(1234); // => passes, as 1234 is not a string
schema.parse("hello"); // => throws an error, as "hello" is a string
```

For convenience, you can also call the `.not()` method on an existing schema.

```ts
const schema = z.string().email().not();

schema.parse(1234); // => passes, as 1234 is not a string or a valid email
schema.parse("hello"); // => passes, as "hello" is not a valid email
schema.parse("[email protected]"); // => throws an error, as "[email protected]" is a valid email
```

## Optionals

You can make any schema optional with `z.optional()`. This wraps the schema in a `ZodOptional` instance and returns the result.
Expand Down
126 changes: 126 additions & 0 deletions deno/lib/__tests__/not.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
// @ts-ignore TS6133
import { expect } from "https://deno.land/x/[email protected]/mod.ts";
const test = Deno.test;

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

/** Note
* Since z.never() allows no values, z.not(z.never()) should allow all values, including undefined. Hence, there are no failing test cases for notNever.
* There are no passing validation cases for notAny, as it should reject everything.
*/

// Set 1
const str = z.string().not();
const email = z.string().email().not();
const num = z.number().not();
const arr = z.array(z.string()).not();
const rec = z.record(z.string()).not();
const obj = z.object({ username: z.string() }).not();

// Set 2
const bigInt = z.bigint().not();
const bool = z.boolean().not();
const date = z.date().not();
const sym = z.symbol().not();
const undef = z.undefined().not();
const nul = z.null().not();
const voidSchema = z.void().not();
const anySchema = z.any().not();
const unknownSchema = z.unknown().not();
const neverSchema = z.never().not();

// Set 3
const notString = z.not(z.string());
const notNumber = z.not(z.number());
const notStringArray = z.not(z.array(z.string()));
const notRecord = z.not(z.record(z.string()));
const notObject = z.not(z.object({ username: z.string() }));

// Set 4
const notBigInt = z.not(z.bigint());
const notBoolean = z.not(z.boolean());
const notDate = z.not(z.date());
const notSymbol = z.not(z.symbol());
const notUndefined = z.not(z.undefined());
const notNull = z.not(z.null());
const notVoid = z.not(z.void());
const notAny = z.not(z.any());
const notUnknown = z.not(z.unknown());
const notNever = z.not(z.never());

test("passing validations", () => {
// Set 1
str.parse(1234);
email.parse(1234);
email.parse("abcd");
num.parse("1234");
arr.parse([1234]);
rec.parse({ key: 1234 });
obj.parse({ username: 1234 });

// Set 2
bigInt.parse(10);
bool.parse(1234);
date.parse("not a date");
sym.parse("1234");
undef.parse(null);
nul.parse(undefined);
voidSchema.parse(null);
neverSchema.parse(undefined); // `z.never()` allows no values, so `notNever` can accept any value.

// Set 3
notString.parse(1234);
notNumber.parse("1234");
notStringArray.parse([1234]);
notRecord.parse({ key: 1234 });
notObject.parse({ username: 1234 });

// Set 4
notBigInt.parse(10);
notBoolean.parse(1234);
notDate.parse("not a date");
notSymbol.parse("1234");
notUndefined.parse(null);
notNull.parse(undefined);
notVoid.parse(null);
notNever.parse(undefined); // `z.never()` allows no values, so `notNever` can accept any value.
});

test("failing validations", () => {
// Set 1
expect(() => str.parse("1234")).toThrow();
expect(() => email.parse("[email protected]")).toThrow();
expect(() => num.parse(1234)).toThrow();
expect(() => arr.parse(["1234"])).toThrow();
expect(() => rec.parse({ key: "value" })).toThrow();
expect(() => obj.parse({ username: "1234" })).toThrow();

// Set 2
expect(() => bigInt.parse(BigInt(10))).toThrow();
expect(() => bool.parse(true)).toThrow();
expect(() => date.parse(new Date())).toThrow();
expect(() => sym.parse(Symbol("symbol"))).toThrow();
expect(() => undef.parse(undefined)).toThrow();
expect(() => nul.parse(null)).toThrow();
expect(() => voidSchema.parse(undefined)).toThrow();
expect(() => anySchema.parse(undefined)).toThrow(); // `z.any()` allows any value, so `notAny` should fail for any input.
expect(() => unknownSchema.parse(undefined)).toThrow(); // Same as `z.any()`

// Set 3
expect(() => notString.parse("1234")).toThrow();
expect(() => notNumber.parse(1234)).toThrow();
expect(() => notStringArray.parse(["1234"])).toThrow();
expect(() => notRecord.parse({ key: "value" })).toThrow();
expect(() => notObject.parse({ username: "1234" })).toThrow();

// Set 4
expect(() => notBigInt.parse(BigInt(10))).toThrow();
expect(() => notBoolean.parse(true)).toThrow();
expect(() => notDate.parse(new Date())).toThrow();
expect(() => notSymbol.parse(Symbol("symbol"))).toThrow();
expect(() => notUndefined.parse(undefined)).toThrow();
expect(() => notNull.parse(null)).toThrow();
expect(() => notVoid.parse(undefined)).toThrow();
expect(() => notAny.parse(undefined)).toThrow(); // `z.any()` allows any value, so `notAny` should fail for any input.
expect(() => notUnknown.parse(undefined)).toThrow(); // Same as `z.any()`
});
63 changes: 63 additions & 0 deletions deno/lib/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -410,6 +410,7 @@ export abstract class ZodType<
this.nullable = this.nullable.bind(this);
this.nullish = this.nullish.bind(this);
this.array = this.array.bind(this);
this.not = this.not.bind(this);
this.promise = this.promise.bind(this);
this.or = this.or.bind(this);
this.and = this.and.bind(this);
Expand All @@ -436,6 +437,9 @@ export abstract class ZodType<
array(): ZodArray<this> {
return ZodArray.create(this, this._def);
}
not(): ZodNot<this> {
return ZodNot.create(this, this._def);
}
promise(): ZodPromise<this> {
return ZodPromise.create(this, this._def);
}
Expand Down Expand Up @@ -2295,6 +2299,62 @@ export class ZodArray<
};
}

export interface ZodNotDef<T extends ZodTypeAny = ZodTypeAny>
extends ZodTypeDef {
item: T;
typeName: ZodFirstPartyTypeKind.ZodNot;
}

export class ZodNot<T extends ZodTypeAny> extends ZodType<
T["_output"] | null,
ZodNotDef<T>,
T["_input"] | null
> {
_parse(input: ParseInput): ParseReturnType<this["_output"]> {
const { ctx } = this._processInputParams(input);

const schema = this._def.item;

if (!schema) {
addIssueToContext(ctx, {
code: ZodIssueCode.invalid_arguments,
argumentsError: ctx.data,
});

return INVALID;
}

const result = schema._parse(
new ParseInputLazyPath(ctx, ctx.data, ctx.path, 0)
);

if (!!result && String((result as any).status) == "valid") {
addIssueToContext(ctx, {
code: ZodIssueCode.custom,
message: "Invalid input",
});

return INVALID;
}

return OK(input.data);
}

static create = <T extends ZodTypeAny>(
schemas: T,
params?: RawCreateParams
): ZodNot<T> => {
if (!schemas) {
throw new Error("You must pass a schema to z.not( ... )");
}
return new ZodNot({
item: schemas,
typeName: ZodFirstPartyTypeKind.ZodNot,
...processCreateParams(params),
});
};
}

export type ZodNonEmptyArray<T extends ZodTypeAny> = ZodArray<T, "atleastone">;

/////////////////////////////////////////
Expand Down Expand Up @@ -5125,6 +5185,7 @@ export enum ZodFirstPartyTypeKind {
ZodUndefined = "ZodUndefined",
ZodNull = "ZodNull",
ZodAny = "ZodAny",
ZodNot = "ZodNot",
ZodUnknown = "ZodUnknown",
ZodNever = "ZodNever",
ZodVoid = "ZodVoid",
Expand Down Expand Up @@ -5216,6 +5277,7 @@ const unknownType = ZodUnknown.create;
const neverType = ZodNever.create;
const voidType = ZodVoid.create;
const arrayType = ZodArray.create;
const notType = ZodNot.create;
const objectType = ZodObject.create;
const strictObjectType = ZodObject.strictCreate;
const unionType = ZodUnion.create;
Expand Down Expand Up @@ -5274,6 +5336,7 @@ export {
nanType as nan,
nativeEnumType as nativeEnum,
neverType as never,
notType as not,
nullType as null,
nullableType as nullable,
numberType as number,
Expand Down
Loading

0 comments on commit 61dc91e

Please sign in to comment.