Skip to content

[Regression 7.4.1] Nested Uint8Array in Json fields serialized as numeric-keyed objects instead of base64 strings #29267

@jay-l-e-e

Description

@jay-l-e-e

Bug description

Uint8Array values nested inside objects or arrays are no longer serialized to base64 strings when stored in a Json field. Instead, they are expanded into plain objects with numeric keys (e.g. {"0":72,"1":101,"2":108,"3":108,"4":111}), which is the default JSON.stringify behavior for Uint8Array.

This is a regression introduced in 7.4.1. In 7.3.0, nested Uint8Array values were correctly serialized to base64 strings (e.g. "SGVsbG8=").

Note: Top-level Uint8Array passed directly to a Json field is still correctly serialized to base64 in both versions. The bug only affects Uint8Array values that are nested inside objects or arrays.

Severity

🚨 Critical: Data loss, app crash, security issue

Reproduction

Minimal schema

generator client {
  provider = "prisma-client"
  output   = "../generated/prisma"
}

datasource db {
  provider = "sqlite"
}

model TestRecord {
  id   Int  @id @default(autoincrement())
  data Json
}

Reproduction script

import { PrismaClient } from "./generated/prisma/client.js";
import { PrismaBetterSqlite3 } from "@prisma/adapter-better-sqlite3";

const adapter = new PrismaBetterSqlite3({ url: "file:./dev.db" });
const prisma = new PrismaClient({ adapter });

async function main() {
  await prisma.testRecord.deleteMany();
  const uint8 = new Uint8Array([72, 101, 108, 108, 111]); // "Hello"

  // Case 1: Uint8Array directly — works fine in both versions
  const r1 = await prisma.testRecord.create({ data: { data: uint8 as any } });
  const f1 = await prisma.testRecord.findFirst({ where: { id: r1.id } });
  console.log("Direct:", JSON.stringify(f1!.data));
  // Both 7.3.0 and 7.4.1: "SGVsbG8="  ✅

  // Case 2: Uint8Array nested in object — BROKEN in 7.4.1
  const r2 = await prisma.testRecord.create({
    data: { data: { payload: uint8, label: "test" } as any },
  });
  const f2 = await prisma.testRecord.findFirst({ where: { id: r2.id } });
  console.log("Nested in object:", JSON.stringify(f2!.data));
  // 7.3.0: {"payload":"SGVsbG8=","label":"test"}  ✅
  // 7.4.1: {"payload":{"0":72,"1":101,"2":108,"3":108,"4":111},"label":"test"}  ❌

  // Case 3: Uint8Array nested in array — BROKEN in 7.4.1
  const r3 = await prisma.testRecord.create({
    data: { data: [uint8, "hello"] as any },
  });
  const f3 = await prisma.testRecord.findFirst({ where: { id: r3.id } });
  console.log("Nested in array:", JSON.stringify(f3!.data));
  // 7.3.0: ["SGVsbG8=","hello"]  ✅
  // 7.4.1: [{"0":72,"1":101,"2":108,"3":108,"4":111},"hello"]  ❌
}

main()
  .catch(console.error)
  .finally(() => prisma.$disconnect());

Steps to reproduce

  1. npm install prisma@7.4.1 @prisma/client@7.4.1 @prisma/adapter-better-sqlite3 better-sqlite3
  2. npx prisma generate
  3. npx prisma db push
  4. Run the reproduction script above
  5. Observe that nested Uint8Array values are stored as {"0":72,"1":101,...} instead of base64 strings

Expected vs. Actual Behavior

Expected (as in 7.3.0):

When a Uint8Array is nested inside an object or array and stored in a Json field, it should be serialized to a base64 string, consistent with how top-level Uint8Array values are handled.

Direct Uint8Array:       "SGVsbG8="                              ✅
Nested in object:        {"payload":"SGVsbG8=","label":"test"}    ✅
Nested in array:         ["SGVsbG8=","hello"]                     ✅

Actual (in 7.4.1):

Nested Uint8Array values are expanded into objects with numeric keys via default JSON.stringify behavior, losing the base64 encoding:

Direct Uint8Array:       "SGVsbG8="                                                              ✅ (still works)
Nested in object:        {"payload":{"0":72,"1":101,"2":108,"3":108,"4":111},"label":"test"}     ❌ BUG
Nested in array:         [{"0":72,"1":101,"2":108,"3":108,"4":111},"hello"]                      ❌ BUG

Raw SQLite values confirm the difference

7.3.0:

id=1: "SGVsbG8="
id=2: {"payload":"SGVsbG8=","label":"test"}
id=3: ["SGVsbG8=","hello"]

7.4.1:

id=1: "SGVsbG8="
id=2: {"payload":{"0":72,"1":101,"2":108,"3":108,"4":111},"label":"test"}
id=3: [{"0":72,"1":101,"2":108,"3":108,"4":111},"hello"]

Frequency

Consistently reproducible

Does this occur in development or production?

Both development and production

Is this a regression?

Yes. This worked correctly in Prisma 7.3.0. The regression was introduced in 7.4.1.

Workaround

Manually convert Uint8Array to a base64 string before storing it in a Json field:

function uint8ToBase64(uint8: Uint8Array): string {
  return Buffer.from(uint8).toString("base64");
}

// Instead of:
await prisma.testRecord.create({
  data: { data: { payload: uint8 } },
});

// Do:
await prisma.testRecord.create({
  data: { data: { payload: uint8ToBase64(uint8) } },
});

Prisma Schema & Queries

generator client {
  provider = "prisma-client"
  output   = "../generated/prisma"
}

datasource db {
  provider = "sqlite"
}

model TestRecord {
  id   Int  @id @default(autoincrement())
  data Json
}
const uint8 = new Uint8Array([72, 101, 108, 108, 111]);

// This works fine (top-level):
await prisma.testRecord.create({ data: { data: uint8 } });

// This is broken (nested):
await prisma.testRecord.create({
  data: { data: { payload: uint8, label: "test" } },
});

Prisma Config

import "dotenv/config";
import { defineConfig } from "prisma/config";

export default defineConfig({
  schema: "prisma/schema.prisma",
  migrations: {
    path: "prisma/migrations",
  },
  datasource: {
    url: process.env["DATABASE_URL"],  // "file:./dev.db"
  },
});

Logs & Debug Info

# Prisma 7.3.0 output (correct):
--- Case 1: Uint8Array directly ---
Stored: "SGVsbG8="
>> BASE64 STRING

--- Case 2: Uint8Array nested in object ---
Stored: {"payload":"SGVsbG8=","label":"test"}
>> payload is base64 string (expected)

--- Case 3: Uint8Array inside array ---
Stored: ["SGVsbG8=","hello"]

# Prisma 7.4.1 output (broken):
--- Case 1: Uint8Array directly ---
Stored: "SGVsbG8="
>> BASE64 STRING

--- Case 2: Uint8Array nested in object ---
Stored: {"payload":{"0":72,"1":101,"2":108,"3":108,"4":111},"label":"test"}
>> payload is object/array (BUG: Uint8Array expanded)

--- Case 3: Uint8Array inside array ---
Stored: [{"0":72,"1":101,"2":108,"3":108,"4":111},"hello"]

Environment & Setup

  • OS: macOS 15.6 (Darwin arm64)
  • Database: SQLite (via @prisma/adapter-better-sqlite3)
  • Node.js version: v22.12.0

Prisma Version

prisma               : 7.4.1
@prisma/client       : 7.4.1
Operating System     : darwin
Architecture         : arm64
Node.js              : v22.12.0
TypeScript           : 5.9.3
Query Compiler       : enabled
PSL                  : @prisma/prisma-schema-wasm 7.5.0-4.55ae170b1ced7fc6ed07a15f110549408c501bb3
Schema Engine        : schema-engine-cli 55ae170b1ced7fc6ed07a15f110549408c501bb3
Studio               : 0.13.1

Metadata

Metadata

Assignees

No one assigned

    Labels

    bug/1-unconfirmedBug should have enough information for reproduction, but confirmation has not happened yet.kind/bugA reported bug.

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions