Serializing Associations
rendersOne
Use .rendersOne() to include a single associated model in serialized output:
// Room with place
export const RoomWithPlaceSerializer = (room: Room) =>
DreamSerializer(Room, room).attribute('id').attribute('name').rendersOne('place')
rendersMany
Use .rendersMany() to include an array of associated models in serialized output:
// From BearBnB: Place with rooms
export const PlaceForGuestsSerializer = (place: Place) =>
DreamSerializer(Place, place)
.attribute('id')
.delegatedAttribute('currentLocalizedText', 'title', { openapi: 'string' })
.rendersMany('rooms', { serializerKey: 'forGuests' })
rendersOne.serializerKey / rendersMany.serializerKey
Specify which serializer to use for the associated model (defaults to 'default'):
export const PlaceDetailSerializer = (place: Place) =>
DreamSerializer(Place, place)
.attribute('id')
.attribute('name')
.rendersMany('rooms', { serializerKey: 'forGuests' })
.rendersMany('bookings') // renders using the default serializer
rendersOne.serializer / rendersMany.serializer
Instead of using serializerKey to reference a serializer by name, you can pass a serializer function directly using the serializer option. This is particularly useful when serializing nested data structures that aren't Dream models, such as arrays of enum values that need to be transformed into objects.
When using the serializer option, passthrough data from the parent serializer is automatically passed to the nested serializer:
import { ObjectSerializer } from '@rvoh/dream'
import { LocalesEnum, BedTypesEnum, BedTypesEnumValues } from '@src/types/db.js'
import i18n from '@src/utils/i18n.js'
// Serializer for a single bed type enum value
export const BedTypeSerializer = (bedType: BedTypesEnum, passthrough: { locale: LocalesEnum }) =>
ObjectSerializer({ bedType }, passthrough)
.attribute('bedType', { as: 'value', openapi: { type: 'string', enum: BedTypesEnumValues } })
.customAttribute('label', () => i18n(passthrough.locale, `rooms.Bedroom.bedTypes.${bedType}`), {
openapi: 'string',
})
// Serializer for a bedroom with localized bed types
export const RoomBedroomForGuestsSerializer = (
roomBedroom: Bedroom,
passthrough: { locale: LocalesEnum },
) =>
DreamSerializer(Bedroom, roomBedroom, passthrough)
.attribute('id')
.attribute('type')
.rendersMany('bedTypes', { serializer: BedTypeSerializer })
In this example, bedTypes is an array of enum values (e.g., ['cot', 'bunk']). The BedTypeSerializer uses ObjectSerializer to transform each enum value into an object with both the value (the enum) and a label (the localized string). The passthrough data containing the locale is automatically passed from the parent serializer to each BedTypeSerializer invocation.
Similarly, you can use rendersOne with a serializer function for single values:
import { ObjectSerializer } from '@rvoh/dream'
import { LocalesEnum, BathOrShowerStylesEnum, BathOrShowerStylesEnumValues } from '@src/types/db.js'
import i18n from '@src/utils/i18n.js'
export const BathOrShowerStyleSerializer = (
bathOrShowerStyle: BathOrShowerStylesEnum,
passthrough: { locale: LocalesEnum },
) =>
ObjectSerializer({ bathOrShowerStyle }, passthrough)
.attribute('bathOrShowerStyle', {
as: 'value',
openapi: { type: 'string', enum: BathOrShowerStylesEnumValues },
})
.customAttribute(
'label',
() => i18n(passthrough.locale, `rooms.Bathroom.bathOrShowerStyles.${bathOrShowerStyle}`),
{
openapi: 'string',
},
)
export const RoomBathroomForGuestsSerializer = (
roomBathroom: Bathroom,
passthrough: { locale: LocalesEnum },
) =>
DreamSerializer(Bathroom, roomBathroom, passthrough)
.attribute('id')
.attribute('type')
.rendersOne('bathOrShowerStyle', { serializer: BathOrShowerStyleSerializer })
When using ObjectSerializer for nested serializers, you must explicitly provide the OpenAPI shape for each attribute since ObjectSerializer doesn't have access to database schema information like DreamSerializer does.
rendersOne.as / rendersMany.as
Rename the association in the output:
export const PlaceSerializer = (place: Place) =>
DreamSerializer(Place, place).attribute('id').rendersMany('rooms', { as: 'accommodations' })
// Output: { id: 1234, accommodations: [...] }
rendersOne.flatten
When flatten: true is included, the attributes of the serialized, associated object are flattened into the serialized results:
export const PlaceSerializer = (place: Place) =>
DreamSerializer(Place, place).attribute('id').rendersOne('currentLocalizedText', {
flatten: true,
serializerKey: 'forPlaces',
})
// Output: { id: 1234, title: 'My localized title', markdown: 'My localized markdown' }
When the goal is to incorporate a property or two from the associated model, a delegatedAttribute may be a simpler approach than creating a new named serializer.
Loading Associations
Associations must be loaded before serialization. Use preloadFor to automatically load everything needed for a serializer:
// Loads all associations needed for PlaceForGuestsSerializer
const places = await Place.preloadFor('forGuests').all()
// Better than manually specifying:
// const places = await Place.preload('rooms', 'currentLocalizedText').all()