Type Mapping
@opra/openapi maps every OPRA data type to an OpenAPI schema. Named model types are placed in components.schemas and referenced with $ref; simple scalar types are always inlined.
ComplexType, MappedType, MixinType → object
All three produce an {type:"object"} schema. OPRA resolves base-class inheritance, mixin merging, and PickType/OmitType/PartialType/RequiredType transforms into a flat field set at construction time, so the mapper always sees the final, flattened view.
OPRA definition:
@ComplexType({ description: 'A customer' })
class Customer {
@ApiField({ required: true })
declare id: string;
@ApiField()
declare givenName?: string;
@ApiField({ readonly: true })
declare createdAt?: Date;
}
OpenAPI output (components.schemas.Customer):
{
"type": "object",
"description": "A customer",
"properties": {
"id": { "type": "string" },
"givenName": { "type": "string" },
"createdAt": { "type": "string", "format": "datetime", "readOnly": true }
},
"required": ["id"]
}
Field-level attributes mapped
OPRA @ApiField attribute | OpenAPI schema field |
|---|---|
description | description |
default | default |
readonly: true | readOnly: true |
writeonly: true | writeOnly: true |
deprecated: true | deprecated: true |
required: true | name appears in parent's required[] |
additionalFields
| OPRA value | OpenAPI |
|---|---|
true | additionalProperties: true |
false | additionalProperties: false |
'error' | additionalProperties: false |
A DataType | additionalProperties: <schema> |
| not set | (omitted — OpenAPI default: allowed) |
EnumType → {type:"string", enum:[...]}
enum Status {
active = 'active',
inactive = 'inactive',
}
EnumType(Status, { name: 'Status' });
{
"type": "string",
"enum": ["active", "inactive"]
}
ArrayType → {type:"array"}
const Tags = ArrayType(String, { name: 'Tags', minOccurs: 1, maxOccurs: 10 });
{
"type": "array",
"items": { "type": "string" },
"minItems": 1,
"maxItems": 10
}
UnionType → {oneOf:[...]}
const PetUnion = UnionType([Dog, Cat], { name: 'PetUnion', discriminator: 'kind' });
{
"oneOf": [
{ "$ref": "#/components/schemas/Dog" },
{ "$ref": "#/components/schemas/Cat" }
],
"discriminator": { "propertyName": "kind" }
}
SimpleType → inlined primitive
Simple types (including custom SimpleType definitions) are always inlined, even when they are named. Extracting them into components.schemas would add indirection without real benefit.
| OPRA built-in type | OpenAPI schema |
|---|---|
string | {type:"string"} |
integer | {type:"integer"} |
number | {type:"number"} |
boolean | {type:"boolean"} |
date | {type:"string", format:"date"} |
datetime | {type:"string", format:"date-time"} |
datetimetz | {type:"string", format:"date-time"} |
uuid | {type:"string", format:"uuid"} |
email | {type:"string", format:"email"} |
url | {type:"string", format:"uri"} |
base64 | {type:"string", format:"byte"} |
bigint | {type:"integer", format:"int64"} |
any | {} |
object | {type:"object"} |
Nullable fields
Nullability is represented differently between OpenAPI versions:
@ApiField({ nullable: true })
declare name?: string | null;
| Version | Output |
|---|---|
| 3.0 | {"type":"string", "nullable":true} |
| 3.1 | {"type":["string","null"]} |
Named types and $ref
Any named model type (ComplexType, EnumType, UnionType, ArrayType) is placed once in components.schemas and referenced elsewhere with $ref. This means each type definition appears exactly once in the output — even if it is used in dozens of places.
{
"components": {
"schemas": {
"Customer": { "type": "object", "properties": { ... } }
}
},
"paths": {
"/Customers": {
"get": {
"responses": {
"200": {
"content": {
"application/json": {
"schema": {
"type": "array",
"items": { "$ref": "#/components/schemas/Customer" }
}
}
}
}
}
}
}
}
}
All named types registered on the ApiDocument appear in components.schemas, even types that no HTTP operation happens to reference directly.
Circular references
The mapper guards against infinite recursion: before recursing into a type's fields, it reserves a slot in components.schemas with an empty object. If the same type is encountered again during recursion, the $ref to the already-reserved slot is returned immediately, breaking the cycle.
→ Overview