From 46447936a8f337dca054639bce159aad0d2371b6 Mon Sep 17 00:00:00 2001 From: Maliik-B Date: Sat, 20 Jun 2026 15:49:34 -0400 Subject: [PATCH] fix(typescript): generate non-nullable json columns as NonNullable The generated Json type includes null, so a NOT NULL json/jsonb column was typed as nullable. Narrow it to NonNullable in the nullability helper so the type reflects the database constraint. Nullable json columns are unchanged since Json already covers null. Closes #1055 --- src/server/templates/typescript.ts | 5 ++++ test/server/typegen.ts | 43 ++++++++++++++++++++++++++++++ 2 files changed, 48 insertions(+) diff --git a/src/server/templates/typescript.ts b/src/server/templates/typescript.ts index 352c4ddc..1c0b76b6 100644 --- a/src/server/templates/typescript.ts +++ b/src/server/templates/typescript.ts @@ -483,6 +483,11 @@ export const apply = async ({ : '' function generateNullableUnionTsType(tsType: string, isNullable: boolean) { + // The Json type already includes `null`, so a NOT NULL json/jsonb column has to be + // narrowed with NonNullable to reflect the database constraint. See supabase/postgres-meta#1055. + if (tsType === 'Json' && !isNullable) { + return `NonNullable<${tsType}>` + } // Only add the null union if the type is not unknown as unknown already includes null if (tsType === 'unknown' || tsType === 'any' || !isNullable) { return tsType diff --git a/test/server/typegen.ts b/test/server/typegen.ts index 50a0896b..3d663b79 100644 --- a/test/server/typegen.ts +++ b/test/server/typegen.ts @@ -6964,3 +6964,46 @@ test('typegen: python w/ excluded/included schemas', async () => { }) } }) + +test('typegen: typescript w/ NOT NULL json column narrows Json to NonNullable', async () => { + // The generated Json type includes `null`, so a NOT NULL json/jsonb column has to be + // narrowed with NonNullable to honor the database constraint. A nullable json column is + // left as-is because Json already covers null. See supabase/postgres-meta#1055. + await app.inject({ + method: 'POST', + path: '/query', + payload: { + query: ` + CREATE SCHEMA IF NOT EXISTS json_nullability_test; + CREATE TABLE IF NOT EXISTS json_nullability_test.documents ( + id serial PRIMARY KEY, + required_metadata jsonb NOT NULL, + optional_metadata jsonb + ); + `, + }, + }) + + try { + const { body } = await app.inject({ + method: 'GET', + path: '/generators/typescript', + query: { included_schemas: 'json_nullability_test' }, + }) + // NOT NULL jsonb narrows to NonNullable (Row and Insert). + expect(body).toContain('required_metadata: NonNullable') + expect(body).not.toContain('required_metadata: Json | null') + // Nullable jsonb is unchanged: Json already includes null. + expect(body).toContain('optional_metadata: Json | null') + } finally { + await app.inject({ + method: 'POST', + path: '/query', + payload: { + query: ` + DROP SCHEMA IF EXISTS json_nullability_test CASCADE; + `, + }, + }) + } +})