Serialización
QuickModel proporciona transformación bidireccional entre tipos en runtime y formatos compatibles con JSON. Esta página explica cómo funciona la serialización y cómo usar serialize() y toJSON().
$qSerialize() vs toJSON() vs $qToJSON()
$qSerialize()→ devuelve un objeto JavaScript plano (recomendado — soporta todas las opciones de serialización)toJSON()→ devuelve un objeto plano (protocolo JS — llamado automáticamente porJSON.stringify())$qToJSON()→ devuelve una cadena JSON (usa esto cuando necesites un string con opciones de QuickModel)
El Patrón de Dos Interfaces
QuickModel usa dos interfaces para representar los mismos datos:
- Interfaz de Serialización - Tipos compatibles con JSON (lo que viaja por la red)
- Interfaz de Runtime - Tipos de TypeScript (con lo que trabajas en el código)
// Interfaz de serialización (Compatible con JSON)
interface IUser {
id: number;
name: string;
createdAt: string; // ISO date string
balance: string; // BigInt como string
tags: string[]; // Array
metadata: [string, any][]; // Map como tuplas
}
// Interfaz de runtime (opcional pero recomendada)
interface IUserTransform {
createdAt: Date;
balance: bigint;
tags: Set<string>;
metadata: Map<string, any>;
}
@Quick({
createdAt: Date,
balance: BigInt,
tags: Set,
metadata: Map,
})
class User
extends QModel<IUser>
implements IQImplements<IUser, IUserTransform>
{
declare id: number;
declare name: string;
declare createdAt: Date;
declare balance: bigint;
declare tags: Set<string>;
declare metadata: Map<string, any>;
}Deserialización (JSON → Runtime)
Cuando creas una instancia de modelo, QuickModel transforma automáticamente los tipos compatibles con JSON en tipos de runtime:
const user = new User({
id: 1,
name: 'John Doe',
createdAt: '2026-01-10T00:00:00.000Z', // string
balance: '999999999999999', // string
tags: ['typescript', 'node'], // array
metadata: [['key1', 'val1']], // array de tuplas
});
// Tipos en Runtime
console.log(user.createdAt instanceof Date); // true
console.log(typeof user.balance); // 'bigint'
console.log(user.tags instanceof Set); // true
console.log(user.metadata instanceof Map); // trueSerialización (Runtime → JSON)
El método serialize() revierte todas las transformaciones y devuelve un objeto JavaScript plano:
const plain = user.$qSerialize();
// {
// id: 1,
// name: 'John Doe',
// createdAt: '2026-01-10T00:00:00.000Z', // Date → string
// balance: '999999999999999', // bigint → string
// tags: ['typescript', 'node'], // Set → array
// metadata: [['key1', 'val1']] // Map → array de tuplas
// }Usa toJSON() cuando necesites una cadena JSON (por ejemplo, para mensajes WebSocket):
const jsonStr = user.toJSON();
// '{"id":1,"name":"John Doe","createdAt":"2026-01-10T00:00:00.000Z",...}'Reglas de Transformación
Date → String ISO
const event = new Event({ createdAt: '2026-01-10T12:30:00.000Z' });
const plain = event.$qSerialize();
console.log(plain.createdAt); // '2026-01-10T12:30:00.000Z'Usa Date.prototype.toISOString().
BigInt → String
const account = new Account({ balance: '999999999999999' });
const plain = account.$qSerialize();
console.log(plain.balance); // '999999999999999'Convierte usando String(bigint).
Set → Array
const post = new Post({ tags: ['js', 'ts', 'js'] });
const plain = post.$qSerialize();
console.log(plain.tags); // ['js', 'ts'] (duplicados eliminados)Convierte usando Array.from(set).
Map → Array de Tuplas
const config = new Config({
metadata: [
['key1', 'val1'],
['key2', 'val2'],
],
});
const plain = config.$qSerialize();
console.log(plain.metadata); // [['key1', 'val1'], ['key2', 'val2']]Convierte usando Array.from(map.entries()).
RegExp → Object
const validator = new Validator({ pattern: '^[a-z]+$' });
const plain = validator.$qSerialize();
console.log(plain.pattern); // { source: '^[a-z]+$', flags: '' }Symbol → String
const config = new Config({ key: 'unique.key' });
const plain = config.$qSerialize();
console.log(plain.key); // 'unique.key'Usa Symbol.keyFor().
ArrayBuffer → Base64
const file = new File({ data: buffer });
const plain = file.$qSerialize();
console.log(plain.data); // 'SGVsbG8gV29ybGQ=' (base64)TypedArray → Array
const image = new Image({ pixels: new Uint8Array([255, 128, 64]) });
const plain = image.$qSerialize();
console.log(plain.pixels); // [255, 128, 64]URL → String
const link = new Link({ homepage: 'https://example.com' });
const plain = link.$qSerialize();
console.log(plain.homepage); // 'https://example.com'Usa URL.prototype.toString().
URLSearchParams → Object
const request = new Request({ params: { page: '1', limit: '10' } });
const plain = request.$qSerialize();
console.log(plain.params); // { page: '1', limit: '10' }Modelos Anidados
Los modelos anidados se serializan recursivamente:
@Quick({ birthDate: Date })
class Profile extends QModel<IProfile> {
declare name: string;
declare birthDate: Date;
}
@Quick({ profile: Profile, createdAt: Date })
class User extends QModel<IUser> {
declare id: number;
declare profile: Profile;
declare createdAt: Date;
}
const user = new User({
id: 1,
profile: { name: 'John', birthDate: '1990-01-01' },
createdAt: '2026-01-10',
});
const plain = user.$qSerialize();
// {
// id: 1,
// profile: {
// name: 'John',
// birthDate: '1990-01-01' // Date → string
// },
// createdAt: '2026-01-10' // Date → string
// }Arrays de Modelos
Los arrays se serializan elemento por elemento:
@Quick({ price: BigInt })
class Product extends QModel<IProduct> {
declare id: string;
declare price: bigint;
}
@Quick({ items: [Product] })
class Cart extends QModel<ICart> {
declare items: Product[];
}
const cart = new Cart({
items: [
{ id: '1', price: '1000' },
{ id: '2', price: '2000' },
],
});
const plain = cart.$qSerialize();
// {
// items: [
// { id: '1', price: '1000' }, // bigint → string
// { id: '2', price: '2000' } // bigint → string
// ]
// }Trabajando con APIs
Enviando Datos
async function createUser(user: User): Promise<void> {
await fetch('/api/users', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(user.$qSerialize()), // serialize() → objeto plano → cadena JSON
});
}Recibiendo Datos
async function getUser(id: number): Promise<User> {
const response = await fetch(`/api/users/${id}`);
const data = await response.json();
return new User(data); // Deserializar desde JSON
}Ejemplo de Ida y Vuelta (Round-Trip)
// 1. Obtener de API
const user = await getUser(1);
console.log(user.createdAt instanceof Date); // true
// 2. Modificar
user.name = 'Jane Doe';
// 3. Enviar de vuelta a API
await updateUser(user);
async function updateUser(user: User): Promise<void> {
await fetch(`/api/users/${user.id}`, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(user.$qSerialize()),
});
}Integración con JSON.stringify
toJSON() se llama automáticamente por JSON.stringify(), por lo que pasar el modelo directamente funciona:
const user = new User({
id: 1,
name: 'John',
createdAt: '2026-01-10',
});
// Los tres son equivalentes y producen el mismo string JSON:
const json1 = JSON.stringify(user.$qSerialize()); // explícito: serialize → stringify
const json2 = user.toJSON(); // toJSON() devuelve un string JSON directamente
const json3 = JSON.stringify(user); // JSON.stringify llama a toJSON() internamente
console.log(json1 === json2); // true
console.log(json2 === json3); // trueClonar Modelos
Usa serialize() para crear copias profundas (deep copies):
const user = new User({ id: 1, name: 'John', createdAt: '2026-01-10' });
// Crear una copia vía serialize() (devuelve objeto plano)
const copy = new User(user.$qSerialize());
copy.name = 'Jane';
console.log(user.name); // 'John' (el original no cambia)
console.log(copy.name); // 'Jane'O usa el método incorporado copy():
const copy = user.$qCopy(); // Equivalente a new User(user.$qSerialize())
QuickModel preserva los valores `null` y `undefined`:
```typescript
const user = new User({
id: 1,
name: 'John',
createdAt: null, // valor null
});
const plain = user.$qSerialize();
console.log(plain.createdAt); // null (preservado)Serialización Personalizada
Para lógica de serialización personalizada, sobrescribe serialize():
@Quick({ createdAt: Date })
class User extends QModel<IUser> {
declare id: number;
declare name: string;
declare createdAt: Date;
serialize() {
const plain = super.$qSerialize();
// Añadir campos personalizados
return { ...plain, displayName: this.name.toUpperCase() };
}
}
const user = new User({ id: 1, name: 'John', createdAt: '2026-01-10' });
const plain = user.$qSerialize();
console.log(plain.displayName); // 'JOHN'Consejos de Rendimiento
1. Evita Serialización Innecesaria
Llama a serialize() solo cuando sea necesario (e.j., antes de enviar a API):
// ❌ Mal - serialización innecesaria
function processUser(user: User) {
const plain = user.$qSerialize();
console.log(plain.name); // ¡Usa user.name directamente!
}
// ✅ Bien - trabaja con el modelo directamente
function processUser(user: User) {
console.log(user.name);
}2. Caché de Datos Serializados
Si serializas el mismo modelo múltiples veces:
class CachedUser extends User {
private _cachedPlain?: IUser;
serialize() {
if (!this._cachedPlain) {
this._cachedPlain = super.$qSerialize();
}
return this._cachedPlain;
}
}3. Operaciones por Lotes (Batch)
Cuando serialices múltiples modelos, hazlo en una sola pasada:
const users = [user1, user2, user3];
const plainArray = users.map((usr) => usr.$qSerialize());Filtrado de Campos
QuickModel ofrece tres formas de controlar qué campos aparecen en la salida serializada.
En tiempo de ejecución: omit y pick
Pasa opciones a serialize() (o toJSON()) para filtrar campos de forma puntual:
const user = new User({
id: 1,
name: 'Alice',
password: 's3cr3t',
role: 'admin',
});
// omit — excluir campos específicos
const publico = user.$qSerialize({ omit: ['password', 'role'] });
// → { id: 1, name: 'Alice' }
// pick — incluir solo campos específicos
const minimal = user.$qSerialize({ pick: ['id', 'name'] });
// → { id: 1, name: 'Alice' }Permanente: excludeFields
Declara qué campos deben siempre ser excluidos de cada llamada a serialización, directamente en el decorador @Quick():
@Quick(
{
id: 'string',
name: 'string',
password: 'string',
},
{
excludeFields: ['password'], // nunca en la salida JSON
}
)
class Account extends QModel<IAccount> {
declare id: string;
declare name: string;
declare password: string;
}
const account = new Account({ id: '1', name: 'Alice', password: 's3cr3t' });
assert(account.password === 's3cr3t'); // sigue en la instancia
assert(account.$qSerialize().password === undefined); // excluido¿Cuándo usar cada enfoque?
| Enfoque | Declarado | Se aplica | Ideal para |
|---|---|---|---|
excludeFields | decorador @Quick() | siempre, cada llamada | contraseñas, secretos, cachés WeakMap |
omit | serialize({ omit }) | solo esa llamada | dar forma a la respuesta API |
pick | serialize({ pick }) | solo esa llamada | proyección dispersa / actualizaciones parciales |
Campos Calculados (@QComputed())
Por defecto, los getters definidos en el prototipo (propiedades calculadas) no se incluyen en la salida de serialize() ni de toJSON(). Esto evita la exposición accidental de lógica interna. Usa @QComputed() para incluir explícitamente un getter.
import { QModel, Quick, QComputed } from 'quickmodel';
interface IUser {
firstName: string;
lastName: string;
}
@Quick({ firstName: String, lastName: String })
class User extends QModel<IUser> {
declare firstName: string;
declare lastName: string;
@QComputed()
get fullName(): string {
return `${this.firstName} ${this.lastName}`;
}
// SIN decorar — excluido de la serialización
get initials(): string {
return `${this.firstName[0]}.${this.lastName[0]}.`;
}
}
const user = User.create({ firstName: 'Alice', lastName: 'Smith' });
user.$qSerialize();
// { firstName: 'Alice', lastName: 'Smith', fullName: 'Alice Smith' }
// Nota: 'initials' NO está incluidoComportamientos clave
- Solo lectura: Los getters calculados nunca se asignan durante
create()nideserialize(). Cualquier dato entrante para una propiedad@QComputed()se ignora silenciosamente. - Herencia:
@QComputed()en un getter de clase padre es automáticamente visible en la serialización de las clases hijas. - Múltiples campos: Puedes decorar tantos getters como necesites.
@Quick({ firstName: String, lastName: String, birthYear: Number })
class User extends QModel<IUser> {
declare firstName: string;
declare lastName: string;
declare birthYear: number;
@QComputed()
get fullName(): string {
return `${this.firstName} ${this.lastName}`;
}
@QComputed()
get age(): number {
return new Date().getFullYear() - this.birthYear;
}
}
user.$qSerialize();
// { firstName: 'Alice', lastName: 'Smith', birthYear: 1990, fullName: 'Alice Smith', age: 35 }toJSON() y JSON.stringify()
Los campos @QComputed() también se incluyen en la salida de toJSON(), lo que significa que aparecen en JSON.stringify(user) — ya que JSON.stringify llama a toJSON() automáticamente.
- Transformadores - Ve todas las reglas de transformación
- Modelos Anidados - Trabaja con estructuras complejas
- Ejemplos - Integración con API del mundo real
Rendimiento
⚡ Comparativa de rendimiento
Pasa el ratón sobre las barras para ver detalles
1k objetos — coerción automática vs manual. QM: un decorador, cero código extra. Zod/valibot: transform manual por campo. class-transformer: solo Date via @Type, no BigInt/Map/Set. Plain JS, TypeBox, yup, joi, vest, superjson y arktype no hacen coerción a nivel de modelo.
No aplica en este escenario: superjson, Plain JS