Skip to content

Serialization

MongoDB documents contain BSON types (ObjectId, Date, Long, etc.) that can't be passed directly to Client Components in Next.js or serialized to JSON. Monch provides built-in serialization to handle this.

Automatic Serialization (Express, Fastify, etc.)

For Express and other frameworks, Monch documents have a toJSON() method that's called automatically by JSON.stringify() and res.json():

typescript
// Express - works automatically!
app.get('/api/users', async (req, res) => {
  const users = await Users.find({}).toArray();
  res.json(users);  // toJSON() called automatically
});

app.get('/api/users/:id', async (req, res) => {
  const user = await Users.findById(req.params.id);
  res.json(user);  // toJSON() called automatically
});

No manual serialization needed - BSON types are converted to JSON-safe values automatically.

Next.js (Manual Serialization Required)

Next.js blocks objects with toJSON at the Server/Client boundary. Use .serialize() explicitly:

typescript
// Server Component
const user = await Users.findOne({ email: 'alice@example.com' });

// This fails! Next.js blocks toJSON
return <UserProfile user={user} />;  // ❌ Error

// Use .serialize() for Next.js
const plain = user.serialize();
return <UserProfile user={plain} />;  // ✅ Works

Single Document

typescript
const user = await Users.findOne({ email: 'alice@example.com' });

// Before serialization
console.log(user._id);        // ObjectId('507f1f77bcf86cd799439011')
console.log(user.createdAt);  // Date object

// After serialization
const plain = user.serialize();
console.log(plain._id);       // '507f1f77bcf86cd799439011' (string)
console.log(plain.createdAt); // '2024-01-15T10:30:00.000Z' (ISO string)

Array of Documents

typescript
const users = await Users.find({ role: 'admin' }).serialize();
// Returns Serialized<User>[] - already serialized

Method 2: Serialize After toArray

typescript
const users = await Users.find({ role: 'admin' }).toArray();
const plain = users.serialize();
// MonchArray has a .serialize() method

Both methods work. The first is more concise.

Type-Safe Serialization

typescript
import { type Serialized, type ModelOf, type SerializedOf } from '@codician-team/monch';

// Document type
type User = ModelOf<typeof Users>;
// { _id: ObjectId, name: string, email: string, createdAt: Date, updatedAt: Date }

// Serialized type (automatic conversion)
type SerializedUser = SerializedOf<typeof Users>;
// { _id: string, name: string, email: string, createdAt: string, updatedAt: string }

// Or manually
type ManualSerialized = Serialized<User>;

// Use in components
interface UserCardProps {
  user: SerializedUser;  // Type-safe serialized user
}

Conversion Table

BSON TypeSerialized ToExample
ObjectIdstring (hex)'507f1f77bcf86cd799439011'
Datestring (ISO 8601)'2024-01-15T10:30:00.000Z'
Longnumber or bigint*9007199254740991
Decimal128string'99999999999999.99'
Binarystring (base64)'aGVsbG8='
Timestamp{ t: number, i: number }{ t: 1234567890, i: 1 }

*Long serializes to number when within JavaScript's safe integer range (±9,007,199,254,740,991). Values outside this range serialize to bigint to preserve precision.

Standalone Serialization

You can serialize data without fetching from a collection:

typescript
import { serialize, serializeArray } from '@codician-team/monch';

// Single document
const plain = serialize(document);

// Array of documents
const plainArray = serializeArray(documents);

MonchArray

When you call .toArray(), Monch returns a MonchArray instead of a regular array. This is an array subclass with a .serialize() method:

typescript
const users = await Users.find({ role: 'admin' }).toArray();

// It's still an array
users.length;       // Works
users.map(...);     // Works
users.filter(...);  // Works

// But with extra functionality
users.serialize();  // Serialize all documents

Next.js Integration

Server Component to Client Component

typescript
// app/users/page.tsx (Server Component)
import { Users } from '@/lib/models';
import { UserList } from './UserList';

export default async function UsersPage() {
  const users = await Users.find({ status: 'active' })
    .sort({ createdAt: -1 })
    .limit(20)
    .serialize();  // Serialize here

  return <UserList users={users} />;
}
typescript
// app/users/UserList.tsx (Client Component)
'use client';

import type { SerializedUser } from '@/lib/models';

interface Props {
  users: SerializedUser[];
}

export function UserList({ users }: Props) {
  return (
    <ul>
      {users.map((user) => (
        <li key={user._id}>{user.name}</li>
      ))}
    </ul>
  );
}

Server Actions

typescript
// app/actions/user.actions.ts
'use server';

import { Users } from '@/lib/models';

export async function getUser(id: string) {
  const user = await Users.findById(id);
  return user?.serialize() ?? null;
}

export async function searchUsers(query: string) {
  const users = await Users.find({
    $text: { $search: query },
  }).serialize();

  return users;
}

Pagination with Serialization

typescript
export async function getUsers(page: number) {
  const result = await Users.find({ status: 'active' })
    .sort({ createdAt: -1 })
    .paginate({ page, limit: 20 });

  return {
    ...result,
    data: result.data.map(user => user.serialize()),
  };
}

When to Serialize

ScenarioMethodNotes
Express res.json()AutomatictoJSON() called by JSON.stringify()
Fastify responseAutomatictoJSON() called by JSON.stringify()
Next.js Client Component.serialize()Next.js blocks toJSON
Next.js Server Action.serialize()Next.js blocks toJSON
JSON.stringify()AutomatictoJSON() called automatically
Server-only processingNoneKeep BSON types
Storing back to MongoDBNoneKeep BSON types

Common Patterns

Optional Chaining

typescript
const user = await Users.findOne({ email });
return user?.serialize() ?? null;

Conditional Serialization

typescript
async function getUser(id: string, serialize = true) {
  const user = await Users.findById(id);
  if (!user) return null;
  return serialize ? user.serialize() : user;
}

Map with Serialization

typescript
const users = await Users.find({ role: 'admin' }).toArray();
const enriched = users.map(user => ({
  ...user.serialize(),
  displayName: `${user.name} (${user.role})`,
}));

Released under the MIT License.