From 8fb71259ba03d4e79b804ad262ca70857af9973f Mon Sep 17 00:00:00 2001 From: chrarnoldus <12196001+chrarnoldus@users.noreply.github.com> Date: Wed, 3 Jun 2026 13:01:57 +0000 Subject: [PATCH 1/2] fix(web): support deep strict intersections Co-authored-by: kiloconnect[bot] <240665456+kiloconnect[bot]@users.noreply.github.com> --- apps/web/src/lib/zod/deep-strict.test.ts | 36 +++++++++++++++++-- apps/web/src/lib/zod/deep-strict.ts | 45 +++++++++++++++++++++--- 2 files changed, 74 insertions(+), 7 deletions(-) diff --git a/apps/web/src/lib/zod/deep-strict.test.ts b/apps/web/src/lib/zod/deep-strict.test.ts index 0a5910198a..83f38ce0a9 100644 --- a/apps/web/src/lib/zod/deep-strict.test.ts +++ b/apps/web/src/lib/zod/deep-strict.test.ts @@ -113,10 +113,40 @@ describe('deepStrict', () => { } }); + test('rejects unknown keys inside object intersections', () => { + const schema = deepStrict( + z.intersection( + z.object({ a: z.object({ value: z.string() }) }), + z.object({ b: z.number() }) + ) + ); + + expect(schema.safeParse({ a: { value: 'x' }, b: 1 }).success).toBe(true); + expect(schema.safeParse({ a: { value: 'x' }, b: 1, typo: true }).success).toBe(false); + expect(schema.safeParse({ a: { value: 'x', typo: true }, b: 1 }).success).toBe(false); + }); + + test('supports chained object intersections', () => { + const schema = deepStrict( + z.object({ a: z.string() }) + .and(z.object({ b: z.number() })) + .and(z.object({ c: z.boolean() })) + ); + + expect(schema.safeParse({ a: 'x', b: 1, c: true }).success).toBe(true); + expect(schema.safeParse({ a: 'x', b: 1, c: true, typo: 1 }).success).toBe(false); + }); + + test('preserves validation for overlapping intersection keys', () => { + const schema = deepStrict( + z.intersection(z.object({ value: z.string() }), z.object({ value: z.string().min(2) })) + ); + + expect(schema.safeParse({ value: 'ok' }).success).toBe(true); + expect(schema.safeParse({ value: 'x' }).success).toBe(false); + }); + test('throws on unsupported wrappers so new Zod types surface loudly', () => { - expect(() => - deepStrict(z.intersection(z.object({ a: z.string() }), z.object({ b: z.number() }))) - ).toThrow(/deepStrict: unsupported Zod type 'intersection'/); expect(() => deepStrict(z.tuple([z.string(), z.number()]))).toThrow( /deepStrict: unsupported Zod type 'tuple'/ ); diff --git a/apps/web/src/lib/zod/deep-strict.ts b/apps/web/src/lib/zod/deep-strict.ts index f7801e4e3f..630ef88e51 100644 --- a/apps/web/src/lib/zod/deep-strict.ts +++ b/apps/web/src/lib/zod/deep-strict.ts @@ -9,15 +9,16 @@ import { z } from 'zod'; * document (e.g. an admin editing a custom-LLM definition). * * Handled wrappers: object, optional, nullable, array, record, union - * (covers `z.discriminatedUnion`, which shares `type: 'union'`). These are - * the wrappers used by `CustomLlmDefinitionSchema` today. + * (covers `z.discriminatedUnion`, which shares `type: 'union'`), and + * intersection. These include the wrappers used by `CustomLlmDefinitionSchema` + * today. * * Recognised leaves (string, number, boolean, date, enum, literal, * template_literal, bigint, symbol, null, undefined, void, never, any, * unknown, nan, file) are returned unchanged. * - * Any other Zod type (intersection, tuple, map, set, nonoptional, default, - * prefault, readonly, catch, pipe, lazy, transform, promise, function, + * Any other Zod type (tuple, map, set, nonoptional, default, prefault, + * readonly, catch, pipe, lazy, transform, promise, function, * custom, success) throws, so a future schema change that introduces a new * wrapper surfaces here instead of silently skipping deep-strict and * allowing unknown keys through. @@ -43,6 +44,30 @@ const LEAF_TYPES = new Set([ 'template_literal', ]); +type ZodShape = Record; + +function mergeIntersectionShapes(left: ZodShape, right: ZodShape): ZodShape { + const merged = { ...left }; + for (const [key, value] of Object.entries(right)) { + merged[key] = Object.hasOwn(merged, key) ? z.intersection(merged[key], value) : value; + } + return merged; +} + +function getIntersectionObjectShape(schema: z.ZodTypeAny): ZodShape | null { + if (schema.type === 'object') { + return (schema as z.ZodObject).shape; + } + if (schema.type !== 'intersection') { + return null; + } + + const intersection = schema as z.ZodIntersection; + const left = getIntersectionObjectShape(intersection.def.left as z.ZodTypeAny); + const right = getIntersectionObjectShape(intersection.def.right as z.ZodTypeAny); + return left && right ? mergeIntersectionShapes(left, right) : null; +} + export function deepStrict(schema: T): z.ZodType> { const anySchema = schema as unknown as z.ZodTypeAny; switch (anySchema.type) { @@ -80,6 +105,18 @@ export function deepStrict(schema: T): z.ZodType strictOptions as unknown as readonly [z.ZodTypeAny, z.ZodTypeAny, ...z.ZodTypeAny[]] ) as unknown as z.ZodType>; } + case 'intersection': { + const objectShape = getIntersectionObjectShape(anySchema); + if (objectShape) { + return deepStrict(z.object(objectShape)) as unknown as z.ZodType>; + } + + const intersection = anySchema as z.ZodIntersection; + return z.intersection( + deepStrict(intersection.def.left as z.ZodTypeAny), + deepStrict(intersection.def.right as z.ZodTypeAny) + ) as unknown as z.ZodType>; + } default: { const type = anySchema.type; if (LEAF_TYPES.has(type)) { From 18582495cb9e4aa72161045216ddce42c94daf38 Mon Sep 17 00:00:00 2001 From: chrarnoldus <12196001+chrarnoldus@users.noreply.github.com> Date: Wed, 3 Jun 2026 13:13:08 +0000 Subject: [PATCH 2/2] style(web): format deep strict intersection tests --- apps/web/src/lib/zod/deep-strict.test.ts | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/apps/web/src/lib/zod/deep-strict.test.ts b/apps/web/src/lib/zod/deep-strict.test.ts index 83f38ce0a9..21774c5f91 100644 --- a/apps/web/src/lib/zod/deep-strict.test.ts +++ b/apps/web/src/lib/zod/deep-strict.test.ts @@ -115,10 +115,7 @@ describe('deepStrict', () => { test('rejects unknown keys inside object intersections', () => { const schema = deepStrict( - z.intersection( - z.object({ a: z.object({ value: z.string() }) }), - z.object({ b: z.number() }) - ) + z.intersection(z.object({ a: z.object({ value: z.string() }) }), z.object({ b: z.number() })) ); expect(schema.safeParse({ a: { value: 'x' }, b: 1 }).success).toBe(true); @@ -128,7 +125,8 @@ describe('deepStrict', () => { test('supports chained object intersections', () => { const schema = deepStrict( - z.object({ a: z.string() }) + z + .object({ a: z.string() }) .and(z.object({ b: z.number() })) .and(z.object({ c: z.boolean() })) );