Serializing
By default, the ORM will define a toJSON
method on all of your entity prototypes during discovery. This means that when you try to serialize your entity via JSON.stringify()
, the ORM serialization will kick in automatically. The default implementation uses EntityTransformer.toObject()
method, which converts an entity instance into a POJO. During this process, ORM specific constructs like the Reference
or Collection
wrappers are converted to their underlying values.
Hidden Properties
If you want to omit some properties from serialized result, you can mark them with hidden
flag on @Property()
decorator. To have this information available on the type level, you also need to use the HiddenProps
symbol:
@Entity()
class Book {
// we use the `HiddenProps` symbol to define hidden properties on type level
[HiddenProps]?: 'hiddenField' | 'otherHiddenField';
@Property({ hidden: true })
hiddenField = Date.now();
@Property({ hidden: true, nullable: true })
otherHiddenField?: string;
}
const book = new Book(...);
console.log(wrap(book).toObject().hiddenField); // undefined
// @ts-expect-error accessing `hiddenField` will fail to compile thanks to the `HiddenProps` symbol
console.log(wrap(book).toJSON().hiddenField); // undefined
Alternatively, you can use the Hidden
type. It works the same as the Opt
type (an alternative for OptionalProps
symbol), and can be used in two ways:
- with generics:
hiddenField?: Hidden<string>;
- with intersections:
hiddenField?: string & Hidden;
Both will work the same, and can be combined with the HiddenProps
symbol approach.
@Entity()
class Book {
@Property({ hidden: true })
hiddenField: Hidden<Date> = Date.now();
@Property({ hidden: true, nullable: true })
otherHiddenField?: string & Hidden;
}
Shadow Properties
The opposite situation where you want to define a property that lives only in memory (is not persisted into database) can be solved by defining your property as persist: false
. Such property can be assigned via one of Entity.assign()
, em.create()
and em.merge()
. It will be also part of serialized result.
This can be handled when dealing with additional values selected via QueryBuilder
or MongoDB's aggregations.
@Entity()
class Book {
@Property({ persist: false })
count?: number;
}
const book = new Book(...);
wrap(book).assign({ count: 123 });
console.log(wrap(book).toObject().count); // 123
console.log(wrap(book).toJSON().count); // 123
Property Serializers
As an alternative to custom toJSON()
method, we can also use property serializers. They allow to specify a callback that will be used when serializing a property:
@Entity()
class Book {
@ManyToOne({ serializer: value => value.name, serializedName: 'authorName' })
author: Author;
}
const author = new Author('God')
const book = new Book(author);
console.log(wrap(book).toJSON().authorName); // 'God'
Implicit serialization
Implicit serialization means calling toObject()
or toJSON()
on the entity, as opposed to explicitly using the serialize()
helper. Since v6, it works entirely based on populate
hints. This means that, unless you explicitly marked some entity as populated via wrap(entity).populated()
, it will be part of the serialized form only if it was part of the populate
hint:
// let's say both Author and Book entity has a m:1 relation to Publisher entity
// we only populate the publisher relation of the Book entity
const user = await em.findOneOrFail(Author, 1, {
populate: ['books.publisher'],
});
const dto = wrap(user).toObject();
console.log(dto.publisher); // only the FK, e.g. `123`
console.log(dto.books[0].publisher); // populated, e.g. `{ id: 123, name: '...' }`
Moreover, the implicit serialization now respects the partial loading hints too. Previously, all loaded properties were serialized, partial loading worked only on the database query level. Since v6, we also prune the data on runtime. This means that unless the property is part of the partial loading hint (fields
option), it won't be part of the DTO. Main difference here is the primary and foreign keys, that are often automatically selected as they are needed to build the entity graph, but will no longer be part of the DTO.
const user = await em.findOneOrFail(Author, 1, {
fields: ['books.publisher.name'],
});
const dto = wrap(user).toObject();
// only the publisher's name will be available + primary keys
// `{ id: 1, books: [{ id: 2, publisher: { id: 3, name: '...' } }] }`
This also works for embeddables, including nesting and object mode.
Primary keys are automatically included. If you want to hide them, you have two options:
- use
hidden: true
in the property options - use
serialization: { includePrimaryKeys: false }
in the ORM config
Foreign keys are forceObject
Unpopulated relations are serialized as foreign key values, e.g. { author: 1 }
, if you want to enforce objects, e.g. { author: { id: 1 } }
, use serialization: { forceObject: true }
in your ORM config.
For strict typings to respect the global config option, you need to define it on your entity class via Config
symbol:
import { Config, Entity, ManyToOne, PrimaryKey, Ref, wrap } from '@mikro-orm/core';
@Entity()
class Book {
[Config]?: DefineConfig<{ forceObject: true }>;
@PrimaryKey()
id!: number;
@ManyToOne(() => User, { ref: true })
author!: Ref<User>;
}
const book = await em.findOneOrFail(Book, 1);
const dto = wrap(book).toObject();
const identityId = dto.author.id; // without the Config symbol, `dto.identity` would resolve to number
Explicit serialization
The serialization process is normally driven by the populate
hints. If you want to take control over this, you can use the serialize()
helper:
import { serialize } from '@mikro-orm/core';
const dtos = serialize([user1, user2]);
// [
// { name: '...', books: [1, 2, 3], identity: 123 },
// { name: '...', ... },
// ]
const [dto] = serialize(user1); // always returns an array
// { name: '...', books: [1, 2, 3], identity: 123 }
// for a single entity instance we can as well use `wrap(e).serialize()`
const dto2 = wrap(user1).serialize();
// { name: '...', books: [1, 2, 3], identity: 123 }
By default, every relation is considered as not populated - this will result in the foreign key values to be present. Loaded collections will be represented as arrays of the foreign keys. To control the shape of the serialized response we can use the second options
parameter:
interface SerializeOptions<T extends object, P extends string = never, E extends string = never> {
/** Specify which relation should be serialized as populated and which as a FK. */
populate?: AutoPath<T, P>[] | boolean;
/** Specify which properties should be omitted. */
exclude?: AutoPath<T, E>[];
/** Enforce unpopulated references to be returned as objects, e.g. `{ author: { id: 1 } }` instead of `{ author: 1 }`. */
forceObject?: boolean;
/** Ignore custom property serializers. */
ignoreSerializers?: boolean;
/** Skip properties with `null` value. */
skipNull?: boolean;
/** Only include properties for a specific group. If a property does not specify any group, it will be included, otherwise only properties with a matching group are included. */
groups?: string[];
}
Here is a more complex example:
import { wrap } from '@mikro-orm/core';
const dto = wrap(author).serialize({
populate: ['books.author', 'books.publisher', 'favouriteBook'], // populate some relations
exclude: ['books.author.email'], // skip property of some relation
forceObject: true, // not populated or not initialized relations will result in object, e.g. `{ author: { id: 1 } }`
skipNull: true, // properties with `null` value won't be part of the result
});
If you try to populate a relation that is not initialized, it will have same effect as the forceObject
option - the value will be represented as object with just the primary key available.
Serialization groups
Every property can specify its serialization groups, which are then used with explicit serialization.
Properties without the
groups
option are always included.
Let's consider the following entity:
@Entity()
class User {
@PrimaryKey()
id!: number;
@Property()
username!: string;
@Property({ groups: ['public', 'private'] })
name!: string;
@Property({ groups: ['private'] })
email!: string;
}
Now when you call serialize()
:
- without the
groups
option, you get all the properties - with
groups: ['public']
you getid
,username
andname
properties - with
groups: ['private']
you getid
,username
,name
andemail
properties - with
groups: []
you get only theid
andusername
properties (those without groups)
const dto1 = serialize(user);
// User { id: 1, username: 'foo', name: 'Jon', email: 'jon@example.com' }
const dto2 = serialize(user, { groups: ['public'] });
// User { id: 1, username: 'foo', name: 'Jon' }
Caching and toPOJO
While toObject
and serialize
are often enough for serializing your entities, there is one use case where they often fall short, which is caching. When caching an entity, you usually want to ignore things like custom serializers or hidden properties. Once you try to load this entity from cache, it needs to have all the properties just like if you load it again.
Imagine the following scenario: you have a User
entity that has a password
property, which is hidden: true
. Calling toObject()
or serialize()
would omit this hidden password
property, while toPOJO()
would keep it. If you want to cache such an entity, you want to have all the properties, not just those that are visible.
The
toPOJO
method will also ignore serialization hints (populate
andfields
) and will expand all relations unless they form a cycle.
Custom toJSON
method
You can provide custom implementation for toJSON
, while using toObject
for initial serialization:
@Entity()
class Book {
// ...
toJSON(strict = true, strip = ['id', 'email'], ...args: any[]): { [p: string]: any } {
const o = wrap(this, true).toObject(...args); // do not forget to pass rest params here
if (strict) {
strip.forEach(k => delete o[k]);
}
return o;
}
}
Do not forget to pass rest params when calling
toObject(...args)
, otherwise the results might not be stable.