forked from mirror/crumbs-original
Install project-level skills via skills.sh CLI: - svelte5-best-practices (runes, snippets, events, migration) - sveltekit-structure (routing, layouts, error boundaries) - sveltekit-data-flow (load functions, form actions, serialization) - typescript-best-practices (type-first development, discriminated unions) - drizzle-migrations (SQLite schema, query patterns) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
7.4 KiB
7.4 KiB
Serialization: What Can/Can't Be Returned
The Rule
Server load functions and form actions must return JSON-serializable data.
Data travels from server → client as JSON. Non-JSON types break.
✅ Serializable (Safe)
| Type | Example | Notes |
|---|---|---|
| String | "hello" |
✅ |
| Number | 42, 3.14 |
✅ |
| Boolean | true, false |
✅ |
| null | null |
✅ |
| Array | [1, 2, 3] |
✅ |
| Plain Object | { name: 'Alice' } |
✅ |
| Nested | { user: { posts: [] } } |
✅ If all values are serializable |
❌ NOT Serializable (Breaks)
| Type | Example | Why | Fix |
|---|---|---|---|
| Date | new Date() |
Becomes string | Use .toISOString() |
| undefined | undefined |
Removed from JSON | Use null |
| Function | () => {} |
Can't serialize | Remove or convert to data |
| Class instance | new User() |
Only serializes properties | Convert to plain object |
| Map | new Map() |
Becomes {} |
Convert to object: Object.fromEntries(map) |
| Set | new Set() |
Becomes {} |
Convert to array: Array.from(set) |
| BigInt | 123n |
Error | Convert to string |
| Symbol | Symbol('id') |
Removed | Don't use |
| RegExp | /test/ |
Becomes {} |
Convert to string |
| Error | new Error() |
Loses stack | Extract message/code |
Examples
❌ Wrong: Returning Date
// +page.server.ts - WRONG
export const load = async () => {
return {
createdAt: new Date(), // Serializes as string, not Date object
};
};
<!-- +page.svelte -->
<script>
export let data;
console.log(data.createdAt); // String, not Date
console.log(data.createdAt.getTime()); // ERROR - not a Date
</script>
✅ Right: Convert Date to ISO String
// +page.server.ts - RIGHT
export const load = async () => {
const user = await db.users.findFirst();
return {
user: {
id: user.id,
name: user.name,
createdAt: user.createdAt.toISOString(), // Convert to string
},
};
};
<!-- +page.svelte -->
<script>
export let data;
const createdAt = new Date(data.user.createdAt); // Parse back to Date
</script>
❌ Wrong: Returning Class Instance
// +page.server.ts - WRONG
class User {
constructor(
public id: number,
public name: string,
) {}
getDisplayName() {
return `User: ${this.name}`;
}
}
export const load = async () => {
const user = new User(1, 'Alice');
return { user }; // Methods are lost during serialization
};
<script>
export let data;
console.log(data.user.getDisplayName()); // ERROR - method doesn't exist
</script>
✅ Right: Return Plain Object
// +page.server.ts - RIGHT
export const load = async () => {
const user = await db.users.findFirst();
return {
user: {
id: user.id,
name: user.name,
email: user.email,
// Only plain data, no methods
},
};
};
❌ Wrong: undefined Values
// +page.server.ts - WRONG
export const load = async () => {
return {
name: 'Alice',
email: undefined, // Removed during JSON.stringify
};
};
<script>
export let data;
console.log('email' in data); // false - key is missing!
</script>
✅ Right: Use null
// +page.server.ts - RIGHT
export const load = async () => {
return {
name: 'Alice',
email: null, // Preserved
};
};
❌ Wrong: Map/Set
// +page.server.ts - WRONG
export const load = async () => {
const tags = new Set(['svelte', 'typescript']);
const metadata = new Map([['version', '1.0']]);
return {
tags, // Becomes {}
metadata, // Becomes {}
};
};
✅ Right: Convert to Array/Object
// +page.server.ts - RIGHT
export const load = async () => {
const tags = new Set(['svelte', 'typescript']);
const metadata = new Map([['version', '1.0']]);
return {
tags: Array.from(tags), // ['svelte', 'typescript']
metadata: Object.fromEntries(metadata), // { version: '1.0' }
};
};
ORM Returns (Drizzle, Prisma)
Most ORMs return plain objects with Date fields:
// +page.server.ts
export const load = async () => {
const user = await db.query.users.findFirst();
// user = { id: 1, name: 'Alice', createdAt: Date }
return {
user: {
...user,
createdAt: user.createdAt.toISOString(), // Convert Date
},
};
};
Or use a helper:
function serialize<T extends Record<string, any>>(obj: T): T {
return JSON.parse(JSON.stringify(obj)); // Forces serialization
}
export const load = async () => {
const user = await db.query.users.findFirst();
return { user: serialize(user) };
};
BigInt
// +page.server.ts - WRONG
export const load = async () => {
return {
userId: 123456789012345678901234567890n, // ERROR - can't serialize
};
};
// +page.server.ts - RIGHT
export const load = async () => {
return {
userId: '123456789012345678901234567890', // String
};
};
Detecting Serialization Issues
SvelteKit will throw an error if you try to return non-serializable data:
Error: Data returned from `load` while rendering / is not serializable:
- Cannot stringify arbitrary non-POJOs
Testing Serialization
const data = { user: new User() };
try {
JSON.parse(JSON.stringify(data));
console.log('✅ Serializable');
} catch (e) {
console.log('❌ Not serializable');
}
TypeScript Help
Use satisfies to catch issues at compile time:
import type { PageServerLoad } from './$types';
export const load = (async () => {
return {
user: {
id: 1,
name: 'Alice',
createdAt: new Date(), // TypeScript allows this, but it's problematic
},
};
}) satisfies PageServerLoad;
Better: Create a type that only allows primitives:
type Serializable =
| string
| number
| boolean
| null
| { [key: string]: Serializable }
| Serializable[];
export const load: PageServerLoad = async () => {
const data: Serializable = {
user: {
id: 1,
name: 'Alice',
createdAt: new Date().toISOString(), // Must be string
},
};
return data;
};
Quick Checklist
Before returning from server load or form action:
- ✅ All values are string, number, boolean, null, array, or plain object?
- ✅ No Date objects? (convert to
.toISOString()) - ✅ No undefined? (use null)
- ✅ No class instances? (convert to plain objects)
- ✅ No Map/Set? (convert to object/array)
- ✅ No functions?
- ✅ No BigInt? (convert to string)