Skip to main content

serialization

Whenever rendering a Dream instance, Psychic will always be smart enough to leverage the provided serializer, making these two lines equivalent:

export default class PlacesController extends PsychicController {
public async index() {
...
this.ok(places)
this.ok(places.map(place => PlaceSerializer(place).render()))
}
}

Serialization is an essential step in rendering resourceful output from your application. This is, of course, fairly tricky, since there are many different ways that your application's models may need to be rendered. For example, you may want your index method to render a shortened version of your Place model, while the show method uses the default serializer, which includes nested rooms:

export default class PlacesController extends PsychicController {
public async show() {
const place = await this.currentHost
.associationQuery('places')
.leftJoinPreload('rooms')
.find(this.castParam('id', 'bigint'))
this.ok(place)
}

public async index() {
const places = await this.currentHost.associationQuery('places')
this.ok(places, { serializerKey: 'summary' })
}
}

Note that when specifying a serializerKey, it must match one of the keys in that model's serializers getter:

class Place extends ApplicationModel {
public get serializers(): DreamSerializers<Place> {
return {
default: 'PlaceSerializer',
summary: 'PlaceSummarySerializer',
}
}
}

Each value is the global name Dream registers for an exported serializer — the export name for a file at the root of serializers/ (see serializer file organization for files in a subdirectory).

Adding New Serializers​

When you need to add a new serializer for a model, follow these steps:

  1. Create the serializer function in your serializer file (e.g., app/serializers/PlaceSerializer.ts):
export const PlaceForGuestsSerializer = (place: Place, passthrough: { locale: LocalesEnum }) =>
DreamSerializer(Place, place)
.attribute('id')
.delegatedAttribute('currentLocalizedText', 'title')
.customAttribute('displayStyle', () => i18n(passthrough.locale, `places.style.${place.style}`), {
openapi: 'string',
})
  1. Sync:
pnpm psy sync
  1. Add the serializer key to the model's serializers getter:
export default class Place extends ApplicationModel {
public get serializers(): DreamSerializers<Place> {
return {
default: 'PlaceSerializer',
summary: 'PlaceSummarySerializer',
forGuests: 'PlaceForGuestsSerializer', // Add your new serializer key here
}
}
}
  1. Use the serializer in your controller by specifying the serializerKey in the @OpenAPI decorator:
@OpenAPI(Place, {
status: 200,
serializerKey: 'forGuests', // This must match a key in the model's serializers getter
})
public async show() {
this.ok(await Place.findOrFail(this.castParam('id', 'bigint')))
}

Alternatively, you can specify the serializerKey when calling this.ok():

public async index() {
const places = await Place.all()
this.ok(places, { serializerKey: 'forGuests' })
}

In addition to being able to do implicit and explicit serializer lookups, Psychic can also simply be handed a serializer (or an array of serializers), in which case it will simply call render on that serializer:

export default class PlacesController extends PsychicController {
public async show() {
this.ok(PlaceSerializer(place))
}
}

passthrough​

In some cases, you may need to send passthrough data to your serializers. This is useful, for example, if you have top level variables that you need to expose to all serializers, such as the locale for the current user. Set it with this.serializerPassthrough(...), typically in a @BeforeAction on a base controller (see serializer passthrough data); every serializer the response renders receives it as its second argument.

A query's .passthrough() is a separate channel: it scopes associations declared with DreamConst.passthrough, such as currentLocalizedText, and never reaches a serializer. The example below uses both — the query passthrough so currentLocalizedText loads the text for the request's locale, and the serializer passthrough so PlaceForGuestsSerializer can translate style:

// src/app/controllers/V1/Guest/BaseController.ts
export default class V1GuestBaseController extends V1BaseController {
@BeforeAction()
public configureSerializers() {
this.serializerPassthrough({ locale: this.locale })
}
}
// src/app/controllers/V1/Guest/PlacesController.ts
export default class V1GuestPlacesController extends V1GuestBaseController {
@OpenAPI(Place, {
status: 200,
many: true,
serializerKey: 'summaryForGuests',
})
public async index() {
this.ok(
await Place
.passthrough({ locale: this.locale })
.preloadFor('summaryForGuests')
.all()
)
}

@OpenAPI(Place, {
status: 200,
serializerKey: 'forGuests',
})
public async show() {
this.ok(
await Place.passthrough({ locale: this.locale })
.preloadFor('forGuests')
.findOrFail(this.castParam('id', 'bigint'))
)
}

...
}

// src/app/models/Place.ts

export default class Place extends ApplicationModel {
public override get table() {
return 'places' as const
}

public get serializers(): DreamSerializers<Place> {
return {
default: 'PlaceSerializer',
summary: 'PlaceSummarySerializer',
summaryForGuests: 'PlaceSummaryForGuestsSerializer',
forGuests: 'PlaceForGuestsSerializer',
}
}

...
}


// src/app/serializers/PlaceSerializer.ts

export const PlaceSummaryForGuestsSerializer = (place: Place) =>
DreamSerializer(Place, place)
.attribute('id')
.delegatedAttribute('currentLocalizedText', 'title')

export const PlaceForGuestsSerializer = (place: Place, passthrough: { locale: LocalesEnum }) =>
PlaceSummaryForGuestsSerializer(place)
.customAttribute('style', () => i18n(passthrough.locale, `places.style.${place.style}`), {
openapi: 'string',
})
.attribute('sleeps')
.rendersMany('rooms', { serializerKey: 'forGuests' })

...