Skip to main content

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​

ScenarioDB StoresRuntime TypeConverter
Simple field (string, number)Raw valueSame valueNone (IdentityConverter)
Reference to another modelReferenced record's id (string)Full model instanceBaseToRefConverter subclass
Array of referencesstring[] of idsModel instance arrayBaseListToRefConverter subclass
Embedded subdocumentFull objectModel instancenew InclusionConverter(ModelClass)
Embedded subdocument arrayArray of objectsModel instance arraynew ArrayInclusion(ModelClass)
Primitive arrayvalue[]Same arraynew 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
Inclusion properties must hold model instances

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 ArrayInclusion property that is null or undefined (declared without default: [], or cleared with null) — the iteration fails;
  • rows that are plain objects, e.g. received through @Extern arguments (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:

  1. MongoDB returns { name: "John", group: "0b3a4f10-6a1b-11f1-..." }
  2. Converter's from() is called with the id string and dataTranslation = false
  3. Converter calls Group.FindById(...) → returns full Group instance
  4. member.Group is now a typed Group object 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:

  1. Frontend sends { Name: "John", Group: { Id: "0b3a4f10-...", Name: "Team A", ... } }
  2. Converter's from() is called with the serialized object and dataTranslation = true
  3. Converter calls Group.From($value) → reconstructs Group instance from the serialized data
  4. member.Group is now a typed Group object
Saving a parent saves its references

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​

  1. Name converters clearly — ModelNameToRefConverter for references, ModelNameListToRefConverter for reference arrays
  2. Always handle both contexts — Even if you think only one path will be used, handle both dataTranslation branches
  3. Use From() for deserialization — Not manual property assignment
  4. Use FindById() for DB loads — Not raw MongoDB queries
  5. Pass instances — converter: new MyConverter(), never converter: MyConverter
  6. Keep converters simple — They should only convert, not contain business logic

See also: Models Overview | Validators | @Entity & @Property