Skip to main content

associations

rendersOne

Use .rendersOne() to include a single associated model in your serialized output:

export const RoomWithPlaceSerializer = (room: Room) =>
DreamSerializer(Room, room).attribute('id').attribute('name').rendersOne('place')

rendersMany

Use .rendersMany() to include an array of associated models:

export const PlaceForGuestsSerializer = (place: Place) =>
DreamSerializer(Place, place)
.attribute('id')
.delegatedAttribute('currentLocalizedText', 'title', { openapi: 'string' })
.rendersMany('rooms', { serializerKey: 'forGuests' })

serializerKey

By default, associations render using the 'default' serializer declared on the associated model. Pass serializerKey to use a different one:

export const PlaceDetailSerializer = (place: Place) =>
DreamSerializer(Place, place)
.attribute('id')
.attribute('name')
.rendersMany('rooms', { serializerKey: 'forGuests' })
.rendersMany('bookings') // uses 'default'

serializer

Instead of referencing a serializer by name with serializerKey, you can pass a serializer function directly. This is particularly useful when you need to serialize something that isn't a Dream model — like transforming an array of enum values into objects:

import { ObjectSerializer } from '@rvoh/dream'

// 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',
})

// parent serializer that uses it
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 })
info

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.

as

Rename the association in the output:

export const PlaceSerializer = (place: Place) =>
DreamSerializer(Place, place).attribute('id').rendersMany('rooms', { as: 'accommodations' })

// renders: { id: 1234, accommodations: [...] }

rendersOne.flatten

When flatten: true is included, the serialized association's attributes get flattened into the parent object:

export const PlaceSerializer = (place: Place) =>
DreamSerializer(Place, place).attribute('id').rendersOne('currentLocalizedText', {
flatten: true,
serializerKey: 'forPlaces',
})

// renders: { id: 1234, title: 'My localized title', markdown: 'My localized markdown' }

When you only need a property or two from an association, a delegatedAttribute is usually simpler than creating a new named serializer just to flatten it.

loading associations

Associations need to be loaded before serialization. The easiest way is with preloadFor, which automatically figures out what to load based on the serializer:

const places = await Place.preloadFor('forGuests').all()