diff --git a/bun.lockb b/bun.lockb
index 4ef05dd..22706bc 100755
Binary files a/bun.lockb and b/bun.lockb differ
diff --git a/package.json b/package.json
index cdf297a..2adc4f6 100644
--- a/package.json
+++ b/package.json
@@ -72,6 +72,7 @@
"devDependencies": {
"@effect/schema": "^0.72.0",
"@prisma/studio-server": "^0.502.0",
+ "@tanstack/form-core": "^0.30.0",
"@types/jsonwebtoken": "^9.0.6",
"bun-types": "^1.1.26",
"effect": "^3.7.0",
diff --git a/src/Schema/TanStackForm/makeSchemaFormValidator.ts b/src/Schema/TanStackForm/makeSchemaFormValidator.ts
new file mode 100644
index 0000000..4349e3a
--- /dev/null
+++ b/src/Schema/TanStackForm/makeSchemaFormValidator.ts
@@ -0,0 +1,138 @@
+import { ArrayFormatter, Schema } from "@effect/schema"
+import type { ParseOptions } from "@effect/schema/AST"
+import type { DeepKeys, DeepValue, FieldApi, FormApi, FormValidationError, ValidationError, Validator } from "@tanstack/react-form"
+import { Array, Effect, Either, flow, Layer, ManagedRuntime, pipe } from "effect"
+import { mapValues } from "remeda"
+
+
+export const makeSchemaFormValidator = (
+ schema: Schema.Schema,
+ options?: ParseOptions,
+) => {
+ const decodeEither = Schema.decodeEither(schema, options)
+
+ return <
+ TFormData extends I,
+ TFormValidator extends Validator | undefined,
+ >(props: {
+ value: TFormData
+ formApi: FormApi
+ }): FormValidationError =>
+ decodeEither(props.value).pipe(result =>
+ Either.isLeft(result)
+ ? {
+ form: "Please check the fields",
+ fields: issuesToFieldsRecord(ArrayFormatter.formatErrorSync(result.left), props.formApi) as any,
+ }
+ : null
+ )
+}
+
+export const makeSchemaAsyncFormValidator = (
+ schema: Schema.Schema,
+ options?: ParseOptions,
+) => {
+ const runtime = ManagedRuntime.make(Layer.empty)
+ const decode = Schema.decode(schema, options)
+
+ return <
+ TFormData extends I,
+ TFormValidator extends Validator | undefined,
+ >(props: {
+ value: TFormData
+ formApi: FormApi
+ signal: AbortSignal
+ }): Promise> =>
+ decode(props.value).pipe(
+ Effect.matchEffect({
+ onSuccess: () => Effect.succeed(null),
+
+ onFailure: e => ArrayFormatter.formatError(e).pipe(
+ Effect.map(issues => ({
+ form: "Please check the fields",
+ fields: issuesToFieldsRecord(issues, props.formApi) as any,
+ }))
+ ),
+ }),
+
+ prgm => runtime.runPromise(prgm, { signal: props.signal }),
+ )
+}
+
+/**
+ * TODO: handle nested array fields
+ */
+const issuesToFieldsRecord = <
+ TFormData,
+ TFormValidator extends Validator | undefined = undefined,
+>(
+ issues: readonly ArrayFormatter.Issue[],
+ _formApi: FormApi,
+) => pipe(issues,
+ Array.groupBy(issue => issue.path.join(".")),
+
+ mapValues(flow(
+ Array.map(issue => issue.message),
+ Array.join("\n"),
+ )),
+)
+
+
+export const makeSchemaFieldValidator = (
+ schema: Schema.Schema,
+ options?: ParseOptions,
+) => {
+ const decodeEither = Schema.decodeEither(schema, options)
+
+ return <
+ TParentData,
+ TName extends DeepKeys,
+ TFieldValidator extends Validator, unknown> | undefined,
+ TFormValidator extends Validator | undefined,
+ TData extends I & DeepValue,
+ >(props: {
+ value: TData
+ fieldApi: FieldApi
+ }): ValidationError =>
+ decodeEither(props.value).pipe(result =>
+ Either.isLeft(result)
+ ? ArrayFormatter.formatErrorSync(result.left)
+ .map(issue => issue.message)
+ .join("\n")
+ : null
+ )
+}
+
+export const makeSchemaAsyncFieldValidator = (
+ schema: Schema.Schema,
+ options?: ParseOptions,
+) => {
+ const runtime = ManagedRuntime.make(Layer.empty)
+ const decode = Schema.decode(schema, options)
+
+ return <
+ TParentData,
+ TName extends DeepKeys,
+ TFieldValidator extends Validator, unknown> | undefined,
+ TFormValidator extends Validator | undefined,
+ TData extends I & DeepValue,
+ >(props: {
+ value: TData
+ fieldApi: FieldApi
+ signal: AbortSignal
+ }): Promise =>
+ decode(props.value).pipe(
+ Effect.matchEffect({
+ onSuccess: () => Effect.succeed(null),
+
+ onFailure: e => ArrayFormatter.formatError(e).pipe(
+ Effect.map(flow(
+ Array.map(issue => issue.message),
+ Array.join("\n"),
+ ))
+ ),
+ }),
+
+ prgm => runtime.runPromise(prgm, { signal: props.signal }),
+ )
+}