Skip to content

PostgresCrudDialect doesn't serialize custom @json types (typeDefs) #493

@uzarsalan

Description

@uzarsalan

Bug Description

When using custom types with @json attribute in ZenStack v3 beta with PostgreSQL, UPDATE operations fail with PostgreSQL error 22P02 (invalid_text_representation).

Environment

  • ZenStack version: 3.0.0-beta.27
  • Database: PostgreSQL (Supabase)
  • ORM: @zenstackhq/orm with Kysely

Schema

type InlineButton {
    id            String
    text          String
    callback_data String?
    url           String?
    message       String?
    type          String?
}

type BotButton {
    id             String
    label          String
    action         String
    enabled        Boolean
    order_index    Int
    message        String
    inline_buttons InlineButton[]?  // Nested custom type
}

model bot_settings {
    id           Int         @id @default(autoincrement())
    setting_key  String      @unique
    menu_buttons BotButton[] @json  // Array of custom type
    @@schema("crm")
}

Root Cause

Found in @zenstackhq/orm/dist/index.js:

SqliteCrudDialect (lines 1530-1531) - handles typeDefs correctly:

if (this.schema.typeDefs && type in this.schema.typeDefs) {
  return JSON.stringify(value);  // ✅ Custom types are serialized
}

PostgresCrudDialect (lines 1285-1304) - missing typeDefs check:

transformPrimitive(value, type, forArrayField) {
  if (Array.isArray(value)) {
    return value.map((v) => this.transformPrimitive(v, type, false));
  } else {
    return match3(type)
      .with("Json", () => {
        if (typeof value === "string" || typeof value === "number" || typeof value === "boolean") {
          return JSON.stringify(value);
        } else {
          return value;  // ❌ Objects returned without serialization
        }
      })
      .otherwise(() => value);
  }
}

When updating menu_buttons with nested inline_buttons, each BotButton object passes through the map function and ends up in the "Json" branch where objects are returned as-is (without JSON.stringify). Kysely then sends JavaScript objects to PostgreSQL, which expects a JSON string for JSONB columns.

Suggested Fix

Add typeDefs check to PostgresCrudDialect.transformPrimitive():

transformPrimitive(value, type, forArrayField) {
  if (value === void 0) return value;
  
  // Add this check (same as SqliteCrudDialect):
  if (this.schema.typeDefs && type in this.schema.typeDefs) {
    return JSON.stringify(value);
  }
  
  // ... rest of existing code
}

Reproduction

  1. Create a model with nested custom @JSON types (type containing array of another type)
  2. Try to update the field with nested data
  3. PostgreSQL throws error 22P02

Workaround

Currently using raw SQL to bypass the issue, but this loses RLS/access control benefits of ZenStack.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions