From 2560233f2a459cebdf103408420d9ca224c8b479 Mon Sep 17 00:00:00 2001 From: ymc9 <104139426+ymc9@users.noreply.github.com> Date: Tue, 5 May 2026 18:26:35 -0700 Subject: [PATCH] fix(orm, zod): allow null in inferred type of required Json fields (#2647) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A required Json column can hold a JSON `null`, so the inferred model type must include `null`. Widen `TypeMap.Json` in @zenstackhq/orm and `JsonValue` in @zenstackhq/zod to align with Prisma's read semantics (the runtime zod schema already accepted null). Filter and mutation types are unaffected — they reference `JsonValue` from @zenstackhq/orm directly, which stays narrow, so explicit `JsonNull`/`DbNull` markers continue to be required. Co-Authored-By: Claude Opus 4.7 (1M context) --- packages/orm/src/utils/type-utils.ts | 2 +- packages/zod/src/types.ts | 6 +-- .../test/issue-2647/regression.test.ts | 33 +++++++++++++++++ tests/regression/test/issue-2647/schema.ts | 37 +++++++++++++++++++ .../regression/test/issue-2647/schema.zmodel | 9 +++++ 5 files changed, 83 insertions(+), 4 deletions(-) create mode 100644 tests/regression/test/issue-2647/regression.test.ts create mode 100644 tests/regression/test/issue-2647/schema.ts create mode 100644 tests/regression/test/issue-2647/schema.zmodel diff --git a/packages/orm/src/utils/type-utils.ts b/packages/orm/src/utils/type-utils.ts index 85152e328..bd020ebae 100644 --- a/packages/orm/src/utils/type-utils.ts +++ b/packages/orm/src/utils/type-utils.ts @@ -40,7 +40,7 @@ export type TypeMap = { Decimal: Decimal; DateTime: Date; Bytes: Uint8Array; - Json: JsonValue; + Json: JsonValue | null; Null: null; Object: Record; Any: unknown; diff --git a/packages/zod/src/types.ts b/packages/zod/src/types.ts index 679eb8231..7715a534d 100644 --- a/packages/zod/src/types.ts +++ b/packages/zod/src/types.ts @@ -135,9 +135,9 @@ type MapFieldTypeToZod = FieldType extends ? z.ZodObject, z.core.$strict> : z.ZodUnknown; -export type JsonValue = string | number | boolean | JsonObject | JsonArray; -type JsonObject = { [key: string]: JsonValue | null }; -type JsonArray = Array; +export type JsonValue = string | number | boolean | JsonObject | JsonArray | null; +type JsonObject = { [key: string]: JsonValue }; +type JsonArray = Array; type JsonZodType = z.ZodType; diff --git a/tests/regression/test/issue-2647/regression.test.ts b/tests/regression/test/issue-2647/regression.test.ts new file mode 100644 index 000000000..a4615efe7 --- /dev/null +++ b/tests/regression/test/issue-2647/regression.test.ts @@ -0,0 +1,33 @@ +import { createTestClient } from '@zenstackhq/testtools'; +import { createSchemaFactory } from '@zenstackhq/zod'; +import { describe, expect, expectTypeOf, it } from 'vitest'; +import z from 'zod'; +import { schema } from './schema'; + +// https://github.com/zenstackhq/zenstack/issues/2647 + +const factory = createSchemaFactory(schema); + +describe('Regression for issue #2647', () => { + it('ORM-inferred type for a required Json field allows null', async () => { + const db = await createTestClient(schema); + type Test = Awaited>; + + // A required Json column can still hold a JSON `null`, so the inferred + // model type for the field must include `null`. + expectTypeOf().toExtend(); + }); + + it('zod-inferred type for a required Json field allows null', () => { + const _schema = factory.makeModelSchema('Test'); + type Test = z.infer; + + expectTypeOf().toExtend(); + }); + + it('zod schema for a required Json field parses null at runtime', () => { + const _schema = factory.makeModelSchema('Test'); + const result = _schema.safeParse({ id: 'test', metaData: null }); + expect(result.success).toBe(true); + }); +}); diff --git a/tests/regression/test/issue-2647/schema.ts b/tests/regression/test/issue-2647/schema.ts new file mode 100644 index 000000000..073812271 --- /dev/null +++ b/tests/regression/test/issue-2647/schema.ts @@ -0,0 +1,37 @@ +////////////////////////////////////////////////////////////////////////////////////////////// +// DO NOT MODIFY THIS FILE // +// This file is automatically generated by ZenStack CLI and should not be manually updated. // +////////////////////////////////////////////////////////////////////////////////////////////// + +/* eslint-disable */ + +import { type SchemaDef, type AttributeApplication, type FieldDefault, ExpressionUtils } from "@zenstackhq/schema"; +export class SchemaType implements SchemaDef { + provider = { + type: "postgresql" + } as const; + models = { + Test: { + name: "Test", + fields: { + id: { + name: "id", + type: "String", + id: true, + attributes: [{ name: "@id" }, { name: "@default", args: [{ name: "value", value: ExpressionUtils.call("uuid") }] }] as readonly AttributeApplication[], + default: ExpressionUtils.call("uuid") as FieldDefault + }, + metaData: { + name: "metaData", + type: "Json" + } + }, + idFields: ["id"], + uniqueFields: { + id: { type: "String" } + } + } + } as const; + plugins = {}; +} +export const schema = new SchemaType(); diff --git a/tests/regression/test/issue-2647/schema.zmodel b/tests/regression/test/issue-2647/schema.zmodel new file mode 100644 index 000000000..ac5f79d0b --- /dev/null +++ b/tests/regression/test/issue-2647/schema.zmodel @@ -0,0 +1,9 @@ +datasource db { + provider = 'postgresql' + url = env("DATABASE_URL") +} + +model Test { + id String @id @default(uuid()) + metaData Json +}