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; + `, + }, + }) + } +})