Skip to main content

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

OptionTypeDescription
typestring | DataType | Class | instanceField data type. Auto-detected from TypeScript design type if omitted.
descriptionstringHuman-readable field description.
requiredbooleanField must be present on create operations.
readonlybooleanField cannot be modified after creation.
writeonlybooleanField is accepted on write but never returned on read.
exclusivebooleanField is excluded from results unless explicitly requested.
defaultanyDefault value when the field is absent.
fixedanyField is locked to this value and ignores input.
deprecatedboolean | stringMarks the field as deprecated, optionally with a message.
examplesany[] | Record<string, any>Example values shown in schema docs.
labelstringHuman-readable label for UI rendering.
localizationbooleanMarks the field as a localization candidate.
isNestedEntitybooleanMarks the field as a nested entity within the parent document.
scopePatternstring | RegExp | (string | RegExp)[]Restricts field visibility to matching scopes.
keyFieldstringKey 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.

ValueBehaviour
false / omittedAdditional properties are silently stripped (default)
trueAny additional property is allowed and passed through as object
DataTypeAdditional 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 changing type, isArray, isNestedEntity, or scopePattern — only behavioural options like required, readonly, default, deprecated, etc.


Decorator Options Reference

@ComplexType(options?)

OptionTypeDescription
namestringRegistry name. Defaults to the class name.
descriptionstringHuman-readable description included in the schema export.
abstractbooleanCannot be used directly in fields — only extended.
additionalFieldsboolean | DataType | ['error'] | ['error', string]Policy for properties not declared as fields.
keyFieldstringField name used as the primary key for this type.
discriminatorFieldstringField used to identify the concrete type in a union.
discriminatorValuestringValue of discriminatorField that maps to this type.
scopePatternstring | RegExp | (string | RegExp)[]Restricts which scopes can use this type.
embeddedbooleanNot exposed as a standalone named type in the schema.
examplesDataTypeExample[]Example values for schema documentation.

@ApiField(options?)

OptionTypeDescription
typestring | DataType | Class | instanceField data type.
descriptionstringField description.
requiredbooleanMust be present on create.
readonlybooleanCannot be modified after creation.
writeonlybooleanAccepted on write, never returned on read.
exclusivebooleanExcluded from results unless explicitly requested.
defaultanyDefault value when absent.
fixedanyLocked value — ignores input.
deprecatedboolean | stringMarks field deprecated, optionally with a message.
examplesany[] | Record<string, any>Example values.
labelstringHuman-readable label for UI.
localizationbooleanLocalization candidate.
isNestedEntitybooleanNested entity within the parent document.
keyFieldstringKey field name for ComplexType array items.
scopePatternstring | RegExp | (string | RegExp)[]Scope visibility filter.