Converters
Converters handle the transformation between how data is stored in MongoDB and how it's represented as TypeScript objects. When a model property's database format differs from its runtime type, you need a converter.
Converters live in io-oidis-commons DAO/Converters/. The @Property converter option always takes an instance
(converter: new GroupToRefConverter()), never the class.
When You Need a Converter
| Scenario | DB Stores | Runtime Type | Converter |
|---|---|---|---|
| Simple field (string, number) | Raw value | Same value | None (IdentityConverter) |
| Reference to another model | Referenced record's id (string) | Full model instance | BaseToRefConverter subclass |
| Array of references | string[] of ids | Model instance array | BaseListToRefConverter subclass |
| Embedded subdocument | Full object | Model instance | new InclusionConverter(ModelClass) |
| Embedded subdocument array | Array of objects | Model instance array | new ArrayInclusion(ModelClass) |
| Primitive array | value[] | Same array | new ArrayConverter() |
Built-in Converters
IdentityConverter (Default)
No transformation — the value is stored and retrieved as-is. This is the implicit default when no converter is specified.
// No converter needed — IdentityConverter is used automatically
@Property({type: String})
public declare Name : string;
InclusionConverter / ArrayInclusion
Embeds a subdocument directly in the parent document. The entire object is stored inline in MongoDB. Both take the model class of the subdocument as the constructor argument.
@Property({type: Address, required: false, converter: new InclusionConverter(Address)})
public declare HomeAddress : Address;
@Property({type: [ContactPerson], default: [], converter: new ArrayInclusion(ContactPerson)})
public declare Contacts : ContactPerson[];
MongoDB document (stored field names are the camelCase property names):
{
"name": "John",
"homeAddress": {
"street": "Main St 42",
"city": "Prague",
"zip": "11000"
}
}
Use inclusion when:
- The subdocument only exists within the parent (no standalone collection)
- You always need the subdocument when loading the parent
- The subdocument is relatively small
Save() runs every property through its converter's to(). ArrayInclusion.to() iterates the value and calls
toModel() on each item; InclusionConverter.to() calls toModel() on a non-empty value. The default PropertyValidator
accepts anything, so the mistake surfaces only as a TypeError thrown from Save():
- an
ArrayInclusionproperty that isnullorundefined(declared withoutdefault: [], or cleared withnull) — the iteration fails; - rows that are plain objects, e.g. received through
@Externarguments (see @Extern — Arguments Arrive as Plain JSON) —toModel is not a function.
Always assign an array ([] to empty it) and fill it with instances:
contact.Addresses = [];
for (const row of $data.Addresses ?? []) {
const address : Address = new Address();
address.Street = row.Street;
address.City = row.City;
contact.Addresses.push(address);
}
await contact.Save();
ArrayConverter
For arrays of primitive values. Ensures proper array serialization.
@Property({type: [String], default: [], converter: new ArrayConverter()})
public declare Tags : string[];
Writing a Reference Converter
Reference converters are the most common custom converters. They map a stored id string to a full model instance.
The dataTranslation Context
Every converter method receives an IContext (io-oidis-commons Interfaces/IBaseObject.ts: parent, property,
dataTranslation):
dataTranslation = false— Data comes from MongoDB (the value is the stored id string)dataTranslation = true— Data comes from LCP transport (the value is an already-serialized object)
This dual context is what makes converters work seamlessly across both database reads and frontend-to-backend communication.
Basic Reference Converter
This is the Hub's own GroupToRefConverter (io-oidis-hub DAO/Converters/GroupToRefConverter.ts):
import { BaseToRefConverter } from "@io-oidis-commons/Io/Oidis/Commons/DAO/Converters/BaseToRefConverter.js";
import { IContext } from "@io-oidis-commons/Io/Oidis/Commons/Interfaces/IBaseObject.js";
export class GroupToRefConverter extends BaseToRefConverter {
public async from($value : any, $context : IContext) : Promise<any> {
if ($context.dataTranslation) {
// Value is a serialized object from LCP — reconstruct
return await Group.From($value);
}
// Value is an id string from MongoDB — load from DB
return await Group.FindById($value);
}
}
BaseToRefConverter.to() is inherited: it stores the referenced record's id — and calls Save() on the referenced model
object (see the caution below).
Usage in a Model
@Entity("members")
export class Member extends BaseModel<Member> {
@Property({type: String})
public declare Name : string;
@Property({type: Group, required: false, converter: new GroupToRefConverter()})
public declare Group : Group;
}
What happens on database read:
- MongoDB returns
{ name: "John", group: "0b3a4f10-6a1b-11f1-..." } - Converter's
from()is called with the id string anddataTranslation = false - Converter calls
Group.FindById(...)→ returns fullGroupinstance member.Groupis now a typedGroupobject with all its properties
Pass {partial: true} to Find()/FindById() (or set noResolve: true on the property) to skip this resolution and keep
plain ids — resolving references costs one extra query per reference per record.
What happens on LCP receive:
- Frontend sends
{ Name: "John", Group: { Id: "0b3a4f10-...", Name: "Team A", ... } } - Converter's
from()is called with the serialized object anddataTranslation = true - Converter calls
Group.From($value)→ reconstructsGroupinstance from the serialized data member.Groupis now a typedGroupobject
BaseToRefConverter.to() and BaseListToRefConverter.to() call Save() on every referenced model object held by the
property. Saving a parent therefore writes its referenced records too. Assign id strings when the references must stay
untouched, and never save reference stubs deserialized from a client.
List Reference Converter
For arrays of model references (Hub GroupListToRefConverter):
import { BaseListToRefConverter } from "@io-oidis-commons/Io/Oidis/Commons/DAO/Converters/BaseListToRefConverter.js";
export class GroupListToRefConverter extends BaseListToRefConverter {
public async from($value : any, $context : IContext) : Promise<any> {
const items : Group[] = [];
for await (const item of $value) {
items.push($context.dataTranslation ? await Group.From(item) : await Group.FindById(item));
}
return items;
}
}
@Property({type: [Group], default: [], converter: new GroupListToRefConverter()})
public declare Groups : Group[];
Frontend Converters
Frontend models use the same converter pattern but with a nuance: on the frontend, dataTranslation is effectively always true because all data arrives via LCP (already serialized). However, the pattern is maintained for consistency — keep both branches as in the backend converter.
Converter Flow Diagram
┌─── Database Read ───────────────────────────────┐
│ │
│ MongoDB: { group: "0b3a4f10-..." } │
│ │ │
│ ▼ dataTranslation = false │
│ Converter.from("0b3a4f10-...") │
│ │ │
│ ▼ │
│ Group.FindById("0b3a4f10-...") → Group instance │
└───────────────────────────────────────────────────┘
┌─── LCP Receive ─────────────────────────────────┐
│ │
│ Frontend sends: { Group: { Id: "...", Name: ... }}│
│ │ │
│ ▼ dataTranslation = true │
│ Converter.from({ Id: "...", Name: ... }) │
│ │ │
│ ▼ │
│ Group.From({...}) → Group instance │
└───────────────────────────────────────────────────┘
Best Practices
- Name converters clearly —
ModelNameToRefConverterfor references,ModelNameListToRefConverterfor reference arrays - Always handle both contexts — Even if you think only one path will be used, handle both
dataTranslationbranches - Use
From()for deserialization — Not manual property assignment - Use
FindById()for DB loads — Not raw MongoDB queries - Pass instances —
converter: new MyConverter(), neverconverter: MyConverter - Keep converters simple — They should only convert, not contain business logic
See also: Models Overview | Validators | @Entity & @Property