diff --git a/.changeset/dirty-buckets-roll.md b/.changeset/dirty-buckets-roll.md
new file mode 100644
index 00000000..dab81927
--- /dev/null
+++ b/.changeset/dirty-buckets-roll.md
@@ -0,0 +1,5 @@
+---
+'@withease/zod': patch
+---
+
+Increased package size limit
diff --git a/.changeset/sharp-trees-obey.md b/.changeset/sharp-trees-obey.md
new file mode 100644
index 00000000..85fef461
--- /dev/null
+++ b/.changeset/sharp-trees-obey.md
@@ -0,0 +1,5 @@
+---
+'@withease/zod': major
+---
+
+Initial release: Zod package adapter package
diff --git a/.gitignore b/.gitignore
index 587668d1..c7a37258 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,6 +1,7 @@
dist
node_modules
.DS_Store
+.idea
*.log
# Playwright
diff --git a/apps/website/docs/.vitepress/config.mjs b/apps/website/docs/.vitepress/config.mjs
index b847f05a..741d5cf0 100644
--- a/apps/website/docs/.vitepress/config.mjs
+++ b/apps/website/docs/.vitepress/config.mjs
@@ -49,6 +49,7 @@ export default defineConfig({
{ text: 'web-api', link: '/web-api/' },
{ text: 'factories', link: '/factories/' },
{ text: 'contracts', link: '/contracts/' },
+ { text: 'zod', link: '/zod/' },
],
},
{ text: 'Magazine', link: '/magazine/' },
@@ -140,6 +141,16 @@ export default defineConfig({
},
{ text: 'APIs', link: '/contracts/api' },
]),
+ ...createSidebar('zod', [
+ { text: 'Get Started', link: '/zod/' },
+ { text: 'Compatibility', link: '/zod/compatibility' },
+ {
+ text: "API",
+ items: [
+ { text: "zodContract", link: '/zod/api/contract/' }
+ ]
+ }
+ ]),
'/magazine/': [
{
text: 'Architecture',
diff --git a/apps/website/docs/zod/api/contract/index.md b/apps/website/docs/zod/api/contract/index.md
new file mode 100644
index 00000000..49ed6e2a
--- /dev/null
+++ b/apps/website/docs/zod/api/contract/index.md
@@ -0,0 +1,54 @@
+# zodContract
+
+Adapter, initially created for [_Contracts_](/protocols/contract) that allows you to introduce data validation of the application.
+
+::: warning
+Async schemas has not supported yet - related with [_Contracts protocol_](/protocols/contract) returned types:\
+Async schemas validation return Promise that can't be used with current [_Contracts protocol_](/protocols/contract)
+:::
+
+## Usage as a _Contract_
+
+`@withease/zod` is an adapter based on `@withease/contract` (used only [_Contract_ protocol](/protocols/contract) type) for full compatibility with Effector's ecosystem without additional interop. Just wrap your zod schema into adapter and use as usual [_Contract_](/protocols/contract).
+
+### Farfetched
+
+[Farfetched](https://ff.effector.dev) is the advanced data fetching tool for web applications based of Effector. It suggests to ensure that data received from the server is conforms desired [_Contract_](/protocols/contract).
+
+```ts
+import { createQuery } from '@farfetched/core';
+import { z } from 'zod';
+import { zodContract } from '@withease/zod';
+
+const CharacterSchema = z.object({
+ id: z.string(),
+ name: z.string(),
+ status: StatusSchema,
+ species: z.string(),
+ type: z.string(),
+ gender: GenderSchema,
+ origin: z.object({ name: z.string(), url: z.string() }),
+ location: z.object({ name: z.string(), url: z.string() }),
+ image: z.string(),
+ episode: z.array(z.string()),
+});
+
+const characterQuery = createQuery({
+ effect: createEffect(async (config: { id: number }) => {
+ const response = await fetch(
+ `https://rickandmortyapi.com/api/character/${config.id}`
+ );
+ return response.json();
+ }),
+ // after receiving data from the server
+ // check if it is conforms the Contract to ensure
+ // API does not return something unexpected
+ contract: zodContract(CharacterSchema),
+});
+```
+
+### Integration with other libraries
+
+Since _zodContract_ (`@withease/zod`) is compatible [_Contract_](/protocols/contract) protocol it can be used with any library that supports it.
+
+The full list of libraries that support _Contract_ protocol can be found [here](/protocols/contract).
diff --git a/apps/website/docs/zod/compatibility.md b/apps/website/docs/zod/compatibility.md
new file mode 100644
index 00000000..c33886c2
--- /dev/null
+++ b/apps/website/docs/zod/compatibility.md
@@ -0,0 +1,30 @@
+# Compatibility
+
+The best practice is to use a supported version of [Zod](https://zod.dev/). Preferably, opt for Zod@4. However, we recognize the need to support transitional versions to assist with migrations.
+
+## With zod@^4
+
+You can use standard zod import.
+
+```ts
+import { z } from 'zod';
+import { zodContract } from '@withease/zod';
+```
+
+## With zod@^3.25.0
+
+You still can use latest versions of zod@3 for migrations we support it.
+
+```ts
+import { z } from 'zod/v3';
+import { zodContract } from '@withease/zod';
+```
+
+## Before zod@3.25.0
+
+You can use adapter from [Farfetched](https://ff.effector.dev/)
+
+```ts
+import { z } from 'zod';
+import { zodContract } from '@farfetched/zod';
+```
diff --git a/apps/website/docs/zod/index.md b/apps/website/docs/zod/index.md
new file mode 100644
index 00000000..ee7e1308
--- /dev/null
+++ b/apps/website/docs/zod/index.md
@@ -0,0 +1,39 @@
+
+
+# @withease/zod
+
+Small adapters (less than **{{maxSize}}** summary controlled by CI) for [Zod](https://zod.dev/) package support.
+
+## Installation
+
+First, you need to install package:
+
+::: code-group
+
+```sh [pnpm]
+pnpm add @withease/zod
+```
+
+```sh [yarn]
+yarn add @withease/zod
+```
+
+```sh [npm]
+npm install @withease/zod
+```
+
+:::
+
+## Version compatibility
+
+Best way of usage - use a supported zod version. zod@4 is preferred.\
+But we understand the need to maintain a transitional versions to assist with migrations.\
+[Read more](./compatibility)
+
+## API
+
+- [zodContract](./api/contract/)
diff --git a/packages/zod/README.md b/packages/zod/README.md
new file mode 100644
index 00000000..b8b36636
--- /dev/null
+++ b/packages/zod/README.md
@@ -0,0 +1,3 @@
+# @withease/zod
+
+Read documentation [here](https://withease.effector.dev/zod/).
diff --git a/packages/zod/package.json b/packages/zod/package.json
new file mode 100644
index 00000000..81613951
--- /dev/null
+++ b/packages/zod/package.json
@@ -0,0 +1,50 @@
+{
+ "name": "@withease/zod",
+ "version": "0.1.0",
+ "license": "MIT",
+ "repository": "https://github.com/effector/withease",
+ "scripts": {
+ "test:run": "vitest run --typecheck",
+ "test:watch": "vitest --typecheck",
+ "build": "vite build",
+ "size": "size-limit",
+ "publint": "node ../../tools/publint.mjs",
+ "typelint": "attw --pack"
+ },
+ "peerDependencies": {
+ "@withease/contracts": "workspace:*",
+ "zod": "^3.25.0 || ^4.0.0"
+ },
+ "devDependencies": {
+ "@withease/contracts": "workspace:*",
+ "zod": "^3.25.0 || ^4.0.0"
+ },
+ "type": "module",
+ "publishConfig": {
+ "access": "public"
+ },
+ "files": [
+ "dist"
+ ],
+ "main": "./dist/zod.cjs",
+ "module": "./dist/zod.js",
+ "types": "./dist/zod.d.ts",
+ "exports": {
+ ".": {
+ "import": {
+ "types": "./dist/zod.d.ts",
+ "default": "./dist/zod.js"
+ },
+ "require": {
+ "types": "./dist/zod.d.cts",
+ "default": "./dist/zod.cjs"
+ }
+ }
+ },
+ "size-limit": [
+ {
+ "path": "./dist/zod.js",
+ "limit": "315 B"
+ }
+ ]
+}
diff --git a/packages/zod/src/__tests__/contract.test-d.ts b/packages/zod/src/__tests__/contract.test-d.ts
new file mode 100644
index 00000000..7da907e6
--- /dev/null
+++ b/packages/zod/src/__tests__/contract.test-d.ts
@@ -0,0 +1,218 @@
+import { describe, test, expectTypeOf } from 'vitest';
+import { z as zodV3 } from 'zod/v3';
+import { z as zodV4 } from 'zod/v4';
+import { z as zodV4mini } from 'zod/v4-mini';
+
+import { zodContract } from '../zod_contract';
+
+describe('zodContract (zod v3)', () => {
+ test('string', () => {
+ const stringContract = zodContract(zodV3.string());
+
+ const smth: unknown = null;
+
+ if (stringContract.isData(smth)) {
+ expectTypeOf(smth).toEqualTypeOf();
+ expectTypeOf(smth).not.toEqualTypeOf();
+ }
+ });
+
+ test('complex object', () => {
+ const complexContract = zodContract(
+ zodV3.tuple([
+ zodV3.object({
+ x: zodV3.number(),
+ y: zodV3.literal(false),
+ k: zodV3.set(zodV3.string()),
+ }),
+ zodV3.literal('literal'),
+ zodV3.literal(42),
+ ])
+ );
+
+ const smth: unknown = null;
+
+ if (complexContract.isData(smth)) {
+ expectTypeOf(smth).toEqualTypeOf<
+ [
+ {
+ x: number;
+ y: false;
+ k: Set;
+ },
+ 'literal',
+ 42
+ ]
+ >();
+
+ expectTypeOf(smth).not.toEqualTypeOf();
+
+ expectTypeOf(smth).not.toEqualTypeOf<
+ [
+ {
+ x: string;
+ y: false;
+ k: Set;
+ },
+ 'literal',
+ 42
+ ]
+ >();
+ }
+ });
+
+ test('branded type', () => {
+ const BrandedContainer = zodV3.object({
+ branded: zodV3.string().brand<'Branded'>(),
+ });
+ const brandedContract = zodContract(BrandedContainer);
+
+ const smth: unknown = { branded: 'branded' };
+
+ if (brandedContract.isData(smth)) {
+ expectTypeOf(smth).toEqualTypeOf>();
+ }
+ });
+});
+
+describe('zodContract (zod v4)', () => {
+ test('string', () => {
+ const stringContract = zodContract(zodV4.string());
+
+ const smth: unknown = null;
+
+ if (stringContract.isData(smth)) {
+ expectTypeOf(smth).toEqualTypeOf();
+ expectTypeOf(smth).not.toEqualTypeOf();
+ }
+ });
+
+ test('complex object', () => {
+ const complexContract = zodContract(
+ zodV4.tuple([
+ zodV4.object({
+ x: zodV4.number(),
+ y: zodV4.literal(false),
+ k: zodV4.set(zodV4.string()),
+ }),
+ zodV4.literal('literal'),
+ zodV4.literal(42),
+ ])
+ );
+
+ const smth: unknown = null;
+
+ if (complexContract.isData(smth)) {
+ expectTypeOf(smth).toEqualTypeOf<
+ [
+ {
+ x: number;
+ y: false;
+ k: Set;
+ },
+ 'literal',
+ 42
+ ]
+ >();
+
+ expectTypeOf(smth).not.toEqualTypeOf();
+
+ expectTypeOf(smth).not.toEqualTypeOf<
+ [
+ {
+ x: string;
+ y: false;
+ k: Set;
+ },
+ 'literal',
+ 42
+ ]
+ >();
+ }
+ });
+
+ test('branded type', () => {
+ const BrandedContainer = zodV4.object({
+ branded: zodV4.string().brand<'Branded'>(),
+ });
+ const brandedContract = zodContract(BrandedContainer);
+
+ const smth: unknown = { branded: 'branded' };
+
+ if (brandedContract.isData(smth)) {
+ expectTypeOf(smth).toEqualTypeOf>();
+ }
+ });
+});
+
+describe('zodContract (zod v4-mini)', () => {
+ test('string', () => {
+ const stringContract = zodContract(zodV4mini.string());
+
+ const smth: unknown = null;
+
+ if (stringContract.isData(smth)) {
+ expectTypeOf(smth).toEqualTypeOf();
+ expectTypeOf(smth).not.toEqualTypeOf();
+ }
+ });
+
+ test('complex object', () => {
+ const complexContract = zodContract(
+ zodV4mini.tuple([
+ zodV4mini.object({
+ x: zodV4mini.number(),
+ y: zodV4mini.literal(false),
+ k: zodV4mini.set(zodV4mini.string()),
+ }),
+ zodV4mini.literal('literal'),
+ zodV4mini.literal(42),
+ ])
+ );
+
+ const smth: unknown = null;
+
+ if (complexContract.isData(smth)) {
+ expectTypeOf(smth).toEqualTypeOf<
+ [
+ {
+ x: number;
+ y: false;
+ k: Set;
+ },
+ 'literal',
+ 42
+ ]
+ >();
+
+ expectTypeOf(smth).not.toEqualTypeOf();
+
+ expectTypeOf(smth).not.toEqualTypeOf<
+ [
+ {
+ x: string;
+ y: false;
+ k: Set;
+ },
+ 'literal',
+ 42
+ ]
+ >();
+ }
+ });
+
+ test('branded type', () => {
+ const BrandedContainer = zodV4mini.object({
+ branded: zodV4mini.string().brand<'Branded'>(),
+ });
+ const brandedContract = zodContract(BrandedContainer);
+
+ const smth: unknown = { branded: 'branded' };
+
+ if (brandedContract.isData(smth)) {
+ expectTypeOf(smth).toEqualTypeOf<
+ zodV4mini.infer
+ >();
+ }
+ });
+});
diff --git a/packages/zod/src/__tests__/contract.test.ts b/packages/zod/src/__tests__/contract.test.ts
new file mode 100644
index 00000000..025315a7
--- /dev/null
+++ b/packages/zod/src/__tests__/contract.test.ts
@@ -0,0 +1,363 @@
+import { z as zod_v3 } from 'zod/v3';
+import { z as zod_v4 } from 'zod/v4';
+import { z as zod_v4mini } from 'zod/v4-mini';
+import { describe, test, expect } from 'vitest';
+
+import { zodContract } from '../zod_contract';
+
+describe('zod/zodContract short (zod v3)', () => {
+ const zod = zod_v3;
+
+ test('interprets invalid response as error', () => {
+ const contract = zodContract(zod.string());
+
+ expect(contract.getErrorMessages(2)).toMatchInlineSnapshot(`
+ [
+ "Expected string, received number",
+ ]
+ `);
+ });
+
+ test('passes valid data', () => {
+ const contract = zodContract(zod.string());
+
+ expect(contract.getErrorMessages('foo')).toEqual([]);
+ });
+
+ test('isData passes for valid data', () => {
+ const contract = zodContract(
+ zod.object({
+ x: zod.number(),
+ y: zod.string(),
+ })
+ );
+
+ expect(
+ contract.isData({
+ x: 42,
+ y: 'answer',
+ })
+ ).toEqual(true);
+ });
+
+ test('isData does not pass for invalid data', () => {
+ const contract = zodContract(
+ zod.object({
+ x: zod.number(),
+ y: zod.string(),
+ })
+ );
+
+ expect(
+ contract.isData({
+ 42: 'x',
+ answer: 'y',
+ })
+ ).toEqual(false);
+ });
+
+ test('interprets complex invalid response as error', () => {
+ const contract = zodContract(
+ zod.tuple([
+ zod.object({
+ x: zod.number(),
+ y: zod.literal(true),
+ k: zod
+ .set(zod.string())
+ .nonempty('Invalid set, expected set of strings'),
+ }),
+ zod.literal('Uhm?'),
+ zod.literal(42),
+ ])
+ );
+
+ expect(
+ contract.getErrorMessages([
+ {
+ x: 456,
+ y: false,
+ k: new Set(),
+ },
+ 'Answer is:',
+ '42',
+ ])
+ ).toMatchInlineSnapshot(`
+ [
+ "Invalid literal value, expected true, path: 0.y",
+ "Invalid set, expected set of strings, path: 0.k",
+ "Invalid literal value, expected "Uhm?", path: 1",
+ "Invalid literal value, expected 42, path: 2",
+ ]
+ `);
+ });
+
+ test('path from original zod error included in final message', () => {
+ const contract = zodContract(
+ zod.object({
+ x: zod.number(),
+ y: zod.object({
+ z: zod.string(),
+ k: zod.object({
+ j: zod.boolean(),
+ }),
+ }),
+ })
+ );
+
+ expect(
+ contract.getErrorMessages({
+ x: '42',
+ y: {
+ z: 123,
+ k: {
+ j: new Map(),
+ },
+ },
+ })
+ ).toMatchInlineSnapshot(`
+ [
+ "Expected number, received string, path: x",
+ "Expected string, received number, path: y.z",
+ "Expected boolean, received map, path: y.k.j",
+ ]
+ `);
+ });
+});
+
+describe('zod/zodContract short (zod v4)', () => {
+ const zod = zod_v4;
+
+ test('interprets invalid response as error', () => {
+ const contract = zodContract(zod.string());
+
+ expect(contract.getErrorMessages(2)).toMatchInlineSnapshot(`
+ [
+ "Invalid input: expected string, received number",
+ ]
+ `);
+ });
+
+ test('passes valid data', () => {
+ const contract = zodContract(zod.string());
+
+ expect(contract.getErrorMessages('foo')).toEqual([]);
+ });
+
+ test('isData passes for valid data', () => {
+ const contract = zodContract(
+ zod.object({
+ x: zod.number(),
+ y: zod.string(),
+ })
+ );
+
+ expect(
+ contract.isData({
+ x: 42,
+ y: 'answer',
+ })
+ ).toEqual(true);
+ });
+
+ test('isData does not pass for invalid data', () => {
+ const contract = zodContract(
+ zod.object({
+ x: zod.number(),
+ y: zod.string(),
+ })
+ );
+
+ expect(
+ contract.isData({
+ 42: 'x',
+ answer: 'y',
+ })
+ ).toEqual(false);
+ });
+
+ test('interprets complex invalid response as error', () => {
+ const contract = zodContract(
+ zod.tuple([
+ zod.object({
+ x: zod.number(),
+ y: zod.literal(true),
+ k: zod
+ .set(zod.string())
+ .nonempty('Invalid input: expected set of strings'),
+ }),
+ zod.literal('Uhm?'),
+ zod.literal(42),
+ ])
+ );
+
+ expect(
+ contract.getErrorMessages([
+ {
+ x: 456,
+ y: false,
+ k: new Set(),
+ },
+ 'Answer is:',
+ '42',
+ ])
+ ).toMatchInlineSnapshot(`
+ [
+ "Invalid input: expected true, path: 0.y",
+ "Invalid input: expected set of strings, path: 0.k",
+ "Invalid input: expected "Uhm?", path: 1",
+ "Invalid input: expected 42, path: 2",
+ ]
+ `);
+ });
+
+ test('path from original zod error included in final message', () => {
+ const contract = zodContract(
+ zod.object({
+ x: zod.number(),
+ y: zod.object({
+ z: zod.string(),
+ k: zod.object({
+ j: zod.boolean(),
+ }),
+ }),
+ })
+ );
+
+ expect(
+ contract.getErrorMessages({
+ x: '42',
+ y: {
+ z: 123,
+ k: {
+ j: new Map(),
+ },
+ },
+ })
+ ).toMatchInlineSnapshot(`
+ [
+ "Invalid input: expected number, received string, path: x",
+ "Invalid input: expected string, received number, path: y.z",
+ "Invalid input: expected boolean, received Map, path: y.k.j",
+ ]
+ `);
+ });
+});
+
+describe('zod/zodContract short (zod v4-mini)', () => {
+ const zod = zod_v4mini;
+
+ test('interprets invalid response as error', () => {
+ const contract = zodContract(zod.string());
+
+ expect(contract.getErrorMessages(2)).toMatchInlineSnapshot(`
+ [
+ "Invalid input: expected string, received number",
+ ]
+ `);
+ });
+
+ test('passes valid data', () => {
+ const contract = zodContract(zod.string());
+
+ expect(contract.getErrorMessages('foo')).toEqual([]);
+ });
+
+ test('isData passes for valid data', () => {
+ const contract = zodContract(
+ zod.object({
+ x: zod.number(),
+ y: zod.string(),
+ })
+ );
+
+ expect(
+ contract.isData({
+ x: 42,
+ y: 'answer',
+ })
+ ).toEqual(true);
+ });
+
+ test('isData does not pass for invalid data', () => {
+ const contract = zodContract(
+ zod.object({
+ x: zod.number(),
+ y: zod.string(),
+ })
+ );
+
+ expect(
+ contract.isData({
+ 42: 'x',
+ answer: 'y',
+ })
+ ).toEqual(false);
+ });
+
+ test('interprets complex invalid response as error', () => {
+ const contract = zodContract(
+ zod.tuple([
+ zod.object({
+ x: zod.number(),
+ y: zod.literal(true),
+ k: zod.set(zod.string(), {
+ error: 'Invalid input: expected set of strings',
+ }),
+ }),
+ zod.literal('Uhm?'),
+ zod.literal(42),
+ ])
+ );
+
+ expect(
+ contract.getErrorMessages([
+ {
+ x: 456,
+ y: false,
+ k: new Map(),
+ },
+ 'Answer is:',
+ '42',
+ ])
+ ).toMatchInlineSnapshot(`
+ [
+ "Invalid input: expected true, path: 0.y",
+ "Invalid input: expected set of strings, path: 0.k",
+ "Invalid input: expected "Uhm?", path: 1",
+ "Invalid input: expected 42, path: 2",
+ ]
+ `);
+ });
+
+ test('path from original zod error included in final message', () => {
+ const contract = zodContract(
+ zod.object({
+ x: zod.number(),
+ y: zod.object({
+ z: zod.string(),
+ k: zod.object({
+ j: zod.boolean(),
+ }),
+ }),
+ })
+ );
+
+ expect(
+ contract.getErrorMessages({
+ x: '42',
+ y: {
+ z: 123,
+ k: {
+ j: new Map(),
+ },
+ },
+ })
+ ).toMatchInlineSnapshot(`
+ [
+ "Invalid input: expected number, received string, path: x",
+ "Invalid input: expected string, received number, path: y.z",
+ "Invalid input: expected boolean, received Map, path: y.k.j",
+ ]
+ `);
+ });
+});
diff --git a/packages/zod/src/index.ts b/packages/zod/src/index.ts
new file mode 100644
index 00000000..44de7711
--- /dev/null
+++ b/packages/zod/src/index.ts
@@ -0,0 +1 @@
+export { zodContract } from './zod_contract';
diff --git a/packages/zod/src/zod_contract.ts b/packages/zod/src/zod_contract.ts
new file mode 100644
index 00000000..139d453b
--- /dev/null
+++ b/packages/zod/src/zod_contract.ts
@@ -0,0 +1,67 @@
+import {
+ type ZodType as ZodTypeV3,
+ type ZodError as ZodErrorV3,
+ type TypeOf as TypeOfV3,
+} from 'zod/v3';
+import {
+ type $ZodType as ZodTypeV4,
+ type output as TypeOfV4,
+ type $ZodError as ZodErrorV4,
+ safeParse,
+} from 'zod/v4/core';
+import { type Contract } from '@withease/contracts';
+
+type ZodAnyType = ZodTypeV3 | ZodTypeV4;
+type ZodAnyError = ZodErrorV3 | ZodErrorV4;
+
+type Output = T extends ZodTypeV4
+ ? TypeOfV4
+ : T extends ZodTypeV3
+ ? TypeOfV3
+ : never;
+
+type ErrorTransformer = (
+ issues: ZodAnyError
+) => ReturnType['getErrorMessages']>;
+
+function isZodV4(schema: unknown): schema is ZodTypeV4 {
+ return !!schema && typeof schema === 'object' && '_zod' in schema;
+}
+
+const standardErrorTransformer: ErrorTransformer = (error) => {
+ return error.issues.map((e) => {
+ const path = e.path.join('.');
+ return path !== '' ? `${e.message}, path: ${path}` : e.message;
+ });
+};
+
+/**
+ * Transforms Zod contracts for `data` to internal Contract.
+ * Any response which does not conform to `data` will be treated as error.
+ *
+ * @param {ZodTypeV3 | ZodTypeV4} data Zod Contract for valid data
+ */
+function zodContract(
+ data: T
+): Contract> {
+ function isData(prepared: unknown): prepared is Output {
+ if (isZodV4(data)) return safeParse(data, prepared).success;
+ return data.safeParse(prepared).success;
+ }
+
+ return {
+ isData,
+ getErrorMessages(raw) {
+ const validation = isZodV4(data)
+ ? safeParse(data, raw)
+ : data.safeParse(raw);
+ if (validation.success) {
+ return [];
+ }
+
+ return standardErrorTransformer(validation.error);
+ },
+ };
+}
+
+export { zodContract };
diff --git a/packages/zod/tsconfig.json b/packages/zod/tsconfig.json
new file mode 100644
index 00000000..65b1ff2c
--- /dev/null
+++ b/packages/zod/tsconfig.json
@@ -0,0 +1,11 @@
+{
+ "extends": "../../tsconfig.base.json",
+ "compilerOptions": {
+ "declaration": true,
+ "types": ["node"],
+ "outDir": "dist",
+ "rootDir": "src",
+ "baseUrl": "src"
+ },
+ "include": ["src/**/*.ts"]
+}
diff --git a/packages/zod/vite.config.ts b/packages/zod/vite.config.ts
new file mode 100644
index 00000000..983728ea
--- /dev/null
+++ b/packages/zod/vite.config.ts
@@ -0,0 +1,22 @@
+import tsconfigPaths from 'vite-tsconfig-paths';
+import dts from '../../tools/vite/types';
+
+export default {
+ test: {
+ typecheck: {
+ ignoreSourceErrors: true,
+ },
+ },
+ plugins: [tsconfigPaths(), dts()],
+ build: {
+ lib: {
+ entry: 'src/index.ts',
+ name: '@withease/zod',
+ fileName: 'zod',
+ formats: ['es', 'cjs'],
+ },
+ rollupOptions: {
+ external: ['zod', 'zod/v3', 'zod/v4/core'],
+ },
+ },
+};
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 14dc7703..9485a514 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -114,6 +114,14 @@ importers:
packages/web-api:
specifiers: {}
+ packages/zod:
+ specifiers:
+ '@withease/contracts': workspace:*
+ zod: ^3.25.0 || ^4.0.0
+ devDependencies:
+ '@withease/contracts': link:../contracts
+ zod: 4.1.5
+
packages:
/@algolia/autocomplete-core/1.9.3_andexh5lxsyy34kfg3pekcyz5e:
@@ -4696,3 +4704,7 @@ packages:
optionalDependencies:
commander: 9.5.0
dev: true
+
+ /zod/4.1.5:
+ resolution: {integrity: sha512-rcUUZqlLJgBC33IT3PNMgsCq6TzLQEG/Ei/KTCU0PedSWRMAXoOUN+4t/0H+Q8bdnLPdqUYnvboJT0bn/229qg==}
+ dev: true