Complex Types
Overview
A ComplexType describes a structured object with named fields — the TypeScript equivalent of a class or interface. Where Simple Types represent scalar values, ComplexTypes represent composite data with multiple properties.
DataType
├── SimpleType → scalar (string, number, email …)
└── ComplexTypeBase → structured object
├── ComplexType → regular class-based type
├── MappedType → projected / transformed type
└── MixinType → multiple-inheritance composite
ComplexTypes are the primary tool for defining request/response bodies, database entities, and nested data structures across your API.
Defining a ComplexType
Decorate a class with @ComplexType() to register it as a structured type in the OPRA schema.
import { ComplexType, ApiField } from '@opra/common';
@ComplexType({
description: 'Country information',
})
export class Country {
@ApiField()
declare code?: string;
@ApiField()
declare name?: string;
@ApiField()
declare phoneCode?: string;
}
OPRA reads the class name (Country) as the type's registry name by default. All @ApiField() declarations on the class become the type's field schema.
Fields (@ApiField)
Each property of a ComplexType is declared with @ApiField(). The decorator records field metadata — type, constraints, visibility, and documentation.
Fields without required: true are optional and should be declared with ? in TypeScript.
@ComplexType({ description: 'Address information' })
export class Address {
@ApiField({ description: 'City name', required: true })
declare city: string;
@ApiField({ description: 'ISO country code', required: true })
declare countryCode: string;
@ApiField()
declare street?: string;
@ApiField()
declare zipCode?: string;
}
Key field options
| Option | Type | Description |
|---|---|---|
type | string | DataType | Class | instance | Field data type. Auto-detected from TypeScript design type if omitted. |
description | string | Human-readable field description. |
required | boolean | Field must be present on create operations. |
readonly | boolean | Field cannot be modified after creation. |
writeonly | boolean | Field is accepted on write but never returned on read. |
exclusive | boolean | Field is excluded from results unless explicitly requested. |
default | any | Default value when the field is absent. |
fixed | any | Field is locked to this value and ignores input. |
deprecated | boolean | string | Marks the field as deprecated, optionally with a message. |
examples | any[] | Record<string, any> | Example values shown in schema docs. |
label | string | Human-readable label for UI rendering. |
localization | boolean | Marks the field as a localization candidate. |
isNestedEntity | boolean | Marks the field as a nested entity within the parent document. |
scopePattern | string | RegExp | (string | RegExp)[] | Restricts field visibility to matching scopes. |
keyField | string | Key field name when the field holds an array of ComplexType items. |
Practical example
@ComplexType({ description: 'Customer information' })
export class Customer {
@ApiField({ readonly: true })
declare _id?: number;
@ApiField({ required: true })
declare givenName: string;
@ApiField({ required: true })
declare familyName: string;
@ApiField({ default: 1 })
declare rate?: number;
@ApiField({ deprecated: 'Use phoneNumbers instead' })
declare phone?: string;
@ApiField({ exclusive: true })
declare address?: Address;
@ApiField({ writeonly: true })
declare password?: string;
}
Field Types
The type option accepts several forms:
import { ApiField, ArrayType, UnionType } from '@opra/common';
import { StringType } from '@opra/common';
@ComplexType()
class Product {
// String name — references a registered type
@ApiField({ type: 'email' })
declare contactEmail?: string;
// JS class — resolved to its mapped OPRA type
@ApiField({ type: String })
declare name?: string;
// Another ComplexType class
@ApiField({ type: Address })
declare address?: Address;
// Inline instance — one-off constraints without a named type
@ApiField({ type: new StringType({ minLength: 3, maxLength: 64 }) })
declare slug?: string;
// ArrayType — typed array
@ApiField({ type: ArrayType(String) })
declare tags?: string[];
// UnionType — field accepts multiple types
@ApiField({ type: UnionType([Boolean, Number]) })
declare hasBranch?: boolean | number;
}
Inheritance
A ComplexType can extend another ComplexType, inheriting all its fields.
@ComplexType({
abstract: true,
description: 'Base record with audit fields',
keyField: '_id',
})
export class Record {
@ApiField({ readonly: true })
declare _id?: number;
@ApiField({ readonly: true })
declare createdAt?: Date;
@ApiField({ readonly: true })
declare updatedAt?: Date;
}
@ComplexType({ description: 'Address information' })
export class Address extends Record {
// Inherits _id, createdAt, updatedAt from Record
@ApiField()
declare city?: string;
@ApiField()
declare street?: string;
}
The abstract: true flag prevents Record from being used directly as a field type — it can only be extended.
MappedType — field projection
MappedType derives a new type from an existing ComplexType by picking, omitting, or changing the optionality of its fields — similar to TypeScript's Pick, Omit, and Partial utilities. No new field declarations are needed.
See Mapped Types for full documentation.
MixinType — multiple inheritance
When you need to compose fields from more than one base class, use MixinType. It merges fields from all listed types into a single class without the single-inheritance limitation of plain extends.
See Mixin Types for full documentation.
Additional Fields
The additionalFields option controls what happens when an object contains properties not declared as fields.
| Value | Behaviour |
|---|---|
false / omitted | Additional properties are silently stripped (default) |
true | Any additional property is allowed and passed through as object |
DataType | Additional properties must match the given type |
['error'] | Additional properties cause a validation error |
['error', 'message'] | Validation error with a custom message |
// Strict — extra props are stripped
@ComplexType()
class StrictDto { }
// Open — any extra prop is allowed
@ComplexType({ additionalFields: true })
class OpenConfig { }
// Typed extras — additional props must be strings
@ComplexType({ additionalFields: new StringType() })
class StringMap { }
// Hard reject with custom message
@ComplexType({ additionalFields: ['error', 'No dynamic properties allowed'] })
class LockedSchema { }
Discriminator (Polymorphism)
Use discriminatorField and discriminatorValue to define polymorphic types. OPRA uses the discriminator field's value at runtime to determine which concrete type to decode into.
@ComplexType({
discriminatorField: 'kind',
discriminatorValue: 'dog',
})
class Dog {
@ApiField({ required: true }) declare kind: string;
@ApiField() declare name?: string;
@ApiField() declare breed?: string;
}
@ComplexType({
discriminatorField: 'kind',
discriminatorValue: 'cat',
})
class Cat {
@ApiField({ required: true }) declare kind: string;
@ApiField() declare name?: string;
@ApiField() declare indoor?: boolean;
}
Combine them with UnionType so OPRA knows which concrete types to check at runtime:
import { UnionType } from '@opra/common';
@ComplexType()
class PetOwner {
@ApiField({ type: UnionType([Dog, Cat]) })
declare pet?: Dog | Cat;
}
At decode time, { kind: 'dog', name: 'Rex', breed: 'Labrador' } is decoded as a Dog instance and { kind: 'cat', name: 'Kitty', indoor: true } as a Cat instance.
See Union Types for full documentation.
Scopes & Field Overrides
Fields can be restricted to specific scopes (e.g. 'public', 'db', 'admin') using scopePattern. Only requests that match the scope will see the field.
@ComplexType()
class Customer {
@ApiField()
declare name?: string;
// Only visible in the 'db' scope — hidden from public API responses
@ApiField({ scopePattern: 'db' })
declare internalScore?: number;
}
Per-scope field overrides
Use .Override(scope, options) to change specific field options for a given scope, without duplicating the field declaration:
@ComplexType({
abstract: true,
keyField: '_id',
})
export class Record {
// Readonly in all scopes by default,
// but writable when accessed from the 'db' scope (e.g. internal services)
@(ApiField({ readonly: true })
.Override('db', { readonly: false }))
declare _id?: number;
@(ApiField({ readonly: true })
.Override('db', { readonly: false }))
declare createdAt?: Date;
@(ApiField({ readonly: true })
.Override('db', { readonly: false }))
declare updatedAt?: Date;
}
.Override() can be chained multiple times for multiple scopes:
@(ApiField({ required: true })
.Override('patch', { required: false })
.Override('admin', { readonly: false }))
declare status: string;
Note:
.Override()does not allow changingtype,isArray,isNestedEntity, orscopePattern— only behavioural options likerequired,readonly,default,deprecated, etc.
Decorator Options Reference
@ComplexType(options?)
| Option | Type | Description |
|---|---|---|
name | string | Registry name. Defaults to the class name. |
description | string | Human-readable description included in the schema export. |
abstract | boolean | Cannot be used directly in fields — only extended. |
additionalFields | boolean | DataType | ['error'] | ['error', string] | Policy for properties not declared as fields. |
keyField | string | Field name used as the primary key for this type. |
discriminatorField | string | Field used to identify the concrete type in a union. |
discriminatorValue | string | Value of discriminatorField that maps to this type. |
scopePattern | string | RegExp | (string | RegExp)[] | Restricts which scopes can use this type. |
embedded | boolean | Not exposed as a standalone named type in the schema. |
examples | DataTypeExample[] | Example values for schema documentation. |
@ApiField(options?)
| Option | Type | Description |
|---|---|---|
type | string | DataType | Class | instance | Field data type. |
description | string | Field description. |
required | boolean | Must be present on create. |
readonly | boolean | Cannot be modified after creation. |
writeonly | boolean | Accepted on write, never returned on read. |
exclusive | boolean | Excluded from results unless explicitly requested. |
default | any | Default value when absent. |
fixed | any | Locked value — ignores input. |
deprecated | boolean | string | Marks field deprecated, optionally with a message. |
examples | any[] | Record<string, any> | Example values. |
label | string | Human-readable label for UI. |
localization | boolean | Localization candidate. |
isNestedEntity | boolean | Nested entity within the parent document. |
keyField | string | Key field name for ComplexType array items. |
scopePattern | string | RegExp | (string | RegExp)[] | Scope visibility filter. |