Skip to main content

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 attributeOpenAPI schema field
descriptiondescription
defaultdefault
readonly: truereadOnly: true
writeonly: truewriteOnly: true
deprecated: truedeprecated: true
required: truename appears in parent's required[]

additionalFields

OPRA valueOpenAPI
trueadditionalProperties: true
falseadditionalProperties: false
'error'additionalProperties: false
A DataTypeadditionalProperties: <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 typeOpenAPI 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;
VersionOutput
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