fix(deps): upgrade zod to v4 and fix breaking changes (#3461)

* fix(deps): update dependency drizzle-zod to ^0.8.2

* chore: update zod to v4 import

* fix: path is no longer available in transform context

* fix: AnyZodObject does no longer exist

* fix: auth env.ts using wrong createEnv and remove unused file env-validation.ts

* fix: required_error no longer exists on z.string

* fix: zod error map is deprecated and replaced with config

* fix: default requires callback now

* fix: migrate zod resolver for mantine

* fix: remove unused form translation file

* fix: wrong enum type

* fix: record now requires two arguments

* fix: add-confirm-password-refinement type issues

* fix: add missing first record argument for entityStateSchema

* fix: migrate superrefine to check

* fix(deps): upgrade zod-form-data to v3

* fix: migrate superRefine to check for mediaUploadSchema

* fix: authProvidersSchema default is array

* fix: use stringbool instead of custom implementation

* fix: record requires first argument

* fix: migrate superRefine to check for certificate router

* fix: confirm pasword refinement is overwriting types

* fix: email optional not working

* fix: migrate intersection to object converter

* fix: safe parse return value rename

* fix: easier access for min and max number value

* fix: migrate superRefine to check for oldmarr import file

* fix: inference of enum shape for old-import board-size wrong

* fix: errors renamed to issues

* chore: address pull request feedback

* fix: zod form requires object

* fix: inference for use-zod-form not working

* fix: remove unnecessary convertion

* fix(deps): upgrade trpc-to-openapi to v3

* fix: build error

* fix: migrate missing zod imports to v4

* fix: migrate zod records to v4

* fix: missing core package dependency in api module

* fix: unable to convert custom zod schema to openapi schema

* fix(deps): upgrade zod to v4

* chore(renovate): enable zod dependency updates

* test: add simple unit test for convertIntersectionToZodObject

---------

Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
This commit is contained in:
Meier Lukas
2025-08-15 20:15:58 +02:00
committed by GitHub
parent b7455d18ed
commit 5c99622fa8
174 changed files with 653 additions and 631 deletions

View File

@@ -6,10 +6,6 @@
matchPackagePatterns: ["^@homarr/"], matchPackagePatterns: ["^@homarr/"],
enabled: false, enabled: false,
}, },
{
matchPackagePatterns: ["^zod$", "^drizzle-zod$", "^zod-form-data$"],
enabled: false,
},
{ {
matchUpdateTypes: ["minor", "patch", "pin", "digest"], matchUpdateTypes: ["minor", "patch", "pin", "digest"],
automerge: true, automerge: true,

View File

@@ -87,7 +87,7 @@
"superjson": "2.2.2", "superjson": "2.2.2",
"swagger-ui-react": "^5.27.1", "swagger-ui-react": "^5.27.1",
"use-deep-compare-effect": "^1.8.1", "use-deep-compare-effect": "^1.8.1",
"zod": "^3.25.76" "zod": "^4.0.14"
}, },
"devDependencies": { "devDependencies": {
"@homarr/eslint-config": "workspace:^0.2.0", "@homarr/eslint-config": "workspace:^0.2.0",

View File

@@ -2,7 +2,7 @@
import { useRouter } from "next/navigation"; import { useRouter } from "next/navigation";
import { Button, PasswordInput, Stack, TextInput } from "@mantine/core"; import { Button, PasswordInput, Stack, TextInput } from "@mantine/core";
import type { z } from "zod"; import type { z } from "zod/v4";
import { clientApi } from "@homarr/api/client"; import { clientApi } from "@homarr/api/client";
import { useZodForm } from "@homarr/form"; import { useZodForm } from "@homarr/form";

View File

@@ -5,7 +5,7 @@ import { useCallback, useEffect, useRef, useState } from "react";
import { useRouter, useSearchParams } from "next/navigation"; import { useRouter, useSearchParams } from "next/navigation";
import { Anchor, Button, Card, Code, Collapse, Divider, PasswordInput, Stack, Text, TextInput } from "@mantine/core"; import { Anchor, Button, Card, Code, Collapse, Divider, PasswordInput, Stack, Text, TextInput } from "@mantine/core";
import { useDisclosure } from "@mantine/hooks"; import { useDisclosure } from "@mantine/hooks";
import { z } from "zod"; import { z } from "zod/v4";
import { signIn } from "@homarr/auth/client"; import { signIn } from "@homarr/auth/client";
import { revalidatePathActionAsync } from "@homarr/common/client"; import { revalidatePathActionAsync } from "@homarr/common/client";

View File

@@ -2,7 +2,7 @@
import { Button, Card, Stack, TextInput } from "@mantine/core"; import { Button, Card, Stack, TextInput } from "@mantine/core";
import { IconArrowRight } from "@tabler/icons-react"; import { IconArrowRight } from "@tabler/icons-react";
import type { z } from "zod"; import type { z } from "zod/v4";
import { clientApi } from "@homarr/api/client"; import { clientApi } from "@homarr/api/client";
import { revalidatePathActionAsync } from "@homarr/common/client"; import { revalidatePathActionAsync } from "@homarr/common/client";

View File

@@ -3,7 +3,7 @@
import { startTransition } from "react"; import { startTransition } from "react";
import { Button, Card, Group, Stack, Switch, Text } from "@mantine/core"; import { Button, Card, Group, Stack, Switch, Text } from "@mantine/core";
import { IconArrowRight } from "@tabler/icons-react"; import { IconArrowRight } from "@tabler/icons-react";
import type { z } from "zod"; import type { z } from "zod/v4";
import { clientApi } from "@homarr/api/client"; import { clientApi } from "@homarr/api/client";
import { revalidatePathActionAsync } from "@homarr/common/client"; import { revalidatePathActionAsync } from "@homarr/common/client";

View File

@@ -1,7 +1,7 @@
"use client"; "use client";
import { Button, PasswordInput, Stack, TextInput } from "@mantine/core"; import { Button, PasswordInput, Stack, TextInput } from "@mantine/core";
import type { z } from "zod"; import type { z } from "zod/v4";
import { clientApi } from "@homarr/api/client"; import { clientApi } from "@homarr/api/client";
import { signIn } from "@homarr/auth/client"; import { signIn } from "@homarr/auth/client";

View File

@@ -2,7 +2,7 @@
import { useCallback } from "react"; import { useCallback } from "react";
import { useRouter } from "next/navigation"; import { useRouter } from "next/navigation";
import type { z } from "zod"; import type { z } from "zod/v4";
import type { RouterOutputs } from "@homarr/api"; import type { RouterOutputs } from "@homarr/api";
import { clientApi } from "@homarr/api/client"; import { clientApi } from "@homarr/api/client";

View File

@@ -2,7 +2,7 @@ import Link from "next/link";
import { redirect } from "next/navigation"; import { redirect } from "next/navigation";
import { ActionIcon, ActionIconGroup, Anchor, Avatar, Card, Group, Stack, Text, Title } from "@mantine/core"; import { ActionIcon, ActionIconGroup, Anchor, Avatar, Card, Group, Stack, Text, Title } from "@mantine/core";
import { IconBox, IconPencil } from "@tabler/icons-react"; import { IconBox, IconPencil } from "@tabler/icons-react";
import { z } from "zod"; import { z } from "zod/v4";
import type { RouterOutputs } from "@homarr/api"; import type { RouterOutputs } from "@homarr/api";
import { api } from "@homarr/api/server"; import { api } from "@homarr/api/server";

View File

@@ -4,7 +4,7 @@ import { useState } from "react";
import Link from "next/link"; import Link from "next/link";
import { useRouter } from "next/navigation"; import { useRouter } from "next/navigation";
import { Button, Fieldset, Group, Stack, TextInput } from "@mantine/core"; import { Button, Fieldset, Group, Stack, TextInput } from "@mantine/core";
import type { z } from "zod"; import type { z } from "zod/v4";
import type { RouterOutputs } from "@homarr/api"; import type { RouterOutputs } from "@homarr/api";
import { clientApi } from "@homarr/api/client"; import { clientApi } from "@homarr/api/client";

View File

@@ -16,7 +16,7 @@ import {
TextInput, TextInput,
} from "@mantine/core"; } from "@mantine/core";
import { IconInfoCircle } from "@tabler/icons-react"; import { IconInfoCircle } from "@tabler/icons-react";
import { z } from "zod"; import { z } from "zod/v4";
import { clientApi } from "@homarr/api/client"; import { clientApi } from "@homarr/api/client";
import { revalidatePathActionAsync } from "@homarr/common/client"; import { revalidatePathActionAsync } from "@homarr/common/client";

View File

@@ -1,6 +1,6 @@
import { notFound } from "next/navigation"; import { notFound } from "next/navigation";
import { Container, Group, Stack, Title } from "@mantine/core"; import { Container, Group, Stack, Title } from "@mantine/core";
import { z } from "zod"; import { z } from "zod/v4";
import { auth } from "@homarr/auth/next"; import { auth } from "@homarr/auth/next";
import type { IntegrationKind } from "@homarr/definitions"; import type { IntegrationKind } from "@homarr/definitions";

View File

@@ -16,7 +16,7 @@ import {
Tooltip, Tooltip,
} from "@mantine/core"; } from "@mantine/core";
import { IconExternalLink } from "@tabler/icons-react"; import { IconExternalLink } from "@tabler/icons-react";
import { z } from "zod"; import { z } from "zod/v4";
import type { RouterOutputs } from "@homarr/api"; import type { RouterOutputs } from "@homarr/api";
import { api } from "@homarr/api/server"; import { api } from "@homarr/api/server";

View File

@@ -4,7 +4,7 @@ import Link from "next/link";
import type { SegmentedControlItem } from "@mantine/core"; import type { SegmentedControlItem } from "@mantine/core";
import { Button, Fieldset, Grid, Group, SegmentedControl, Stack, Textarea, TextInput } from "@mantine/core"; import { Button, Fieldset, Grid, Group, SegmentedControl, Stack, Textarea, TextInput } from "@mantine/core";
import { WidgetIntegrationSelect } from "node_modules/@homarr/widgets/src/widget-integration-select"; import { WidgetIntegrationSelect } from "node_modules/@homarr/widgets/src/widget-integration-select";
import type { z } from "zod"; import type { z } from "zod/v4";
import { clientApi } from "@homarr/api/client"; import { clientApi } from "@homarr/api/client";
import { searchEngineTypes } from "@homarr/definitions"; import { searchEngineTypes } from "@homarr/definitions";

View File

@@ -2,7 +2,7 @@
import { useCallback } from "react"; import { useCallback } from "react";
import { useRouter } from "next/navigation"; import { useRouter } from "next/navigation";
import type { z } from "zod"; import type { z } from "zod/v4";
import type { RouterOutputs } from "@homarr/api"; import type { RouterOutputs } from "@homarr/api";
import { clientApi } from "@homarr/api/client"; import { clientApi } from "@homarr/api/client";

View File

@@ -2,7 +2,7 @@
import { useCallback } from "react"; import { useCallback } from "react";
import { useRouter } from "next/navigation"; import { useRouter } from "next/navigation";
import type { z } from "zod"; import type { z } from "zod/v4";
import { clientApi } from "@homarr/api/client"; import { clientApi } from "@homarr/api/client";
import { revalidatePathActionAsync } from "@homarr/common/client"; import { revalidatePathActionAsync } from "@homarr/common/client";

View File

@@ -2,7 +2,7 @@ import Link from "next/link";
import { redirect } from "next/navigation"; import { redirect } from "next/navigation";
import { ActionIcon, ActionIconGroup, Anchor, Avatar, Card, Group, Stack, Text, Title } from "@mantine/core"; import { ActionIcon, ActionIconGroup, Anchor, Avatar, Card, Group, Stack, Text, Title } from "@mantine/core";
import { IconPencil, IconSearch } from "@tabler/icons-react"; import { IconPencil, IconSearch } from "@tabler/icons-react";
import { z } from "zod"; import { z } from "zod/v4";
import type { RouterOutputs } from "@homarr/api"; import type { RouterOutputs } from "@homarr/api";
import { api } from "@homarr/api/server"; import { api } from "@homarr/api/server";

View File

@@ -1,7 +1,7 @@
"use client"; "use client";
import { Button, Group, Stack } from "@mantine/core"; import { Button, Group, Stack } from "@mantine/core";
import type { z } from "zod"; import type { z } from "zod/v4";
import type { RouterOutputs } from "@homarr/api"; import type { RouterOutputs } from "@homarr/api";
import { clientApi } from "@homarr/api/client"; import { clientApi } from "@homarr/api/client";

View File

@@ -1,7 +1,7 @@
"use client"; "use client";
import { Button, Group, Select, Stack, Switch } from "@mantine/core"; import { Button, Group, Select, Stack, Switch } from "@mantine/core";
import type { z } from "zod"; import type { z } from "zod/v4";
import type { RouterOutputs } from "@homarr/api"; import type { RouterOutputs } from "@homarr/api";
import { clientApi } from "@homarr/api/client"; import { clientApi } from "@homarr/api/client";

View File

@@ -4,7 +4,7 @@ import { Button, Group, Radio, Stack } from "@mantine/core";
import type { DayOfWeek } from "@mantine/dates"; import type { DayOfWeek } from "@mantine/dates";
import dayjs from "dayjs"; import dayjs from "dayjs";
import localeData from "dayjs/plugin/localeData"; import localeData from "dayjs/plugin/localeData";
import type { z } from "zod"; import type { z } from "zod/v4";
import type { RouterOutputs } from "@homarr/api"; import type { RouterOutputs } from "@homarr/api";
import { clientApi } from "@homarr/api/client"; import { clientApi } from "@homarr/api/client";

View File

@@ -1,7 +1,7 @@
"use client"; "use client";
import { Button, Group, Stack, Switch } from "@mantine/core"; import { Button, Group, Stack, Switch } from "@mantine/core";
import type { z } from "zod"; import type { z } from "zod/v4";
import type { RouterOutputs } from "@homarr/api"; import type { RouterOutputs } from "@homarr/api";
import { clientApi } from "@homarr/api/client"; import { clientApi } from "@homarr/api/client";

View File

@@ -17,7 +17,7 @@ import {
} from "@mantine/core"; } from "@mantine/core";
import { useListState } from "@mantine/hooks"; import { useListState } from "@mantine/hooks";
import { IconPlus, IconUserCheck } from "@tabler/icons-react"; import { IconPlus, IconUserCheck } from "@tabler/icons-react";
import { z } from "zod"; import { z } from "zod/v4";
import { clientApi } from "@homarr/api/client"; import { clientApi } from "@homarr/api/client";
import { everyoneGroup, groupPermissions } from "@homarr/definitions"; import { everyoneGroup, groupPermissions } from "@homarr/definitions";

View File

@@ -1,6 +1,6 @@
import { useCallback, useRef } from "react"; import { useCallback, useRef } from "react";
import { Button, Grid, Group, NumberInput, Stack } from "@mantine/core"; import { Button, Grid, Group, NumberInput, Stack } from "@mantine/core";
import { z } from "zod"; import { z } from "zod/v4";
import { useZodForm } from "@homarr/form"; import { useZodForm } from "@homarr/form";
import type { GridStack } from "@homarr/gridstack"; import type { GridStack } from "@homarr/gridstack";

View File

@@ -1,7 +1,7 @@
"use client"; "use client";
import { Button, Group, Stack, TextInput } from "@mantine/core"; import { Button, Group, Stack, TextInput } from "@mantine/core";
import type { z } from "zod"; import type { z } from "zod/v4";
import { clientApi } from "@homarr/api/client"; import { clientApi } from "@homarr/api/client";
import { useZodForm } from "@homarr/form"; import { useZodForm } from "@homarr/form";

View File

@@ -1,5 +1,5 @@
import { Button, Group, Stack, TextInput } from "@mantine/core"; import { Button, Group, Stack, TextInput } from "@mantine/core";
import { z } from "zod"; import { z } from "zod/v4";
import { useZodForm } from "@homarr/form"; import { useZodForm } from "@homarr/form";
import { createModal } from "@homarr/modals"; import { createModal } from "@homarr/modals";

View File

@@ -1,5 +1,5 @@
import { useCallback } from "react"; import { useCallback } from "react";
import type { z } from "zod"; import type { z } from "zod/v4";
import { useUpdateBoard } from "@homarr/boards/updater"; import { useUpdateBoard } from "@homarr/boards/updater";
import type { dynamicSectionOptionsSchema } from "@homarr/validation/shared"; import type { dynamicSectionOptionsSchema } from "@homarr/validation/shared";

View File

@@ -1,7 +1,7 @@
"use client"; "use client";
import { Button, CloseButton, ColorInput, Group, Stack, TextInput, useMantineTheme } from "@mantine/core"; import { Button, CloseButton, ColorInput, Group, Stack, TextInput, useMantineTheme } from "@mantine/core";
import type { z } from "zod"; import type { z } from "zod/v4";
import { useZodForm } from "@homarr/form"; import { useZodForm } from "@homarr/form";
import { createModal } from "@homarr/modals"; import { createModal } from "@homarr/modals";

View File

@@ -74,14 +74,14 @@
"overrides": { "overrides": {
"proxmox-api>undici": "7.13.0" "proxmox-api>undici": "7.13.0"
}, },
"patchedDependencies": {
"@types/node-unifi": "patches/@types__node-unifi.patch"
},
"allowUnusedPatches": true, "allowUnusedPatches": true,
"ignoredBuiltDependencies": [ "ignoredBuiltDependencies": [
"@scarf/scarf", "@scarf/scarf",
"core-js-pure", "core-js-pure",
"protobufjs" "protobufjs"
], ]
"patchedDependencies": {
"@types/node-unifi": "patches/@types__node-unifi.patch"
}
} }
} }

View File

@@ -24,6 +24,7 @@
"@homarr/auth": "workspace:^0.1.0", "@homarr/auth": "workspace:^0.1.0",
"@homarr/certificates": "workspace:^0.1.0", "@homarr/certificates": "workspace:^0.1.0",
"@homarr/common": "workspace:^0.1.0", "@homarr/common": "workspace:^0.1.0",
"@homarr/core": "workspace:^0.1.0",
"@homarr/cron-job-api": "workspace:^0.1.0", "@homarr/cron-job-api": "workspace:^0.1.0",
"@homarr/cron-job-status": "workspace:^0.1.0", "@homarr/cron-job-status": "workspace:^0.1.0",
"@homarr/cron-jobs": "workspace:^0.1.0", "@homarr/cron-jobs": "workspace:^0.1.0",
@@ -51,8 +52,8 @@
"react": "19.1.1", "react": "19.1.1",
"react-dom": "19.1.1", "react-dom": "19.1.1",
"superjson": "2.2.2", "superjson": "2.2.2",
"trpc-to-openapi": "^2.4.0", "trpc-to-openapi": "^3.0.0",
"zod": "^3.25.76" "zod": "^4.0.14"
}, },
"devDependencies": { "devDependencies": {
"@homarr/eslint-config": "workspace:^0.2.0", "@homarr/eslint-config": "workspace:^0.2.0",

View File

@@ -1,7 +1,6 @@
import { createEnv } from "@t3-oss/env-nextjs"; import { z } from "zod/v4";
import { z } from "zod";
import { shouldSkipEnvValidation } from "@homarr/common/env-validation"; import { createEnv } from "@homarr/core/infrastructure/env";
export const env = createEnv({ export const env = createEnv({
server: { server: {
@@ -10,6 +9,4 @@ export const env = createEnv({
runtimeEnv: { runtimeEnv: {
KUBERNETES_SERVICE_ACCOUNT_NAME: process.env.KUBERNETES_SERVICE_ACCOUNT_NAME, KUBERNETES_SERVICE_ACCOUNT_NAME: process.env.KUBERNETES_SERVICE_ACCOUNT_NAME,
}, },
skipValidation: shouldSkipEnvValidation(),
emptyStringAsUndefined: true,
}); });

View File

@@ -1,5 +1,5 @@
import { TRPCError } from "@trpc/server"; import { TRPCError } from "@trpc/server";
import { z } from "zod"; import { z } from "zod/v4";
import type { Session } from "@homarr/auth"; import type { Session } from "@homarr/auth";
import { hasQueryAccessToIntegrationsAsync } from "@homarr/auth/server"; import { hasQueryAccessToIntegrationsAsync } from "@homarr/auth/server";

View File

@@ -1,5 +1,5 @@
import { TRPCError } from "@trpc/server"; import { TRPCError } from "@trpc/server";
import { z } from "zod"; import { z } from "zod/v4";
import { and, eq } from "@homarr/db"; import { and, eq } from "@homarr/db";
import { items } from "@homarr/db/schema"; import { items } from "@homarr/db/schema";

View File

@@ -1,4 +1,4 @@
import { z } from "zod"; import { z } from "zod/v4";
import { createSaltAsync, hashPasswordAsync } from "@homarr/auth"; import { createSaltAsync, hashPasswordAsync } from "@homarr/auth";
import { createId } from "@homarr/common"; import { createId } from "@homarr/common";

View File

@@ -1,5 +1,5 @@
import { TRPCError } from "@trpc/server"; import { TRPCError } from "@trpc/server";
import { z } from "zod"; import { z } from "zod/v4";
import { createId } from "@homarr/common"; import { createId } from "@homarr/common";
import { asc, eq, inArray, like } from "@homarr/db"; import { asc, eq, inArray, like } from "@homarr/db";

View File

@@ -1,6 +1,6 @@
import { TRPCError } from "@trpc/server"; import { TRPCError } from "@trpc/server";
import superjson from "superjson"; import superjson from "superjson";
import { z } from "zod"; import { z } from "zod/v4";
import { constructBoardPermissions } from "@homarr/auth/shared"; import { constructBoardPermissions } from "@homarr/auth/shared";
import { createId } from "@homarr/common"; import { createId } from "@homarr/common";
@@ -1623,7 +1623,7 @@ const getFullBoardWithWhereAsync = async (db: Database, where: SQL<unknown>, use
const forKind = <T extends WidgetKind>(kind: T) => const forKind = <T extends WidgetKind>(kind: T) =>
z.object({ z.object({
kind: z.literal(kind), kind: z.literal(kind),
options: z.record(z.unknown()), options: z.record(z.string(), z.unknown()),
}); });
const outputItemSchema = zodUnionFromArray(widgetKinds.map((kind) => forKind(kind))).and(sharedItemSchema); const outputItemSchema = zodUnionFromArray(widgetKinds.map((kind) => forKind(kind))).and(sharedItemSchema);

View File

@@ -1,13 +1,13 @@
import { X509Certificate } from "node:crypto"; import { X509Certificate } from "node:crypto";
import { TRPCError } from "@trpc/server"; import { TRPCError } from "@trpc/server";
import { z } from "zod";
import { zfd } from "zod-form-data"; import { zfd } from "zod-form-data";
import { z } from "zod/v4";
import { addCustomRootCertificateAsync, removeCustomRootCertificateAsync } from "@homarr/certificates/server"; import { addCustomRootCertificateAsync, removeCustomRootCertificateAsync } from "@homarr/certificates/server";
import { and, eq } from "@homarr/db"; import { and, eq } from "@homarr/db";
import { trustedCertificateHostnames } from "@homarr/db/schema"; import { trustedCertificateHostnames } from "@homarr/db/schema";
import { logger } from "@homarr/log"; import { logger } from "@homarr/log";
import { certificateValidFileNameSchema, superRefineCertificateFile } from "@homarr/validation/certificates"; import { certificateValidFileNameSchema, checkCertificateFile } from "@homarr/validation/certificates";
import { createTRPCRouter, permissionRequiredProcedure } from "../../trpc"; import { createTRPCRouter, permissionRequiredProcedure } from "../../trpc";
@@ -16,7 +16,7 @@ export const certificateRouter = createTRPCRouter({
.requiresPermission("admin") .requiresPermission("admin")
.input( .input(
zfd.formData({ zfd.formData({
file: zfd.file().superRefine(superRefineCertificateFile), file: zfd.file().check(checkCertificateFile),
}), }),
) )
.mutation(async ({ input }) => { .mutation(async ({ input }) => {

View File

@@ -1,6 +1,6 @@
import { TRPCError } from "@trpc/server"; import { TRPCError } from "@trpc/server";
import { observable } from "@trpc/server/observable"; import { observable } from "@trpc/server/observable";
import { z } from "zod"; import { z } from "zod/v4";
import type { Container, ContainerState, Docker, Port } from "@homarr/docker"; import type { Container, ContainerState, Docker, Port } from "@homarr/docker";
import { DockerSingleton } from "@homarr/docker"; import { DockerSingleton } from "@homarr/docker";

View File

@@ -1,5 +1,5 @@
import { TRPCError } from "@trpc/server"; import { TRPCError } from "@trpc/server";
import { z } from "zod"; import { z } from "zod/v4";
import { createId } from "@homarr/common"; import { createId } from "@homarr/common";
import type { Database } from "@homarr/db"; import type { Database } from "@homarr/db";

View File

@@ -1,4 +1,4 @@
import { z } from "zod"; import { z } from "zod/v4";
import { analyseOldmarrImportForRouterAsync, analyseOldmarrImportInputSchema } from "@homarr/old-import/analyse"; import { analyseOldmarrImportForRouterAsync, analyseOldmarrImportInputSchema } from "@homarr/old-import/analyse";
import { import {

View File

@@ -1,4 +1,4 @@
import z from "zod"; import z from "zod/v4";
import packageJson from "../../../../package.json"; import packageJson from "../../../../package.json";
import { createTRPCRouter, protectedProcedure } from "../trpc"; import { createTRPCRouter, protectedProcedure } from "../trpc";

View File

@@ -1,5 +1,5 @@
import { TRPCError } from "@trpc/server"; import { TRPCError } from "@trpc/server";
import { z } from "zod"; import { z } from "zod/v4";
import { createId, objectEntries } from "@homarr/common"; import { createId, objectEntries } from "@homarr/common";
import { decryptSecret, encryptSecret } from "@homarr/common/server"; import { decryptSecret, encryptSecret } from "@homarr/common/server";

View File

@@ -1,6 +1,6 @@
import { randomBytes } from "crypto"; import { randomBytes } from "crypto";
import { TRPCError } from "@trpc/server"; import { TRPCError } from "@trpc/server";
import { z } from "zod"; import { z } from "zod/v4";
import { createId } from "@homarr/common"; import { createId } from "@homarr/common";
import { asc, eq } from "@homarr/db"; import { asc, eq } from "@homarr/db";

View File

@@ -1,4 +1,4 @@
import { z } from "zod"; import { z } from "zod/v4";
import { fetchWithTimeout } from "@homarr/common"; import { fetchWithTimeout } from "@homarr/common";

View File

@@ -1,5 +1,5 @@
import { observable } from "@trpc/server/observable"; import { observable } from "@trpc/server/observable";
import z from "zod"; import z from "zod/v4";
import { logger } from "@homarr/log"; import { logger } from "@homarr/log";
import { logLevels } from "@homarr/log/constants"; import { logLevels } from "@homarr/log/constants";

View File

@@ -1,5 +1,5 @@
import { TRPCError } from "@trpc/server"; import { TRPCError } from "@trpc/server";
import { z } from "zod"; import { z } from "zod/v4";
import { createId } from "@homarr/common"; import { createId } from "@homarr/common";
import type { InferInsertModel } from "@homarr/db"; import type { InferInsertModel } from "@homarr/db";

View File

@@ -1,4 +1,4 @@
import { z } from "zod"; import { z } from "zod/v4";
import { onboarding } from "@homarr/db/schema"; import { onboarding } from "@homarr/db/schema";
import { onboardingSteps } from "@homarr/definitions"; import { onboardingSteps } from "@homarr/definitions";

View File

@@ -1,5 +1,5 @@
import { TRPCError } from "@trpc/server"; import { TRPCError } from "@trpc/server";
import { z } from "zod"; import { z } from "zod/v4";
import { createId } from "@homarr/common"; import { createId } from "@homarr/common";
import { asc, eq, like } from "@homarr/db"; import { asc, eq, like } from "@homarr/db";

View File

@@ -1,5 +1,5 @@
import { TRPCError } from "@trpc/server"; import { TRPCError } from "@trpc/server";
import { z } from "zod"; import { z } from "zod/v4";
import { and, eq } from "@homarr/db"; import { and, eq } from "@homarr/db";
import { sectionCollapseStates, sections } from "@homarr/db/schema"; import { sectionCollapseStates, sections } from "@homarr/db/schema";

View File

@@ -1,4 +1,4 @@
import { z } from "zod"; import { z } from "zod/v4";
import { getServerSettingByKeyAsync, getServerSettingsAsync, updateServerSettingByKeyAsync } from "@homarr/db/queries"; import { getServerSettingByKeyAsync, getServerSettingsAsync, updateServerSettingByKeyAsync } from "@homarr/db/queries";
import type { ServerSettings } from "@homarr/server-settings"; import type { ServerSettings } from "@homarr/server-settings";

View File

@@ -1,5 +1,5 @@
import { TRPCError } from "@trpc/server"; import { TRPCError } from "@trpc/server";
import { z } from "zod"; import { z } from "zod/v4";
import { createSaltAsync, hashPasswordAsync } from "@homarr/auth"; import { createSaltAsync, hashPasswordAsync } from "@homarr/auth";
import { createId } from "@homarr/common"; import { createId } from "@homarr/common";

View File

@@ -1,5 +1,5 @@
import { TRPCError } from "@trpc/server"; import { TRPCError } from "@trpc/server";
import { z } from "zod"; import { z } from "zod/v4";
import type { Session } from "@homarr/auth"; import type { Session } from "@homarr/auth";
import type { Modify } from "@homarr/common/types"; import type { Modify } from "@homarr/common/types";

View File

@@ -1,5 +1,5 @@
import { observable } from "@trpc/server/observable"; import { observable } from "@trpc/server/observable";
import { z } from "zod"; import { z } from "zod/v4";
import { sendPingRequestAsync } from "@homarr/ping"; import { sendPingRequestAsync } from "@homarr/ping";
import { pingChannel, pingUrlChannel } from "@homarr/redis"; import { pingChannel, pingUrlChannel } from "@homarr/redis";

View File

@@ -1,4 +1,4 @@
import { z } from "zod"; import { z } from "zod/v4";
import { getIntegrationKindsByCategory } from "@homarr/definitions"; import { getIntegrationKindsByCategory } from "@homarr/definitions";
import { radarrReleaseTypes } from "@homarr/integrations/types"; import { radarrReleaseTypes } from "@homarr/integrations/types";

View File

@@ -1,5 +1,5 @@
import { observable } from "@trpc/server/observable"; import { observable } from "@trpc/server/observable";
import { z } from "zod"; import { z } from "zod/v4";
import type { Modify } from "@homarr/common/types"; import type { Modify } from "@homarr/common/types";
import type { Integration } from "@homarr/db/schema"; import type { Integration } from "@homarr/db/schema";

View File

@@ -1,5 +1,5 @@
import { observable } from "@trpc/server/observable"; import { observable } from "@trpc/server/observable";
import { z } from "zod"; import { z } from "zod/v4";
import type { Modify } from "@homarr/common/types"; import type { Modify } from "@homarr/common/types";
import type { Integration } from "@homarr/db/schema"; import type { Integration } from "@homarr/db/schema";

View File

@@ -1,5 +1,5 @@
import { observable } from "@trpc/server/observable"; import { observable } from "@trpc/server/observable";
import { z } from "zod"; import { z } from "zod/v4";
import { getIntegrationKindsByCategory } from "@homarr/definitions"; import { getIntegrationKindsByCategory } from "@homarr/definitions";
import { createIntegrationAsync } from "@homarr/integrations"; import { createIntegrationAsync } from "@homarr/integrations";

View File

@@ -1,5 +1,5 @@
import { observable } from "@trpc/server/observable"; import { observable } from "@trpc/server/observable";
import { z } from "zod"; import { z } from "zod/v4";
import { getIntegrationKindsByCategory } from "@homarr/definitions"; import { getIntegrationKindsByCategory } from "@homarr/definitions";
import type { StreamSession } from "@homarr/integrations"; import type { StreamSession } from "@homarr/integrations";

View File

@@ -1,5 +1,5 @@
import { observable } from "@trpc/server/observable"; import { observable } from "@trpc/server/observable";
import { z } from "zod"; import { z } from "zod/v4";
import type { MinecraftServerStatus } from "@homarr/request-handler/minecraft-server-status"; import type { MinecraftServerStatus } from "@homarr/request-handler/minecraft-server-status";
import { minecraftServerStatusRequestHandler } from "@homarr/request-handler/minecraft-server-status"; import { minecraftServerStatusRequestHandler } from "@homarr/request-handler/minecraft-server-status";

View File

@@ -1,6 +1,6 @@
import { TRPCError } from "@trpc/server"; import { TRPCError } from "@trpc/server";
import SuperJSON from "superjson"; import SuperJSON from "superjson";
import { z } from "zod"; import { z } from "zod/v4";
import { eq } from "@homarr/db"; import { eq } from "@homarr/db";
import { boards, items } from "@homarr/db/schema"; import { boards, items } from "@homarr/db/schema";

View File

@@ -1,5 +1,5 @@
import { escapeForRegEx } from "@tiptap/react"; import { escapeForRegEx } from "@tiptap/react";
import { z } from "zod"; import { z } from "zod/v4";
import { getIntegrationKindsByCategory } from "@homarr/definitions"; import { getIntegrationKindsByCategory } from "@homarr/definitions";
import { releasesRequestHandler } from "@homarr/request-handler/releases"; import { releasesRequestHandler } from "@homarr/request-handler/releases";

View File

@@ -1,4 +1,4 @@
import { z } from "zod"; import { z } from "zod/v4";
import { rssFeedsRequestHandler } from "@homarr/request-handler/rss-feeds"; import { rssFeedsRequestHandler } from "@homarr/request-handler/rss-feeds";

View File

@@ -1,5 +1,5 @@
import { observable } from "@trpc/server/observable"; import { observable } from "@trpc/server/observable";
import { z } from "zod"; import { z } from "zod/v4";
import { getIntegrationKindsByCategory } from "@homarr/definitions"; import { getIntegrationKindsByCategory } from "@homarr/definitions";
import { createIntegrationAsync } from "@homarr/integrations"; import { createIntegrationAsync } from "@homarr/integrations";

View File

@@ -1,4 +1,4 @@
import { z } from "zod"; import { z } from "zod/v4";
import { fetchStockPriceHandler } from "@homarr/request-handler/stock-price"; import { fetchStockPriceHandler } from "@homarr/request-handler/stock-price";

View File

@@ -1,4 +1,4 @@
import { z } from "zod"; import { z } from "zod/v4";
import { fetchWithTimeout } from "@homarr/common"; import { fetchWithTimeout } from "@homarr/common";

View File

@@ -1,21 +1,20 @@
import { z } from "zod"; import { z } from "zod/v4";
import type { AnyZodObject, ZodIntersection, ZodObject } from "zod"; import type { ZodIntersection, ZodObject } from "zod/v4";
export function convertIntersectionToZodObject<TIntersection extends ZodIntersection<AnyZodObject, AnyZodObject>>( export function convertIntersectionToZodObject<TIntersection extends ZodIntersection<ZodObject, ZodObject>>(
intersection: TIntersection, intersection: TIntersection,
) { ) {
const { _def } = intersection; const left = intersection.def.left;
const right = intersection.def.right;
// Merge the shapes // Merge the shapes
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment const mergedShape = { ...left.def.shape, ...right.def.shape };
const mergedShape = { ..._def.left.shape, ..._def.right.shape };
// Return a new ZodObject // Return a new ZodObject
return z.object(mergedShape) as unknown as TIntersection extends ZodIntersection<infer TLeft, infer TRight> return z.object(mergedShape) as unknown as TIntersection extends ZodIntersection<infer TLeft, infer TRight>
? TLeft extends AnyZodObject ? TLeft extends ZodObject
? TRight extends AnyZodObject ? TRight extends ZodObject
? // eslint-disable-next-line @typescript-eslint/no-explicit-any ? ZodObject<TLeft["shape"] & TRight["shape"]>
ZodObject<TLeft["shape"] & TRight["shape"], any, any, z.infer<TLeft> & z.infer<TRight>>
: never : never
: never : never
: never; : never;

View File

@@ -0,0 +1,23 @@
import { describe, expect, test } from "vitest";
import z from "zod/v4";
import { convertIntersectionToZodObject } from "../schema-merger";
describe("convertIntersectionToZodObject should convert zod intersection to zod object", () => {
test("should merge two ZodObjects with different properties", () => {
const objectA = z.object({
id: z.string(),
});
const objectB = z.object({
name: z.string(),
});
const intersection = objectA.and(objectB);
const result = convertIntersectionToZodObject(intersection);
expect(result.def.type).toBe("object");
expect(result.shape).toHaveProperty("id");
expect(result.shape).toHaveProperty("name");
});
});

View File

@@ -9,7 +9,7 @@
import { initTRPC, TRPCError } from "@trpc/server"; import { initTRPC, TRPCError } from "@trpc/server";
import superjson from "superjson"; import superjson from "superjson";
import type { OpenApiMeta } from "trpc-to-openapi"; import type { OpenApiMeta } from "trpc-to-openapi";
import { ZodError } from "zod"; import { ZodError } from "zod/v4";
import type { Session } from "@homarr/auth"; import type { Session } from "@homarr/auth";
import { FlattenError } from "@homarr/common"; import { FlattenError } from "@homarr/common";

View File

@@ -1,4 +1,4 @@
import { z } from "zod"; import { z } from "zod/v4";
import { createBooleanSchema, createDurationSchema, createEnv } from "@homarr/core/infrastructure/env"; import { createBooleanSchema, createDurationSchema, createEnv } from "@homarr/core/infrastructure/env";
import { supportedAuthProviders } from "@homarr/definitions"; import { supportedAuthProviders } from "@homarr/definitions";
@@ -19,7 +19,7 @@ const authProvidersSchema = z
return false; return false;
}), }),
) )
.default("credentials"); .default(["credentials"]);
const authProviders = authProvidersSchema.safeParse(process.env.AUTH_PROVIDERS).data ?? []; const authProviders = authProvidersSchema.safeParse(process.env.AUTH_PROVIDERS).data ?? [];

View File

@@ -39,7 +39,7 @@
"next-auth": "5.0.0-beta.29", "next-auth": "5.0.0-beta.29",
"react": "19.1.1", "react": "19.1.1",
"react-dom": "19.1.1", "react-dom": "19.1.1",
"zod": "^3.25.76" "zod": "^4.0.14"
}, },
"devDependencies": { "devDependencies": {
"@homarr/eslint-config": "workspace:^0.2.0", "@homarr/eslint-config": "workspace:^0.2.0",

View File

@@ -1,5 +1,5 @@
import bcrypt from "bcrypt"; import bcrypt from "bcrypt";
import type { z } from "zod"; import type { z } from "zod/v4";
import type { Database } from "@homarr/db"; import type { Database } from "@homarr/db";
import { and, eq } from "@homarr/db"; import { and, eq } from "@homarr/db";

View File

@@ -1,5 +1,5 @@
import { CredentialsSignin } from "@auth/core/errors"; import { CredentialsSignin } from "@auth/core/errors";
import { z } from "zod"; import { z } from "zod/v4";
import { createId, extractErrorMessage } from "@homarr/common"; import { createId, extractErrorMessage } from "@homarr/common";
import type { Database, InferInsertModel } from "@homarr/db"; import type { Database, InferInsertModel } from "@homarr/db";

View File

@@ -1,5 +1,5 @@
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import { z } from "zod"; import { z } from "zod/v4";
import { expireDateAfter, generateSessionToken } from "../session"; import { expireDateAfter, generateSessionToken } from "../session";

View File

@@ -25,7 +25,7 @@ export const recreateAdmin = command({
if (!result.success) { if (!result.success) {
console.error("Invalid username:"); console.error("Invalid username:");
console.error(result.error.errors.map((error) => `- ${error.message}`).join("\n")); console.error(result.error.issues.map((error) => `- ${error.message}`).join("\n"));
return; return;
} }

View File

@@ -1,5 +1,5 @@
import { randomBytes } from "crypto"; import { randomBytes } from "crypto";
import { z } from "zod"; import { z } from "zod/v4";
import { createEnv } from "@homarr/core/infrastructure/env"; import { createEnv } from "@homarr/core/infrastructure/env";
@@ -12,7 +12,7 @@ export const env = createEnv({
server: { server: {
SECRET_ENCRYPTION_KEY: z SECRET_ENCRYPTION_KEY: z
.string({ .string({
required_error: `SECRET_ENCRYPTION_KEY is required${errorSuffix}`, error: `SECRET_ENCRYPTION_KEY is required${errorSuffix}`,
}) })
.min(64, { .min(64, {
message: `SECRET_ENCRYPTION_KEY has to be 64 characters${errorSuffix}`, message: `SECRET_ENCRYPTION_KEY has to be 64 characters${errorSuffix}`,

View File

@@ -9,8 +9,7 @@
"./types": "./src/types.ts", "./types": "./src/types.ts",
"./server": "./src/server.ts", "./server": "./src/server.ts",
"./client": "./src/client.ts", "./client": "./src/client.ts",
"./env": "./env.ts", "./env": "./env.ts"
"./env-validation": "./src/env-validation.ts"
}, },
"typesVersions": { "typesVersions": {
"*": { "*": {
@@ -35,7 +34,7 @@
"react": "19.1.1", "react": "19.1.1",
"react-dom": "19.1.1", "react-dom": "19.1.1",
"undici": "7.13.0", "undici": "7.13.0",
"zod": "^3.25.76", "zod": "^4.0.14",
"zod-validation-error": "^3.5.3" "zod-validation-error": "^3.5.3"
}, },
"devDependencies": { "devDependencies": {

View File

@@ -1,42 +0,0 @@
import { z } from "zod";
const trueStrings = ["1", "yes", "t", "true"];
const falseStrings = ["0", "no", "f", "false"];
export const createBooleanSchema = (defaultValue: boolean) =>
z
.string()
.default(defaultValue.toString())
.transform((value, ctx) => {
const normalized = value.trim().toLowerCase();
if (trueStrings.includes(normalized)) return true;
if (falseStrings.includes(normalized)) return false;
throw new Error(`Invalid boolean value for ${ctx.path.join(".")}`);
});
export const createDurationSchema = (defaultValue: `${number}${"s" | "m" | "h" | "d"}`) =>
z
.string()
.regex(/^\d+[smhd]?$/)
.default(defaultValue)
.transform((duration) => {
const lastChar = duration[duration.length - 1] as "s" | "m" | "h" | "d";
if (!isNaN(Number(lastChar))) {
return Number(defaultValue);
}
const multipliers = {
s: 1,
m: 60,
h: 60 * 60,
d: 60 * 60 * 24,
};
const numberDuration = Number(duration.slice(0, -1));
const multiplier = multipliers[lastChar];
return numberDuration * multiplier;
});
export const shouldSkipEnvValidation = () =>
Boolean(process.env.CI) || Boolean(process.env.SKIP_ENV_VALIDATION) || process.env.npm_lifecycle_event === "lint";

View File

@@ -1,5 +1,5 @@
import { ZodError } from "zod";
import { fromError } from "zod-validation-error"; import { fromError } from "zod-validation-error";
import { ZodError } from "zod/v4";
import { logger } from "@homarr/log"; import { logger } from "@homarr/log";

View File

@@ -1,4 +1,4 @@
import type { z } from "zod"; import type { z } from "zod/v4";
export type MaybePromise<T> = T | Promise<T>; export type MaybePromise<T> = T | Promise<T>;
@@ -19,7 +19,7 @@ export type Inverse<T extends Invertible> = {
type Invertible = Record<PropertyKey, PropertyKey>; type Invertible = Record<PropertyKey, PropertyKey>;
export type inferSearchParamsFromSchema<TSchema extends z.AnyZodObject> = inferSearchParamsFromSchemaInner< export type inferSearchParamsFromSchema<TSchema extends z.ZodObject> = inferSearchParamsFromSchemaInner<
z.infer<TSchema> z.infer<TSchema>
>; >;

View File

@@ -26,7 +26,7 @@
"dependencies": { "dependencies": {
"@t3-oss/env-nextjs": "^0.13.8", "@t3-oss/env-nextjs": "^0.13.8",
"ioredis": "5.7.0", "ioredis": "5.7.0",
"zod": "^3.25.76" "zod": "^4.0.14"
}, },
"devDependencies": { "devDependencies": {
"@homarr/eslint-config": "workspace:^0.2.0", "@homarr/eslint-config": "workspace:^0.2.0",

View File

@@ -1,19 +1,16 @@
import { z } from "zod"; import { z } from "zod/v4";
const trueStrings = ["1", "yes", "t", "true"]; const trueStrings = ["1", "yes", "t", "true"];
const falseStrings = ["0", "no", "f", "false"]; const falseStrings = ["0", "no", "f", "false"];
export const createBooleanSchema = (defaultValue: boolean) => export const createBooleanSchema = (defaultValue: boolean) =>
z z
.string() .stringbool({
.default(defaultValue.toString()) truthy: trueStrings,
.transform((value, ctx) => { falsy: falseStrings,
const normalized = value.trim().toLowerCase(); case: "insensitive",
if (trueStrings.includes(normalized)) return true; })
if (falseStrings.includes(normalized)) return false; .default(defaultValue);
throw new Error(`Invalid boolean value for ${ctx.path.join(".")}`);
});
export const createDurationSchema = (defaultValue: `${number}${"s" | "m" | "h" | "d"}`) => export const createDurationSchema = (defaultValue: `${number}${"s" | "m" | "h" | "d"}`) =>
z z

View File

@@ -35,7 +35,7 @@
"@trpc/tanstack-react-query": "^11.4.4", "@trpc/tanstack-react-query": "^11.4.4",
"node-cron": "^4.2.1", "node-cron": "^4.2.1",
"react": "19.1.1", "react": "19.1.1",
"zod": "^3.25.76" "zod": "^4.0.14"
}, },
"devDependencies": { "devDependencies": {
"@homarr/eslint-config": "workspace:^0.2.0", "@homarr/eslint-config": "workspace:^0.2.0",

View File

@@ -1,4 +1,4 @@
import { z } from "zod"; import { z } from "zod/v4";
import { env as commonEnv } from "@homarr/common/env"; import { env as commonEnv } from "@homarr/common/env";
import { createEnv } from "@homarr/core/infrastructure/env"; import { createEnv } from "@homarr/core/infrastructure/env";
@@ -42,7 +42,7 @@ export const env = createEnv({
.regex(/\d+/) .regex(/\d+/)
.transform(Number) .transform(Number)
.refine((number) => number >= 1) .refine((number) => number >= 1)
.default("3306"), .default(3306),
DB_USER: z.string(), DB_USER: z.string(),
DB_PASSWORD: z.string(), DB_PASSWORD: z.string(),
DB_NAME: z.string(), DB_NAME: z.string(),

View File

@@ -51,7 +51,7 @@
"dotenv": "^17.2.1", "dotenv": "^17.2.1",
"drizzle-kit": "^0.31.4", "drizzle-kit": "^0.31.4",
"drizzle-orm": "^0.44.4", "drizzle-orm": "^0.44.4",
"drizzle-zod": "^0.7.1", "drizzle-zod": "^0.8.2",
"mysql2": "3.14.3", "mysql2": "3.14.3",
"superjson": "2.2.2" "superjson": "2.2.2"
}, },

View File

@@ -25,7 +25,7 @@
"dependencies": { "dependencies": {
"@homarr/common": "workspace:^0.1.0", "@homarr/common": "workspace:^0.1.0",
"fast-xml-parser": "^5.2.5", "fast-xml-parser": "^5.2.5",
"zod": "^3.25.76" "zod": "^4.0.14"
}, },
"devDependencies": { "devDependencies": {
"@homarr/eslint-config": "workspace:^0.2.0", "@homarr/eslint-config": "workspace:^0.2.0",

View File

@@ -2,7 +2,7 @@ import fs from "node:fs/promises";
import path, { dirname } from "node:path"; import path, { dirname } from "node:path";
import { fileURLToPath } from "node:url"; import { fileURLToPath } from "node:url";
import { XMLParser } from "fast-xml-parser"; import { XMLParser } from "fast-xml-parser";
import { z } from "zod"; import { z } from "zod/v4";
import { createDocumentationLink } from "./index"; import { createDocumentationLink } from "./index";

View File

@@ -1,4 +1,4 @@
import { z } from "zod"; import { z } from "zod/v4";
import { createBooleanSchema, createEnv } from "@homarr/core/infrastructure/env"; import { createBooleanSchema, createEnv } from "@homarr/core/infrastructure/env";

View File

@@ -27,7 +27,8 @@
"@homarr/translation": "workspace:^0.1.0", "@homarr/translation": "workspace:^0.1.0",
"@homarr/validation": "workspace:^0.1.0", "@homarr/validation": "workspace:^0.1.0",
"@mantine/form": "^8.2.4", "@mantine/form": "^8.2.4",
"zod": "^3.25.76" "mantine-form-zod-resolver": "^1.2.1",
"zod": "^4.0.14"
}, },
"devDependencies": { "devDependencies": {
"@homarr/eslint-config": "workspace:^0.2.0", "@homarr/eslint-config": "workspace:^0.2.0",

View File

@@ -1,29 +1,39 @@
import { useForm, zodResolver } from "@mantine/form"; import { useForm } from "@mantine/form";
import { z } from "zod"; import { zod4Resolver } from "mantine-form-zod-resolver";
import type { AnyZodObject, ZodDiscriminatedUnion, ZodEffects, ZodIntersection } from "zod"; import type { ZodDiscriminatedUnion, ZodIntersection, ZodObject, ZodPipe } from "zod/v4";
import { z } from "zod/v4";
import { useI18n } from "@homarr/translation/client"; import { useI18n } from "@homarr/translation/client";
import { zodErrorMap } from "@homarr/validation/form/i18n"; import { zodErrorMap } from "@homarr/validation/form/i18n";
type inferPossibleSchema<
TSchema extends
| ZodObject
| ZodPipe<ZodObject>
| ZodIntersection<ZodObject | ZodDiscriminatedUnion<ZodObject[]>, ZodObject>,
> = z.infer<TSchema> extends Record<string, unknown> ? z.infer<TSchema> : never;
export const useZodForm = < export const useZodForm = <
TSchema extends TSchema extends
| AnyZodObject | ZodObject
| ZodEffects<AnyZodObject> | ZodPipe<ZodObject>
| ZodIntersection<AnyZodObject | ZodDiscriminatedUnion<string, AnyZodObject[]>, AnyZodObject>, | ZodIntersection<ZodObject | ZodDiscriminatedUnion<ZodObject[]>, ZodObject>,
>( >(
schema: TSchema, schema: TSchema,
options: Omit< options: Omit<
Exclude<Parameters<typeof useForm<z.infer<TSchema>>>[0], undefined>, Exclude<Parameters<typeof useForm<inferPossibleSchema<TSchema>>>[0], undefined>,
"validate" | "validateInputOnBlur" | "validateInputOnChange" "validate" | "validateInputOnBlur" | "validateInputOnChange"
>, >,
) => { ) => {
const t = useI18n(); const t = useI18n();
z.setErrorMap(zodErrorMap(t)); z.config({
return useForm<z.infer<TSchema>>({ customError: zodErrorMap(t),
});
return useForm<inferPossibleSchema<TSchema>>({
...options, ...options,
validateInputOnBlur: true, validateInputOnBlur: true,
validateInputOnChange: true, validateInputOnChange: true,
validate: zodResolver(schema), validate: zod4Resolver(schema),
}); });
}; };

View File

@@ -1,103 +0,0 @@
import type { ErrorMapCtx, z, ZodTooBigIssue, ZodTooSmallIssue } from "zod";
import { ZodIssueCode } from "zod";
import type { TranslationObject } from "@homarr/translation";
const handleStringError = (issue: z.ZodInvalidStringIssue) => {
if (typeof issue.validation === "object") {
// Check if object contains startsWith / endsWith key to determine the error type. If not, it's an includes error. (see type of issue.validation)
if ("startsWith" in issue.validation) {
return {
key: "errors.string.startsWith",
params: {
startsWith: issue.validation.startsWith,
},
} as const;
} else if ("endsWith" in issue.validation) {
return {
key: "errors.string.endsWith",
params: {
endsWith: issue.validation.endsWith,
},
} as const;
}
return {
key: "errors.invalid_string.includes",
params: {
includes: issue.validation.includes,
},
} as const;
}
return {
message: issue.message,
};
};
const handleTooSmallError = (issue: ZodTooSmallIssue) => {
if (issue.type !== "string" && issue.type !== "number") {
return {
message: issue.message,
};
}
return {
key: `errors.tooSmall.${issue.type}`,
params: {
minimum: issue.minimum,
count: issue.minimum,
},
} as const;
};
const handleTooBigError = (issue: ZodTooBigIssue) => {
if (issue.type !== "string" && issue.type !== "number") {
return {
message: issue.message,
};
}
return {
key: `errors.tooBig.${issue.type}`,
params: {
maximum: issue.maximum,
count: issue.maximum,
},
} as const;
};
export const handleZodError = (issue: z.ZodIssueOptionalMessage, ctx: ErrorMapCtx) => {
if (ctx.defaultError === "Required") {
return {
key: "errors.required",
params: {},
} as const;
}
if (issue.code === ZodIssueCode.invalid_string) {
return handleStringError(issue);
}
if (issue.code === ZodIssueCode.too_small) {
return handleTooSmallError(issue);
}
if (issue.code === ZodIssueCode.too_big) {
return handleTooBigError(issue);
}
if (issue.code === ZodIssueCode.custom && issue.params?.i18n) {
const { i18n } = issue.params as CustomErrorParams;
return {
key: `errors.custom.${i18n.key}`,
} as const;
}
return {
message: issue.message,
};
};
export interface CustomErrorParams {
i18n: {
key: keyof TranslationObject["common"]["zod"]["errors"]["custom"];
params?: Record<string, unknown>;
};
}

View File

@@ -31,7 +31,7 @@
"@homarr/validation": "workspace:^0.1.0", "@homarr/validation": "workspace:^0.1.0",
"@mantine/core": "^8.2.4", "@mantine/core": "^8.2.4",
"react": "19.1.1", "react": "19.1.1",
"zod": "^3.25.76" "zod": "^4.0.14"
}, },
"devDependencies": { "devDependencies": {
"@homarr/eslint-config": "workspace:^0.2.0", "@homarr/eslint-config": "workspace:^0.2.0",

View File

@@ -2,7 +2,7 @@
import { useCallback } from "react"; import { useCallback } from "react";
import { useRouter } from "next/navigation"; import { useRouter } from "next/navigation";
import type { z } from "zod"; import type { z } from "zod/v4";
import { clientApi } from "@homarr/api/client"; import { clientApi } from "@homarr/api/client";
import { revalidatePathActionAsync } from "@homarr/common/client"; import { revalidatePathActionAsync } from "@homarr/common/client";

View File

@@ -5,7 +5,7 @@ import { useEffect, useRef } from "react";
import Link from "next/link"; import Link from "next/link";
import { Button, Checkbox, Collapse, Group, Stack, Textarea, TextInput } from "@mantine/core"; import { Button, Checkbox, Collapse, Group, Stack, Textarea, TextInput } from "@mantine/core";
import { useDebouncedValue, useDisclosure } from "@mantine/hooks"; import { useDebouncedValue, useDisclosure } from "@mantine/hooks";
import type { z } from "zod"; import type { z } from "zod/v4";
import { clientApi } from "@homarr/api/client"; import { clientApi } from "@homarr/api/client";
import { useZodForm } from "@homarr/form"; import { useZodForm } from "@homarr/form";

View File

@@ -47,7 +47,7 @@
"tsdav": "^2.1.5", "tsdav": "^2.1.5",
"undici": "7.13.0", "undici": "7.13.0",
"xml2js": "^0.6.2", "xml2js": "^0.6.2",
"zod": "^3.25.76" "zod": "^4.0.14"
}, },
"devDependencies": { "devDependencies": {
"@homarr/eslint-config": "workspace:^0.2.0", "@homarr/eslint-config": "workspace:^0.2.0",

View File

@@ -1,4 +1,4 @@
import { z } from "zod"; import { z } from "zod/v4";
export const statsResponseSchema = z.object({ export const statsResponseSchema = z.object({
time_units: z.enum(["hours", "days"]), time_units: z.enum(["hours", "days"]),

View File

@@ -1,4 +1,4 @@
import { z } from "zod"; import { z } from "zod/v4";
export const releasesResponseSchema = z.array( export const releasesResponseSchema = z.array(
z.object({ z.object({

View File

@@ -3,7 +3,7 @@ import { humanFileSize } from "@homarr/common";
import "@homarr/redis"; import "@homarr/redis";
import dayjs from "dayjs"; import dayjs from "dayjs";
import { z } from "zod"; import { z } from "zod/v4";
import { fetchWithTrustedCertificatesAsync } from "@homarr/certificates/server"; import { fetchWithTrustedCertificatesAsync } from "@homarr/certificates/server";

View File

@@ -1,4 +1,4 @@
import { z } from "zod"; import { z } from "zod/v4";
export const accessTokenResponseSchema = z.object({ export const accessTokenResponseSchema = z.object({
access_token: z.string(), access_token: z.string(),

View File

@@ -1,4 +1,4 @@
import { z } from "zod"; import { z } from "zod/v4";
export const queueSchema = z.object({ export const queueSchema = z.object({
queue: z.object({ queue: z.object({

View File

@@ -1,5 +1,5 @@
import { BaseItemKind } from "@jellyfin/sdk/lib/generated-client/models"; import { BaseItemKind } from "@jellyfin/sdk/lib/generated-client/models";
import { z } from "zod"; import { z } from "zod/v4";
import { fetchWithTrustedCertificatesAsync } from "@homarr/certificates/server"; import { fetchWithTrustedCertificatesAsync } from "@homarr/certificates/server";
import { ResponseError } from "@homarr/common/server"; import { ResponseError } from "@homarr/common/server";

View File

@@ -1,7 +1,8 @@
import { z } from "zod"; import { z } from "zod/v4";
export const entityStateSchema = z.object({ export const entityStateSchema = z.object({
attributes: z.record( attributes: z.record(
z.string(),
z.union([z.string(), z.number(), z.boolean(), z.null(), z.array(z.union([z.string(), z.number()]))]), z.union([z.string(), z.number(), z.boolean(), z.null(), z.array(z.union([z.string(), z.number()]))]),
), ),
entity_id: z.string(), entity_id: z.string(),

Some files were not shown because too many files have changed in this diff Show More