
Prisma Decimal Date Serialization: Why It Still Breaks RSC
Skilldham
Engineering deep-dives for developers who want real understanding.
Last updated: July 2026
TL;DR
Prisma decimal date serialization fails because Decimal, BigInt, and Date are not plain JSON values. React blocks them at the Server-to-Client boundary. JSON.stringify alone breaks on BigInt and cannot handle nested relations. The fix is one recursive serializer. It converts all three types at once. Wire it into a Prisma client extension and every query returns clean data by default.
You write a Server Component. You fetch a record with a Decimal price field. You pass it to a Client Component.
Red text hits your terminal.
Only plain objects can be passed to Client Components from Server Components. Decimal objects are not supported.
You Google it. You find a two-year-old GitHub discussion. The Prisma team says they are "not sure what the optimal solution is." Still open. Still unresolved in 2026.
You add JSON.parse(JSON.stringify(data)). A Stack Overflow answer told you to. It works, until next month.
You add a BigInt id column. Now you get a new crash: Do not know how to serialize a BigInt. Two problems. Two different patches. Neither one touches your nested relations.
Here is the serializer that handles all three types at once. Here is where to put it, so you only write it once.
Why Decimal, BigInt, and Date Break the Server-Client Boundary
React Server Components only allow plain, JSON-safe values across the boundary. Strings, numbers, plain objects, and arrays pass through fine.
Decimal, BigInt, and class instances do not. React cannot verify they survive the round trip.
Prisma's Decimal type is a Decimal.js instance. It is not a plain number. It exists so financial values do not lose precision.
BigInt is a native JS primitive. But JSON.stringify has no built-in rule for it. It throws instead of failing quietly.
Date is the odd one out. Next.js allows raw Date objects across the boundary. The problem shows up later. You serialize the object yourself and forget dates need string conversion too.
The Verbatim Error You Will See
I hit this building Paisa. It is a trading app. Every price and prediction score is a Decimal. The console showed this:
Warning: Only plain objects can be passed to Client Components from Server Components.
Decimal objects are not supported. {id: 12, symbol: ..., predictedPrice: Decimal, confidence: Decimal, createdAt: Date, ...}Every field after predictedPrice looked fine. Only the Decimal fields triggered the warning. That is the tell. See this error? Check for Decimal or BigInt columns first. Do not check your component logic first.
The Wrong Fix: Ad Hoc JSON.stringify on Every Page
Most fixes online look like this.
javascript
// Wrong: works for one flat object, breaks on BigInt and nested relations
async function getProduct(id: number) {
const product = await prisma.product.findUnique({ where: { id } });
return JSON.parse(JSON.stringify(product));
}This looks fine at first. Then your schema changes. Three things break it.
First, BigInt throws inside JSON.stringify. No replacer function, no fallback. TypeError: Do not know how to serialize a BigInt. Your whole request crashes instead of warning.
Second, Decimal.js has a toJSON() method. It returns a string. That part works. But you lose the type information. Your Client Component now gets "49.99" as plain text. Forget to convert it back, and your math silently turns into string concatenation.
Third, and nobody mentions this part: the pattern only works on the top level. Add an include for a relation, and JSON.stringify still walks the tree. But you lose control of the shape. String dates and string decimals look identical once both are strings.

Build One Serializer for Decimal, BigInt, and Date
The fix is one recursive function. It walks any value. It checks the type. It converts once. It works at the top level. It works three relations deep too.
typescript
// File: src/lib/serialize-prisma.ts
import { Decimal } from "@prisma/client/runtime/library";
type Serialized<T> = T extends Decimal
? string
: T extends bigint
? string
: T extends Date
? string
: T extends (infer U)[]
? Serialized<U>[]
: T extends object
? { [K in keyof T]: Serialized<T[K]> }
: T;
export function serializePrisma<T>(value: T): Serialized<T> {
if (value === null || value === undefined) {
return value as Serialized<T>;
}
// Decimal has internal d, e, s fields - check the instance directly, before the generic object check
if (value instanceof Decimal) {
return value.toString() as Serialized<T>;
}
if (typeof value === "bigint") {
return value.toString() as Serialized<T>;
}
if (value instanceof Date) {
return value.toISOString() as Serialized<T>;
}
if (Array.isArray(value)) {
return value.map((item) => serializePrisma(item)) as Serialized<T>;
}
if (typeof value === "object") {
const result: Record<string, unknown> = {};
for (const key of Object.keys(value)) {
result[key] = serializePrisma((value as Record<string, unknown>)[key]);
}
return result as Serialized<T>;
}
return value as Serialized<T>;
}Why the Fix Works
The function checks instanceof Decimal first. This order matters. A Decimal also looks like a plain object to typeof. A generic object branch would mangle it. You would get raw internal fields, not a clean string.
Recursion handles nesting for free. Every array and nested object gets the same three checks. No extra code for relations. No matter how deep they go.
The Serialized<T> type keeps your Client Component types honest. Decimal becomes string in the type system too. TypeScript catches it if you try math on it without converting first.
Handling Nested Relations and Arrays
This is the part most guides skip. A flat product record is easy. A product with an array of price history entries is not. Each entry has its own Decimal and Date. Manual per-page fixes fall apart here.
typescript
// Wrong: only serializes the top level, items array still has Decimal instances
const order = await prisma.order.findUnique({
where: { id: orderId },
include: { items: true },
});
return JSON.parse(JSON.stringify(order)); // items[].unitPrice still risky with BigInt idstypescript
// Correct: one call, handles the order, the items array, and every Decimal and BigInt inside it
const order = await prisma.order.findUnique({
where: { id: orderId },
include: { items: true },
});
return serializePrisma(order);The recursive function does not know items is a relation. It does not need to. It sees an array. It maps over it. It applies the same three checks to every entry. A Decimal three levels deep gets the same treatment as one at the top.
Using a Prisma Client Extension Instead of a Manual Serializer
Calling serializePrisma() on every query works. But you will forget it somewhere. The better fix centralizes it at the client level. Every query returns clean data automatically.
Prisma 7 removed middleware ($use) entirely. Client extensions are now the only hook into the query lifecycle. This pattern is the current, supported approach. It is not a workaround.
typescript
// File: src/lib/prisma.ts
import { PrismaClient } from "@prisma/client";
import { serializePrisma } from "./serialize-prisma";
const basePrisma = new PrismaClient();
export const prisma = basePrisma.$extends({
name: "serializePrismaTypes",
query: {
$allOperations: async ({ args, query }) => {
const result = await query(args);
return serializePrisma(result);
},
},
});Why This Beats a Per-Query Serializer
Every query through prisma now returns safe data by default. You cannot forget the step. It is not a separate call anymore.
$allOperations runs on every query type. That includes findMany, create, and update. Create a record with a Decimal field. It is safe the moment it comes back. Not just on reads.
One tradeoff to know: $allOperations intercepts every query, including transactions. Need raw Decimal math server-side before rendering? Keep a second, unextended client for that logic. Serialize only at the point you hand data to a component.
Precision Loss: Where Converting Decimal to Number Breaks Silently
A common shortcut converts Decimal straight to Number instead of string.
typescript
// Wrong: works for small values, breaks precision on large or many-decimal-place numbers
return { ...product, price: Number(product.price) };JavaScript numbers use 64-bit floating point. A Decimal field storing "19.99" converts fine. A Decimal field storing "922337203685.4775807" does not. That precision range is common in financial or crypto data. Converting to Number drops digits the moment it happens.
I hit this on Paisa. Confidence scores went to six decimal places. Converting to Number on the server rounded values. A strict equality check on the client then failed. The bug only showed up on specific symbols. It looked like a data issue at first, not a type issue.
toString() avoids this entirely. Format the string for display with toFixed() or a formatting library. Keep the source of truth as a string until the last render step.
Wiring It Into the Repository Layer
Put the extended client behind your data access functions. Do not put it directly in components. This keeps the serialization logic in one file. Change it later, and you only change it once.
typescript
// File: src/repositories/order-repository.ts
import { prisma } from "@/lib/prisma";
export async function getOrderWithItems(orderId: number) {
return prisma.order.findUnique({
where: { id: orderId },
include: { items: true },
});
// Already serialized - the extension in src/lib/prisma.ts handles it
}Server Components call getOrderWithItems. They get back a plain object. No manual conversion in the page file. The Prisma 7 NeonDB Next.js driver adapter setup guide uses the same pattern. That guide centralizes connection setup. This post centralizes serialization instead.
Hit driver adapter or migration errors while setting up the extension above? The Prisma 7 migration errors: driver adapter, P1012 and SSL post covers those separately.
For the mechanics behind Prisma's own BigInt recommendation, see Prisma's serialization docs.
Key Takeaways
Prisma decimal date serialization fails because Decimal and BigInt are not plain JSON values. React blocks them at the Server-to-Client boundary.
JSON.stringify alone throws on BigInt. It cannot tell decimals from dates once both become strings.
One recursive serializer handles all three types at any depth. It checks instanceof Decimal, typeof "bigint", and instanceof Date.
Wire the serializer into a Prisma client extension with $allOperations. Prisma 7 removed middleware entirely, so this is the current path.
Converting Decimal to Number instead of string loses precision silently. This hits large values and values with many decimal places.
Keep a second, unextended client if you need raw Decimal math before serialization.
Centralize serialization in your repository layer, not in individual page components.
FAQ
Does this error still happen in Next.js 15 and Next.js 16?
Yes. The restriction on passing non-plain objects from Server to Client Components has not changed. It applies the same way in Next.js 15 and 16.
Is this fixed in Prisma 7?
No. Prisma 7 did not add built-in serialization for Decimal or BigInt. What changed is how you hook into queries. Middleware is gone. Client extensions are now the only supported hook. The pattern above uses one.
Can I just use superjson instead of writing my own serializer?
You can. It solves type preservation differently. It encodes type metadata alongside the value. That adds a dependency and a decode step on the client. For most apps, a plain string conversion is simpler.
Why does JSON.stringify throw on BigInt but not on Decimal?
Decimal.js instances have a toJSON() method. JSON.stringify calls it automatically. That converts them to a string without error. Native BigInt has no such method. JSON.stringify throws instead of guessing a conversion.
Will converting Decimal to a string break my math on the client?
Only if you do arithmetic on the string directly. Convert it back with Number() or Decimal.js on the client first. Keep it as a string for display and for passing between components.
Does this serializer work with Prisma's driver adapters?
Yes. The $allOperations hook runs at the query result level. That is after the driver adapter already returned data. Neon adapter, pg adapter, or the default connection. It works the same on all three.
What about Decimal fields inside a groupBy or aggregate query?
Aggregate results still contain Decimal instances for sums and averages. The extension wraps $allOperations. So aggregate and groupBy results get serialized the same way as regular finds.
Can I disable this warning instead of fixing it?
No supported flag disables it. This is a React Server Components constraint. It is not a Prisma or Next.js config option. Serializing the data is the only fix.
Conclusion
Decimal and BigInt were never meant to cross a server-to-client JSON boundary. That is why this keeps breaking. Patching it page by page just moves the bug. It does not fix it.
One recursive serializer closes the gap. Wire it into a Prisma client extension. It covers every query at once, nested relations included. Adding a new Decimal column stops being a multi-file refactor.
Running Prisma 7 with driver adapters? Pair this with your connection setup. Keep both in one place.
Want the next production fix sent straight to you? Subscribe to the SkillDham newsletter. Real bugs from real Next.js and Prisma projects. No tutorial theory.