---
title: "Fix Next.js Prisma Serialization: Centralize Decimal/Date"
slug: "centralize-prisma-serialization-nextjs"
published: "2026-01-19"
updated: "2026-04-06"
categories:
  - "Next.js"
tags:
  - "Next.js Prisma serialization"
  - "Prisma Decimal"
  - "serialize Prisma Date"
  - "JSON-safe serialization"
  - "toClientSafe"
  - "toClient"
  - "repository pattern"
  - "Server Components Client Components"
  - "TypeScript"
  - "date-fns-tz"
llm-intent: "how-to"
audience-level: "intermediate"
llm-purpose: "Next.js Prisma serialization: Convert Prisma Decimal and Date to JSON-safe values at the repo level to remove client warnings and simplify pages. Start…"
llm-prereqs:
  - "Next.js"
  - "Prisma"
  - "TypeScript"
  - "date-fns-tz"
---

**Summary Triples**
- (Fix Next.js Prisma Serialization: Centralize Decimal/Date, expresses-intent, how-to)
- (Fix Next.js Prisma Serialization: Centralize Decimal/Date, covers-topic, Next.js Prisma serialization)
- (Fix Next.js Prisma Serialization: Centralize Decimal/Date, provides-guidance-for, Next.js Prisma serialization: Convert Prisma Decimal and Date to JSON-safe values at the repo level to remove client warnings and simplify pages. Start…)

### {GOAL}
Next.js Prisma serialization: Convert Prisma Decimal and Date to JSON-safe values at the repo level to remove client warnings and simplify pages. Start…

### {PREREQS}
- Next.js
- Prisma
- TypeScript
- date-fns-tz

### {STEPS}
1. Add a JSON-safe serializer helper
2. Centralize serialization in repository base
3. Return serialized values from read methods
4. Support ISO strings in date helpers

<!-- llm:goal="Next.js Prisma serialization: Convert Prisma Decimal and Date to JSON-safe values at the repo level to remove client warnings and simplify pages. Start…" -->
<!-- llm:prereq="Next.js" -->
<!-- llm:prereq="Prisma" -->
<!-- llm:prereq="TypeScript" -->
<!-- llm:prereq="date-fns-tz" -->

# Fix Next.js Prisma Serialization: Centralize Decimal/Date
> Next.js Prisma serialization: Convert Prisma Decimal and Date to JSON-safe values at the repo level to remove client warnings and simplify pages. Start…
Matija Žiberna · 2026-01-19

I was building an admin inventory area in Next.js when I hit a noisy warning about Prisma types leaking into Client Components. After chasing it across pages, I realized the fix wasn't to keep serializing props everywhere, but to centralize it in the repository layer. This guide shows how I solved the Decimal/Date serialization problem once, so all inventory pages stop screaming at me.

The exact warning we saw was:

```
Only plain objects can be passed to Client Components from Server Components. Decimal objects are not
supported.
{id: ..., reservoirId: ..., productId: ..., name: ..., type: ..., currentQuantity: Decimal,
minQuantity: ..., unit: ..., createdAt: ..., updatedAt: ..., createdById: ..., updatedById: ...,
vehicleId: ..., reservoir: ..., product: ...}
                                                                                    ^^^^^^^
src/app/(admin)/admin/zaloge/[id]/uredi/page.tsx (31:7) @ EditStockPage
```

That message is accurate: Prisma's Decimal (and Date) are not plain JSON values, so Next.js won't allow
them across the server→client boundary.

## Step 1: Add a JSON-safe serializer helper

The first step is to create a serializer that converts Prisma Decimal, Date, and other non-plain
objects into JSON-safe values. I placed this in a dedicated helper so I could reuse it anywhere.

```typescript
// File: src/lib/serializers/to-client.ts
type JsonPrimitive = string | number | boolean | null;
export type JsonValue =
  | JsonPrimitive
  | JsonValue[]
  | { [key: string]: JsonValue };

const isPlainObject = (value: unknown): value is Record<string, unknown> => {
  if (!value || typeof value !== "object") {
    return false;
  }
  const proto = Object.getPrototypeOf(value);
  return proto === Object.prototype || proto === null;
};

export const toNumberOrNull = (value: unknown): number | null => {
  if (value === null || value === undefined) {
    return null;
  }
  if (typeof value === "number") {
    return Number.isFinite(value) ? value : null;
  }
  if (typeof value === "string") {
    const parsed = Number(value);
    return Number.isFinite(parsed) ? parsed : null;
  }
  if (typeof (value as { toNumber?: () => number }).toNumber === "function") {
    const parsed = (value as { toNumber: () => number }).toNumber();
    return Number.isFinite(parsed) ? parsed : null;
  }
  return null;
};

const toClientSafeInternal = (value: unknown): JsonValue => {
  if (value === null || value === undefined) {
    return value;
  }
  if (
    typeof value === "string" ||
    typeof value === "number" ||
    typeof value === "boolean"
  ) {
    return value;
  }
  if (typeof value === "bigint") {
    return value.toString();
  }
  if (value instanceof Date) {
    return value.toISOString();
  }
  if (typeof (value as { toNumber?: () => number }).toNumber === "function") {
    const parsed = (value as { toNumber: () => number }).toNumber();
    return Number.isFinite(parsed) ? parsed : null;
  }
  if (typeof (value as { toJSON?: () => unknown }).toJSON === "function") {
    return toClientSafeInternal((value as { toJSON: () => unknown }).toJSON());
  }
  if (Array.isArray(value)) {
    return value.map(toClientSafeInternal);
  }
  if (isPlainObject(value)) {
    const result: Record<string, JsonValue> = {};
    for (const [key, nested] of Object.entries(value)) {
      result[key] = toClientSafeInternal(nested);
    }
    return result;
  }
  return String(value);
};

export const toClientSafe = <T>(value: T): JsonValue =>
  toClientSafeInternal(value);
```

This helper intentionally converts non-plain values to safe primitives. That means Decimal becomes a
number, and Date becomes an ISO string. It does exactly what Next.js expects.

## Step 2: Centralize serialization in the repository base class

Instead of serializing data in every page, I added a single toClient helper in the base repository.
That gives me one place to control how all repository results are normalized.

```typescript
// File: src/lib/repositories/base/base-repository.ts
import { prisma } from "@/lib/prisma";
import { toClientSafe } from "@/lib/serializers/to-client";

export abstract class BaseRepository {
  protected readonly prisma = prisma;
  protected readonly context: AuthContext;

  constructor(context: AuthContext) {
    // existing validation
    this.context = context;
  }

  protected toClient<T>(value: T): T {
    return toClientSafe(value) as T;
  }
}
```

This doesn't change any write logic. It only gives us a way to return JSON-safe values from read
methods.

## Step 3: Return serialized values from inventory read methods

With the helper in place, I updated inventory read methods to return this.toClient(...). That's the key
move: every list/get in the inventory repository now returns plain values, so the pages don't need
extra mapping.

```typescript
// File: src/lib/repositories/inventory-repository.ts
async listStockItems(...) {
  const items = await this.prisma.stockItem.findMany({
    where,
    include: { reservoir: { include: { location: true } }, product: true },
    orderBy: [...]
  });

  if (filters?.lowStock) {
    const filtered = items.filter(item =>
      item.reservoir.minQuantity && item.currentQuantity.lessThanOrEqualTo(item.reservoir.minQuantity)
    );
    return this.toClient(filtered);
  }

  return this.toClient(items);
}

async getStockItem(id: string) {
  const item = await this.prisma.stockItem.findFirst({
    where: { id, reservoir: { location: { organisationId: this.organisationId } } },
    include: { reservoir: { include: { location: true } }, product: true }
  });

  return item ? this.toClient(item) : null;
}
```

I applied the same change to all inventory read methods (listLocationsWithReservoirs, listReservoirs,
getReservoir, getLocation, listProducts, getProduct, searchProducts, getReservoirTransactions,
listFuelToppingActivities, getStockTransactions). Now anything coming out of that repository is already
safe to pass into a Client Component.

## Step 4: Accept serialized dates in formatting helpers

Because the serializer turns Date into strings, I made sure our date formatting helpers handle strings
as well.

```typescript
// File: src/lib/time-zone.ts
export function formatLocalDate(
  utcDate: Date | string | number,
  formatStr: string = "yyyy-MM-dd",
): string {
  return formatInTimeZone(new Date(utcDate), APP_TIMEZONE, formatStr);
}
```

This keeps UI formatting stable whether the source is a Date or an ISO string.

## Conclusion

The problem was a Next.js serialization warning caused by Prisma Decimal and Date values crossing into
Client Components. Instead of serializing props page by page, I centralized the solution in the
repository layer and added a reusable serializer. Now all inventory data is JSON-safe by default, and
the warning disappears without adding SWR or rewriting every page.

If you follow these steps, you'll stop seeing that "Only plain objects can be passed to Client
Components…" warning and gain a consistent, maintainable data boundary.

Let me know in the comments if you have questions, and subscribe for more practical development guides.

Thanks, Matija