• Home
BuildWithMatija
Get In Touch
  1. Home
  2. Blog
  3. Next.js
  4. Fix Next.js Prisma Serialization: Centralize Decimal/Date

Fix Next.js Prisma Serialization: Centralize Decimal/Date

Convert Prisma Decimal & Date to JSON-safe values by centralizing serialization in your repository for Next.js

19th January 2026·Updated on:22nd February 2026·MŽMatija Žiberna·
Next.js
Fix Next.js Prisma Serialization: Centralize Decimal/Date

⚡ Next.js Implementation Guides

In-depth Next.js guides covering App Router, RSC, ISR, and deployment. Get code examples, optimization checklists, and prompts to accelerate development.

No spam. Unsubscribe anytime.

Related Posts:

  • •Ultimate Fiidbakk Next.js Integration: Quick Setup Guide
  • •Dynamic robots.txt in Next.js for Multi-Tenant Sites
  • •Interactive TypeScript Schema Visualizer for Next.js

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.

// 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.

// 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.

// 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.

// 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

📄View markdown version
0

Frequently Asked Questions

Comments

Leave a Comment

Your email will not be published

Stay updated! Get our weekly digest with the latest learnings on NextJS, React, AI, and web development tips delivered straight to your inbox.

10-2000 characters

• Comments are automatically approved and will appear immediately

• Your name and email will be saved for future comments

• Be respectful and constructive in your feedback

• No spam, self-promotion, or off-topic content

Matija Žiberna
Matija Žiberna
Full-stack developer, co-founder

I'm Matija Žiberna, a self-taught full-stack developer and co-founder passionate about building products, writing clean code, and figuring out how to turn ideas into businesses. I write about web development with Next.js, lessons from entrepreneurship, and the journey of learning by doing. My goal is to provide value through code—whether it's through tools, content, or real-world software.

You might be interested in

Ultimate Fiidbakk Next.js Integration: Quick Setup Guide
Ultimate Fiidbakk Next.js Integration: Quick Setup Guide

22nd January 2026

Dynamic robots.txt in Next.js for Multi-Tenant Sites
Dynamic robots.txt in Next.js for Multi-Tenant Sites

9th January 2026

Interactive TypeScript Schema Visualizer for Next.js
Interactive TypeScript Schema Visualizer for Next.js

2nd February 2026

Table of Contents

  • Step 1: Add a JSON-safe serializer helper
  • Step 2: Centralize serialization in the repository base class
  • Step 3: Return serialized values from inventory read methods
  • Step 4: Accept serialized dates in formatting helpers
  • Conclusion
On this page:
  • Step 1: Add a JSON-safe serializer helper
  • Step 2: Centralize serialization in the repository base class
  • Step 3: Return serialized values from inventory read methods
  • Step 4: Accept serialized dates in formatting helpers
  • Conclusion
Build With Matija Logo

Build with Matija

Matija Žiberna

I turn scattered business knowledge into one usable system. End-to-end system architecture, AI integration, and development.

Quick Links

Payload CMS Websites
  • Bespoke AI Applications
  • Projects
  • How I Work
  • Blog
  • Get in Touch

    Have a project in mind? Let's discuss how we can help your business grow.

    Contact me →
    © 2026BuildWithMatija•Principal-led system architecture•All rights reserved