From 2feb889b9cdb189b08ef55fb94904315fdac04be Mon Sep 17 00:00:00 2001 From: Timon Date: Mon, 24 Aug 2026 10:59:22 +0200 Subject: [PATCH] chore(server): update zod to 4.4.3 and nestjs-zod to 5.5.0 (#30532) --- open-api/immich-openapi-specs.json | 3 --- pnpm-lock.yaml | 18 +++++++++--------- server/package.json | 4 ++-- server/src/dtos/asset-media.dto.ts | 13 ++++++++++--- server/src/dtos/config.dto.ts | 15 ++++++--------- server/src/dtos/download.dto.ts | 1 + server/src/enum.ts | 1 + server/src/utils/misc.ts | 10 ++++++++++ 8 files changed, 39 insertions(+), 26 deletions(-) diff --git a/open-api/immich-openapi-specs.json b/open-api/immich-openapi-specs.json index 66a430ae1..ce0566524 100644 --- a/open-api/immich-openapi-specs.json +++ b/open-api/immich-openapi-specs.json @@ -4947,7 +4947,6 @@ "in": "query", "description": "Return edited asset if available", "schema": { - "default": false, "type": "boolean" } }, @@ -5035,7 +5034,6 @@ "in": "query", "description": "Return edited asset if available", "schema": { - "default": false, "type": "boolean" } }, @@ -25478,7 +25476,6 @@ "type": "string" }, "expiresAt": { - "default": null, "description": "Expiration date", "example": "2024-01-01T00:00:00.000Z", "format": "date-time", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 6d7cbe9c2..1c437996c 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -575,8 +575,8 @@ importers: specifier: ^8.0.0 version: 8.1.0(@nestjs/common@11.1.29(reflect-metadata@0.2.2)(rxjs@7.8.2)(supports-color@8.1.1))(@nestjs/core@11.1.29)(rxjs@7.8.2) nestjs-zod: - specifier: ^5.3.0 - version: 5.5.0(@nestjs/common@11.1.29(reflect-metadata@0.2.2)(rxjs@7.8.2)(supports-color@8.1.1))(@nestjs/swagger@11.4.6(@nestjs/common@11.1.29(reflect-metadata@0.2.2)(rxjs@7.8.2)(supports-color@8.1.1))(@nestjs/core@11.1.29)(@typescript/typescript6@6.0.2)(reflect-metadata@0.2.2))(rxjs@7.8.2)(zod@4.3.6) + specifier: ^5.5.0 + version: 5.5.0(@nestjs/common@11.1.29(reflect-metadata@0.2.2)(rxjs@7.8.2)(supports-color@8.1.1))(@nestjs/swagger@11.4.6(@nestjs/common@11.1.29(reflect-metadata@0.2.2)(rxjs@7.8.2)(supports-color@8.1.1))(@nestjs/core@11.1.29)(@typescript/typescript6@6.0.2)(reflect-metadata@0.2.2))(rxjs@7.8.2)(zod@4.4.3) nodemailer: specifier: ^9.0.0 version: 9.0.5 @@ -641,8 +641,8 @@ importers: specifier: ^13.12.0 version: 13.15.35 zod: - specifier: 4.3.6 - version: 4.3.6 + specifier: 4.4.3 + version: 4.4.3 devDependencies: '@eslint/js': specifier: ^10.0.0 @@ -13066,8 +13066,8 @@ packages: resolution: {integrity: sha512-zK7YHHz4ZXpW89AHXUPbQVGKI7uvkd3hzusTdotCg1UxyaVtg0zFJSTfW/Dq5f7OBBVnq6cZIaC8Ti4hb6dtCA==} engines: {node: '>= 14'} - zod@4.3.6: - resolution: {integrity: sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg==} + zod@4.4.3: + resolution: {integrity: sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ==} zwitch@1.0.5: resolution: {integrity: sha512-V50KMwwzqJV0NpZIZFwfOD5/lyny3WlSzRiXgA0G7VUnRlqttta1L6UQIHzd6EuBY/cHGfwTIck7w1yH6Q5zUw==} @@ -23363,12 +23363,12 @@ snapshots: rxjs: 7.8.2 tslib: 2.8.1 - nestjs-zod@5.5.0(@nestjs/common@11.1.29(reflect-metadata@0.2.2)(rxjs@7.8.2)(supports-color@8.1.1))(@nestjs/swagger@11.4.6(@nestjs/common@11.1.29(reflect-metadata@0.2.2)(rxjs@7.8.2)(supports-color@8.1.1))(@nestjs/core@11.1.29)(@typescript/typescript6@6.0.2)(reflect-metadata@0.2.2))(rxjs@7.8.2)(zod@4.3.6): + nestjs-zod@5.5.0(@nestjs/common@11.1.29(reflect-metadata@0.2.2)(rxjs@7.8.2)(supports-color@8.1.1))(@nestjs/swagger@11.4.6(@nestjs/common@11.1.29(reflect-metadata@0.2.2)(rxjs@7.8.2)(supports-color@8.1.1))(@nestjs/core@11.1.29)(@typescript/typescript6@6.0.2)(reflect-metadata@0.2.2))(rxjs@7.8.2)(zod@4.4.3): dependencies: '@nestjs/common': 11.1.29(reflect-metadata@0.2.2)(rxjs@7.8.2)(supports-color@8.1.1) deepmerge: 4.3.1 rxjs: 7.8.2 - zod: 4.3.6 + zod: 4.4.3 optionalDependencies: '@nestjs/swagger': 11.4.6(@nestjs/common@11.1.29(reflect-metadata@0.2.2)(rxjs@7.8.2)(supports-color@8.1.1))(@nestjs/core@11.1.29)(@typescript/typescript6@6.0.2)(reflect-metadata@0.2.2) @@ -27081,7 +27081,7 @@ snapshots: compress-commons: 6.0.2 readable-stream: 4.7.0 - zod@4.3.6: {} + zod@4.4.3: {} zwitch@1.0.5: {} diff --git a/server/package.json b/server/package.json index facce43aa..00a83abfe 100644 --- a/server/package.json +++ b/server/package.json @@ -94,7 +94,7 @@ "nestjs-cls": "^6.0.0", "nestjs-kysely": "3.1.2", "nestjs-otel": "^8.0.0", - "nestjs-zod": "^5.3.0", + "nestjs-zod": "^5.5.0", "nodemailer": "^9.0.0", "openid-client": "^6.3.3", "pg": "^8.11.3", @@ -116,7 +116,7 @@ "ua-parser-js": "^2.0.0", "uuid": "^14.0.0", "validator": "^13.12.0", - "zod": "4.3.6" + "zod": "4.4.3" }, "devDependencies": { "@eslint/js": "^10.0.0", diff --git a/server/src/dtos/asset-media.dto.ts b/server/src/dtos/asset-media.dto.ts index c378a7c74..88542a11e 100644 --- a/server/src/dtos/asset-media.dto.ts +++ b/server/src/dtos/asset-media.dto.ts @@ -1,7 +1,7 @@ import { createZodDto } from 'nestjs-zod'; import { HistoryBuilder } from 'src/decorators'; import { AssetMetadataUpsertItemSchema } from 'src/dtos/asset.dto'; -import { AssetVisibilitySchema } from 'src/enum'; +import { ApiCustomExtension, AssetVisibilitySchema } from 'src/enum'; import { isoDatetimeToDate, JsonParsed, stringToBool } from 'src/validation'; import z from 'zod'; @@ -40,8 +40,15 @@ const AssetMediaBaseSchema = z.object({ fileModifiedAt: isoDatetimeToDate.describe('File modification date'), duration: z.coerce.number().int().min(0).optional().describe('Duration in milliseconds (for videos)'), filename: z.string().optional().describe('Filename'), - /** The properties below are added to correctly generate the API docs and client SDKs. Validation should be handled in the controller. */ - [UploadFieldName.ASSET_DATA]: z.any().describe('Asset file data').meta({ type: 'string', format: 'binary' }), + /** + * The properties below are added to correctly generate the API docs and client SDKs. Validation should be handled + * in the controller. File parts never reach the request body, so they must be optional here + */ + [UploadFieldName.ASSET_DATA]: z + .any() + .optional() + .describe('Asset file data') + .meta({ type: 'string', format: 'binary', [ApiCustomExtension.Required]: true }), }); const AssetMediaCreateSchema = AssetMediaBaseSchema.extend({ diff --git a/server/src/dtos/config.dto.ts b/server/src/dtos/config.dto.ts index b0f99996e..8fbdd637e 100644 --- a/server/src/dtos/config.dto.ts +++ b/server/src/dtos/config.dto.ts @@ -36,15 +36,12 @@ import z from 'zod'; const { Admin, User, Public } = ConfigVisibility; const configBool = z - .preprocess((val) => { - if (val === 'true') { - return true; - } - if (val === 'false') { - return false; - } - return val; - }, z.boolean()) + .preprocess( + (val) => z.stringbool({ truthy: ['true'], falsy: ['false'], case: 'sensitive' }).safeParse(val).data ?? val, + z.boolean(), + ) + + .nonoptional() .meta({ type: 'boolean' }); const cronExpressionSchema = z diff --git a/server/src/dtos/download.dto.ts b/server/src/dtos/download.dto.ts index fd61557d0..5c5dc7bcf 100644 --- a/server/src/dtos/download.dto.ts +++ b/server/src/dtos/download.dto.ts @@ -31,6 +31,7 @@ const DownloadArchiveSchema = z // which would limit this DTO to a maximum of 1000 assets. assetIds: z .preprocess((val) => (typeof val === 'string' ? val.split(',') : val), z.array(z.uuidv4())) + .nonoptional() .describe('Asset IDs'), edited: z .preprocess((val) => { diff --git a/server/src/enum.ts b/server/src/enum.ts index 4be515a9e..8cb6582c9 100644 --- a/server/src/enum.ts +++ b/server/src/enum.ts @@ -603,6 +603,7 @@ export enum ApiCustomExtension { AdminOnly = 'x-immich-admin-only', History = 'x-immich-history', State = 'x-immich-state', + Required = 'x-immich-required', } export enum MetadataKey { diff --git a/server/src/utils/misc.ts b/server/src/utils/misc.ts index 7727a8eb8..0c2b18b46 100644 --- a/server/src/utils/misc.ts +++ b/server/src/utils/misc.ts @@ -211,6 +211,16 @@ const patchOpenAPI = (document: OpenAPIObject) => { for (const schema of Object.values(schemas)) { delete (schema as Record).id; + + // documents a property as required even though it is optional during body validation + for (const [key, value] of Object.entries(schema.properties ?? {})) { + if (!(ApiCustomExtension.Required in value)) { + continue; + } + + delete (value as Record)[ApiCustomExtension.Required]; + (schema.required ??= []).push(key); + } } document.components.schemas = sortKeys(schemas);