Skip to content

(v3) Custom JSON - typescript errors when working with null values #2411

@alexbatis

Description

@alexbatis

Description and expected behavior
im experiencing typescript errors when working with nullable custom json types

i followed the docs here:
https://zenstack.dev/docs/orm/typed-json
https://zenstack.dev/docs/orm/api/json-null

using the latest version of v3 (^3.3.3) but experiencing this across older versions as well. see the test script below i made where i try to perform create/update/find operations for a nullable vs non-nullable json column:

type Metadata {
    someString String
    someInt    Int
}

model Foo {
    id               String    @id @default(cuid())
    createdAt        DateTime  @default(now())
    updatedAt        DateTime  @updatedAt


    metadata         Metadata  @json
    optionalMetadata Metadata? @json
}
import { AnyNull, DbNull, JsonNull } from "@zenstackhq/orm";
import { db } from "./db";
import type { Metadata } from "@/zenstack/models";

/* References:
    - https://zenstack.dev/docs/orm/api/json-null
    - https://zenstack.dev/docs/orm/typed-json
    - https://github.com/zenstackhq/zenstack/issues/2278 (Maybe related (from v2))
*/
export const testJsonNulls = async () => {
  /* --------------------------------- CREATE --------------------------------- */
  const metadata: Metadata = { someInt: 1, someString: "test" };

  // metadata (non nullable)
  // @ts-expect-error - should not be able to set a null value to the non nullable field
  await db.foo.create({ data: { metadata: DbNull } });
  // @ts-expect-error - should not be able to set a null value to the non nullable field
  await db.foo.create({ data: { metadata: JsonNull } });
  // @ts-expect-error - should not be able to set a null value to the non nullable field
  await db.foo.create({ data: { metadata: AnyNull } });
  // @ts-expect-error - should not be able to set a null value to the non nullable field
  await db.foo.create({ data: { metadata: null } });
  await db.foo.create({ data: { metadata } }); // ✅ No typescript error

  // optionalMetadata (nullable)
  /* 
    Type 'DbNullClass' is not assignable to type 'Omit<{ someInt: number; someString: string; }, never> & Partial<Pick<{ someInt: number; someString: string; }, never>> & Record<string, unknown>'.
    Type 'DbNullClass' is missing the following properties from type 'Omit<{ someInt: number; someString: string; }, never>': someInt, someString
  */
  await db.foo.create({ data: { metadata, optionalMetadata: DbNull } }); // ❌ typescript error
  await db.foo.create({ data: { metadata, optionalMetadata: JsonNull } }); // ❌ typescript error
  await db.foo.create({ data: { metadata, optionalMetadata: AnyNull } }); // ❌ typescript error
  await db.foo.create({ data: { metadata, optionalMetadata: null } }); // ✅ No typescript error

  /* --------------------------------- UPDATE --------------------------------- */
  const firstFoo = await db.foo.findFirst();
  if (firstFoo) {
    // metadata (non nullable)
    const where = { id: firstFoo.id };
    // @ts-expect-error - should not be able to set a null value to the non nullable field
    await db.foo.update({ where, data: { metadata: DbNull } });
    // @ts-expect-error - should not be able to set a null value to the non nullable field
    await db.foo.update({ where, data: { metadata: JsonNull } });
    // @ts-expect-error - should not be able to set a null value to the non nullable field
    await db.foo.update({ where, data: { metadata: AnyNull } });
    // @ts-expect-error - should not be able to set a null value to the non nullable field
    await db.foo.update({ where, data: { metadata: null } });
    await db.foo.update({ where, data: { metadata } }); // ✅ No typescript error

    // optionalMetadata (nullable)
    await db.foo.update({ where, data: { metadata, optionalMetadata: DbNull } }); // ❌ typescript error
    await db.foo.update({ where, data: { metadata, optionalMetadata: JsonNull } }); // ❌ typescript error
    await db.foo.update({ where, data: { metadata, optionalMetadata: AnyNull } }); // ❌ typescript error
    await db.foo.update({ where, data: { metadata, optionalMetadata: null } }); // ✅ No typescript error
  }

  /* ---------------------------------- FIND ---------------------------------- */
  // metadata (non nullable)
  // @ts-expect-error - should not be able to filter by DbNull on a non nullable field
  await db.foo.findMany({ where: { metadata: DbNull } });
  // @ts-expect-error - should not be able to filter by JsonNull on a non nullable field
  await db.foo.findMany({ where: { metadata: JsonNull } });
  // @ts-expect-error - should not be able to filter by AnyNull on a non nullable field
  await db.foo.findMany({ where: { metadata: AnyNull } });
  // @ts-expect-error - should not be able to filter by null on a non nullable field
  await db.foo.findMany({ where: { metadata: null } });

  // optionalMetadata (nullable)
  /*
    Type 'DbNullClass' is not assignable to type '(Without<JsonFilter, TypedJsonFieldsFilter<SchemaType, "Metadata">> & TypedJsonFieldsFilter<SchemaType, "Metadata">) | ... 5 more ... | undefined'.ts(2322)
  */
  await db.foo.findMany({ where: { optionalMetadata: DbNull } }); // ❌ typescript error
  await db.foo.findMany({ where: { optionalMetadata: JsonNull } }); // ❌ typescript error
  await db.foo.findMany({ where: { optionalMetadata: AnyNull } }); // ❌ typescript error
  await db.foo.findMany({ where: { optionalMetadata: null } }); // ✅ No typescript error
};

also related to working with nulls, im wondering how this would work using a frontend query client like tanstack. looks like an issue was created around this for V2: #2278

example:

  const client = useClientQueries(schema);
  
  // No typescript error, but will only find rows with db null values
  const query = client.foo.useFindMany({ where: { optionalMetadata: null } });
  
  const mutation = client.foo.useUpdate();
  const performMutation = async (id: string) => {
    // No typescript error, but sets the value to json null, not db null.
    // The above query will not find return the updated row because the stored null value does not match the json null value.
    await mutation.mutate({ where: { id }, data: { optionalMetadata: null } });
  };

Environment (please complete the following information):

  • ZenStack version: 3.3.3
  • Database type: Postgresql
  • Node.js/Bun version: 24.12.0
  • Package manager: bun

Additional context
link to discord message

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions