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/"],
enabled: false,
},
{
matchPackagePatterns: ["^zod$", "^drizzle-zod$", "^zod-form-data$"],
enabled: false,
},
{
matchUpdateTypes: ["minor", "patch", "pin", "digest"],
automerge: true,

View File

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

View File

@@ -2,7 +2,7 @@
import { useRouter } from "next/navigation";
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 { useZodForm } from "@homarr/form";

View File

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

View File

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

View File

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

View File

@@ -1,7 +1,7 @@
"use client";
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 { signIn } from "@homarr/auth/client";

View File

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

View File

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

View File

@@ -4,7 +4,7 @@ import { useState } from "react";
import Link from "next/link";
import { useRouter } from "next/navigation";
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 { clientApi } from "@homarr/api/client";

View File

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

View File

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

View File

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

View File

@@ -4,7 +4,7 @@ import Link from "next/link";
import type { SegmentedControlItem } 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 type { z } from "zod";
import type { z } from "zod/v4";
import { clientApi } from "@homarr/api/client";
import { searchEngineTypes } from "@homarr/definitions";

View File

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

View File

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

View File

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

View File

@@ -1,7 +1,7 @@
"use client";
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 { clientApi } from "@homarr/api/client";

View File

@@ -1,7 +1,7 @@
"use client";
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 { 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 dayjs from "dayjs";
import localeData from "dayjs/plugin/localeData";
import type { z } from "zod";
import type { z } from "zod/v4";
import type { RouterOutputs } from "@homarr/api";
import { clientApi } from "@homarr/api/client";

View File

@@ -1,7 +1,7 @@
"use client";
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 { clientApi } from "@homarr/api/client";

View File

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

View File

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

View File

@@ -1,7 +1,7 @@
"use client";
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 { useZodForm } from "@homarr/form";

View File

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

View File

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

View File

@@ -1,7 +1,7 @@
"use client";
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 { createModal } from "@homarr/modals";

View File

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

View File

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

View File

@@ -1,7 +1,6 @@
import { createEnv } from "@t3-oss/env-nextjs";
import { z } from "zod";
import { z } from "zod/v4";
import { shouldSkipEnvValidation } from "@homarr/common/env-validation";
import { createEnv } from "@homarr/core/infrastructure/env";
export const env = createEnv({
server: {
@@ -10,6 +9,4 @@ export const env = createEnv({
runtimeEnv: {
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 { z } from "zod";
import { z } from "zod/v4";
import type { Session } from "@homarr/auth";
import { hasQueryAccessToIntegrationsAsync } from "@homarr/auth/server";

View File

@@ -1,5 +1,5 @@
import { TRPCError } from "@trpc/server";
import { z } from "zod";
import { z } from "zod/v4";
import { and, eq } from "@homarr/db";
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 { createId } from "@homarr/common";

View File

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

View File

@@ -1,6 +1,6 @@
import { TRPCError } from "@trpc/server";
import superjson from "superjson";
import { z } from "zod";
import { z } from "zod/v4";
import { constructBoardPermissions } from "@homarr/auth/shared";
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) =>
z.object({
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);

View File

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

View File

@@ -1,6 +1,6 @@
import { TRPCError } from "@trpc/server";
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 { DockerSingleton } from "@homarr/docker";

View File

@@ -1,5 +1,5 @@
import { TRPCError } from "@trpc/server";
import { z } from "zod";
import { z } from "zod/v4";
import { createId } from "@homarr/common";
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 {

View File

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

View File

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

View File

@@ -1,6 +1,6 @@
import { randomBytes } from "crypto";
import { TRPCError } from "@trpc/server";
import { z } from "zod";
import { z } from "zod/v4";
import { createId } from "@homarr/common";
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";

View File

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

View File

@@ -1,5 +1,5 @@
import { TRPCError } from "@trpc/server";
import { z } from "zod";
import { z } from "zod/v4";
import { createId } from "@homarr/common";
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 { onboardingSteps } from "@homarr/definitions";

View File

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

View File

@@ -1,5 +1,5 @@
import { TRPCError } from "@trpc/server";
import { z } from "zod";
import { z } from "zod/v4";
import { and, eq } from "@homarr/db";
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 type { ServerSettings } from "@homarr/server-settings";

View File

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

View File

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

View File

@@ -1,5 +1,5 @@
import { observable } from "@trpc/server/observable";
import { z } from "zod";
import { z } from "zod/v4";
import { sendPingRequestAsync } from "@homarr/ping";
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 { radarrReleaseTypes } from "@homarr/integrations/types";

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,5 +1,5 @@
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 { minecraftServerStatusRequestHandler } from "@homarr/request-handler/minecraft-server-status";

View File

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

View File

@@ -1,5 +1,5 @@
import { escapeForRegEx } from "@tiptap/react";
import { z } from "zod";
import { z } from "zod/v4";
import { getIntegrationKindsByCategory } from "@homarr/definitions";
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";

View File

@@ -1,5 +1,5 @@
import { observable } from "@trpc/server/observable";
import { z } from "zod";
import { z } from "zod/v4";
import { getIntegrationKindsByCategory } from "@homarr/definitions";
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";

View File

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

View File

@@ -1,21 +1,20 @@
import { z } from "zod";
import type { AnyZodObject, ZodIntersection, ZodObject } from "zod";
import { z } from "zod/v4";
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,
) {
const { _def } = intersection;
const left = intersection.def.left;
const right = intersection.def.right;
// Merge the shapes
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
const mergedShape = { ..._def.left.shape, ..._def.right.shape };
const mergedShape = { ...left.def.shape, ...right.def.shape };
// Return a new ZodObject
return z.object(mergedShape) as unknown as TIntersection extends ZodIntersection<infer TLeft, infer TRight>
? TLeft extends AnyZodObject
? TRight extends AnyZodObject
? // eslint-disable-next-line @typescript-eslint/no-explicit-any
ZodObject<TLeft["shape"] & TRight["shape"], any, any, z.infer<TLeft> & z.infer<TRight>>
? TLeft extends ZodObject
? TRight extends ZodObject
? ZodObject<TLeft["shape"] & TRight["shape"]>
: 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 superjson from "superjson";
import type { OpenApiMeta } from "trpc-to-openapi";
import { ZodError } from "zod";
import { ZodError } from "zod/v4";
import type { Session } from "@homarr/auth";
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 { supportedAuthProviders } from "@homarr/definitions";
@@ -19,7 +19,7 @@ const authProvidersSchema = z
return false;
}),
)
.default("credentials");
.default(["credentials"]);
const authProviders = authProvidersSchema.safeParse(process.env.AUTH_PROVIDERS).data ?? [];

View File

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

View File

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

View File

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

View File

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

View File

@@ -25,7 +25,7 @@ export const recreateAdmin = command({
if (!result.success) {
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;
}

View File

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

View File

@@ -9,8 +9,7 @@
"./types": "./src/types.ts",
"./server": "./src/server.ts",
"./client": "./src/client.ts",
"./env": "./env.ts",
"./env-validation": "./src/env-validation.ts"
"./env": "./env.ts"
},
"typesVersions": {
"*": {
@@ -35,7 +34,7 @@
"react": "19.1.1",
"react-dom": "19.1.1",
"undici": "7.13.0",
"zod": "^3.25.76",
"zod": "^4.0.14",
"zod-validation-error": "^3.5.3"
},
"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 { ZodError } from "zod/v4";
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>;
@@ -19,7 +19,7 @@ export type Inverse<T extends Invertible> = {
type Invertible = Record<PropertyKey, PropertyKey>;
export type inferSearchParamsFromSchema<TSchema extends z.AnyZodObject> = inferSearchParamsFromSchemaInner<
export type inferSearchParamsFromSchema<TSchema extends z.ZodObject> = inferSearchParamsFromSchemaInner<
z.infer<TSchema>
>;

View File

@@ -26,7 +26,7 @@
"dependencies": {
"@t3-oss/env-nextjs": "^0.13.8",
"ioredis": "5.7.0",
"zod": "^3.25.76"
"zod": "^4.0.14"
},
"devDependencies": {
"@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 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(".")}`);
});
.stringbool({
truthy: trueStrings,
falsy: falseStrings,
case: "insensitive",
})
.default(defaultValue);
export const createDurationSchema = (defaultValue: `${number}${"s" | "m" | "h" | "d"}`) =>
z

View File

@@ -35,7 +35,7 @@
"@trpc/tanstack-react-query": "^11.4.4",
"node-cron": "^4.2.1",
"react": "19.1.1",
"zod": "^3.25.76"
"zod": "^4.0.14"
},
"devDependencies": {
"@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 { createEnv } from "@homarr/core/infrastructure/env";
@@ -42,7 +42,7 @@ export const env = createEnv({
.regex(/\d+/)
.transform(Number)
.refine((number) => number >= 1)
.default("3306"),
.default(3306),
DB_USER: z.string(),
DB_PASSWORD: z.string(),
DB_NAME: z.string(),

View File

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

View File

@@ -25,7 +25,7 @@
"dependencies": {
"@homarr/common": "workspace:^0.1.0",
"fast-xml-parser": "^5.2.5",
"zod": "^3.25.76"
"zod": "^4.0.14"
},
"devDependencies": {
"@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 { fileURLToPath } from "node:url";
import { XMLParser } from "fast-xml-parser";
import { z } from "zod";
import { z } from "zod/v4";
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";

View File

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

View File

@@ -1,29 +1,39 @@
import { useForm, zodResolver } from "@mantine/form";
import { z } from "zod";
import type { AnyZodObject, ZodDiscriminatedUnion, ZodEffects, ZodIntersection } from "zod";
import { useForm } from "@mantine/form";
import { zod4Resolver } from "mantine-form-zod-resolver";
import type { ZodDiscriminatedUnion, ZodIntersection, ZodObject, ZodPipe } from "zod/v4";
import { z } from "zod/v4";
import { useI18n } from "@homarr/translation/client";
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 = <
TSchema extends
| AnyZodObject
| ZodEffects<AnyZodObject>
| ZodIntersection<AnyZodObject | ZodDiscriminatedUnion<string, AnyZodObject[]>, AnyZodObject>,
| ZodObject
| ZodPipe<ZodObject>
| ZodIntersection<ZodObject | ZodDiscriminatedUnion<ZodObject[]>, ZodObject>,
>(
schema: TSchema,
options: Omit<
Exclude<Parameters<typeof useForm<z.infer<TSchema>>>[0], undefined>,
Exclude<Parameters<typeof useForm<inferPossibleSchema<TSchema>>>[0], undefined>,
"validate" | "validateInputOnBlur" | "validateInputOnChange"
>,
) => {
const t = useI18n();
z.setErrorMap(zodErrorMap(t));
return useForm<z.infer<TSchema>>({
z.config({
customError: zodErrorMap(t),
});
return useForm<inferPossibleSchema<TSchema>>({
...options,
validateInputOnBlur: 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",
"@mantine/core": "^8.2.4",
"react": "19.1.1",
"zod": "^3.25.76"
"zod": "^4.0.14"
},
"devDependencies": {
"@homarr/eslint-config": "workspace:^0.2.0",

View File

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

View File

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

View File

@@ -47,7 +47,7 @@
"tsdav": "^2.1.5",
"undici": "7.13.0",
"xml2js": "^0.6.2",
"zod": "^3.25.76"
"zod": "^4.0.14"
},
"devDependencies": {
"@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({
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(
z.object({

View File

@@ -3,7 +3,7 @@ import { humanFileSize } from "@homarr/common";
import "@homarr/redis";
import dayjs from "dayjs";
import { z } from "zod";
import { z } from "zod/v4";
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({
access_token: z.string(),

View File

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

View File

@@ -1,5 +1,5 @@
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 { 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({
attributes: z.record(
z.string(),
z.union([z.string(), z.number(), z.boolean(), z.null(), z.array(z.union([z.string(), z.number()]))]),
),
entity_id: z.string(),

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