OpenAPI - Serializers
Serializers have the unique ability to transform the attributes and associations assigned to them into robust OpenAPI types. Dream and Psychic both work together to leverage the products of the types provided by serializers into the openapi schemas produced. This enables you to compose openapi types with ease, and will automatically interpret database-level types into fully-expanded OpenAPI types without any effort on your part, providing all of the OpenAPI types for your app out of the box without doing anything (unless your serializers need to deliver something more than what is in the database).
Automatic type inference
Serializers will automatically infer types from the model when using the functional DreamSerializer API. The serializer automatically reads database column types and associations to generate accurate OpenAPI schemas:
Attribute type inference
import { DreamSerializer } from '@rvoh/dream'
import Place from '../models/Place.js'
export const PlaceSerializer = (place: Place) =>
DreamSerializer(Place, place).attribute('name')
Dream will automatically read the database types for Place#name
, returning an OpenAPI type like:
schema: {
Place: {
type: 'object',
required: ['name'],
properties: {
name: {
{ type: 'string', nullable: true }
}
}
}
}
We recommend that you utilize type inference where possible, since it will automatically respond to migrations to generate the most up-to-date types, leaving you with a completely hands-off approach to maintaining your openapi types.
Date inference
Unlike most javascript-based frameworks, Psychic makes a careful distinction between DateTime and CalendarDate, which it uses for datetimes and dates, respectively. Psychic will automatically instantiate instances of DateTime
and CalendarDate
as it reads postgres timestamp
or date
fields from database rows and instantiates model instances out of them, and will automatically convert them to iso8601
strings upon serializing to json, correctly delivering YYYY-MM-DD
format for date fields, while delivering the full iso8601
timestamp spec for date-time fields, including TZ information. In the OpenAPI layer, Psychic will automatically type dates and date-times respectively, like so:
export const PlaceSerializer = (place: Place) =>
DreamSerializer(Place, place)
.attribute('createdOn')
.attribute('createdAt')
schema: {
Place: {
type: 'object',
required: ['createdOn', 'createdAt'],
properties: {
createdOn: {
{ type: 'string', format: 'date' },
},
createdAt: {
{ type: 'string', format: 'date-time' },
}
}
}
}
Association type inference
With the functional API, we can render associations using rendersOne
and rendersMany
methods, which provide nested serializer usage without duplicating structures unnecessarily:
export const UserSerializer = (user: User) =>
DreamSerializer(User, user)
.rendersMany('places', (place) => PlaceSerializer(place))
.rendersOne(
'latestPlace',
(place) => (place ? PlaceSerializer(place) : null),
{ optional: true }
)
which would produce something like:
schema: {
User: {
type: 'object',
required: ['places', 'latestPlace'],
properties: {
places: {
type: 'array',
items: {
$ref: '#/components/schemas/Place'
}
}
latestPlace: {
anyOf: [
{
$ref: '#/components/schemas/Place'
},
{ null: true }
]
}
}
},
Place: {
type: 'object',
required: ['id','name'],
properties: {
id: { type: 'string' },
name: { type: 'string', nullable: true },
}
}
}
Custom attribute definitions
If the inferred types will not work for your use case, then overrides can be provided directly to the attribute
method, enabling you to specify custom OpenAPI types to fit your needs:
export const PlaceSerializer = (place: Place) =>
DreamSerializer(Place, place)
.attribute('description', {
type: 'object',
required: ['en-US', 'en-ES']
properties: {
'en-US': 'Cozy mountain cottage',
'en-ES': 'Cabaña acogedora en la montaña',
}
})
.attribute('name', {
type: 'string',
nullable: true
})
which would produce:
schema: {
Place: {
type: 'object',
required: ['description', 'name'],
properties: {
description: {
type: 'object',
required: ['en-US', 'en-ES']
properties: {
'en-US': 'Cozy mountain cottage',
'en-ES': 'Cabaña acogedora en la montaña',
}
},
name: {
type: 'string',
nullable: true,
}
}
}
}
Psychic and dream are currently fixed to OpenAPI 3.1.0
, and plan to march forward with later versions. We attempt to support the full depth of their api (and if you notice anything that isn't, please open up an issue and we will correct it promptly!), which means you will be able to get typed support and autocomplete to aid you as you define your custom openapi definitions. This includes support for the following types:
- string
- number
- integer
- decimal
- boolean
- date
- date-time
- object
- array
- null
as well as conjuncting types, like:
- anyOf
- oneOf
- allOf
Shorthand
Manually typing OpenAPI specs can be pretty annoying after a while, so we have provided some helpful shorthand for those looking to save their wrists a little. For type primitives, rather than typing { type: 'string' }
, you can simply type 'string'
. The following shorthand strings are available:
- 'string'
- 'string[]'
- 'number'
- 'number[]'
- 'integer'
- 'integer[]'
- 'decimal'
- 'decimal[]'
- 'boolean'
- 'boolean[]'
- 'date'
- 'date[]'
- 'date-time'
- 'date-time[]'
Any of these shorthand representations can be wrapped in an array with 'null' to represent a nullable attribute, e.g., ['string', 'null']
.
export const PlaceSerializer = (place: Place) =>
DreamSerializer(Place, place)
.attribute('name', 'string')
.attribute('amenities', ['string[]', 'null'])
which would produce:
schema: {
Place {
type: 'object',
required: ['name', 'amenities'],
properties: {
name: {
type: 'string',
},
amenities: {
// usually, we wouldn't recommend nullable arrays, because an empty array
// usually represents what we want, but this represents what the openapi
// shorthand can do
type: ['array', 'null'],
items: {
type: 'string',
}
}
}
}
}