Skip to main content

Simple Types

Overview

A SimpleType is the most fundamental building block in OPRA's type system. It represents a scalar value — a single piece of data with no internal structure (unlike Complex Types which describe objects with fields).

SimpleTypes sit at the leaf level of the data type hierarchy:

DataType
└── SimpleType
├── Built-in primitives (string, number, boolean …)
├── Built-in extended (email, uuid, date, url …)
└── Custom types (your own domain-specific scalars)

Every SimpleType can:

  • validate values at runtime through its codec pipeline
  • carry attributes that constrain validation (e.g. minLength, maxValue)
  • be extended to create narrower custom types
  • be registered in an ApiDocument and referenced by name from any field

Built-in Simple Types

OPRA ships two groups of built-in simple types.

Primitive types

NameJS equivalentDescription
anyanyAccepts any value without validation
bigintbigintArbitrary-precision integer
booleanbooleantrue / false
integernumberWhole numbers (no decimal part)
nullnullThe null value
numbernumberInteger and floating-point numbers
objectobjectPlain object (no field constraints)
stringstringSequence of characters

Extended types

NameDescription
base64Base64-encoded binary data
credit-cardCredit card number
dateCalendar date (YYYY-MM-DD)
datetimeDate and time without timezone
datetime-tzDate and time with timezone offset
eanEuropean Article Number (barcode)
emailRFC 5321 email address
field-pathDot-notation field path (e.g. address.city)
filterOPRA filter expression string
ibanInternational Bank Account Number
ipIPv4 or IPv6 address
mobile-phoneMobile phone number
object-idDatabase object identifier (e.g. MongoDB ObjectId)
operation-resultStructured operation result envelope
timeTime of day (HH:mm:ss)
urlURL (RFC 3986)
uuidUniversally Unique Identifier

Using a Simple Type

Reference a built-in type by name in a @ComplexType field:

import { ComplexType, ApiField } from "@opra/common";

@ComplexType()
class Product {
@ApiField({ type: "string" })
name: string;

@ApiField({ type: "number" })
price: number;

@ApiField({ type: "email" })
contactEmail: string;

@ApiField({ type: "uuid" })
id: string;

@ApiField({ type: "date" })
releaseDate: string;
}

You can also pass the JS class directly — OPRA resolves the mapping automatically:

@ApiField({ type: String }) // → 'string'
@ApiField({ type: Number }) // → 'number'
@ApiField({ type: Boolean }) // → 'boolean'

Inline type instantiation

If you need attribute constraints on a single field without registering a new named type, you can pass a type instance directly:

import { ComplexType, ApiField } from '@opra/common';
import { StringType, NumberType } from '@opra/common';

@ComplexType()
class Product {
@ApiField({ type: new StringType({ pattern: /\w+/ }) })
name: string;

@ApiField({ type: new NumberType({ minValue: 1, maxValue: 99 }) })
age: number;
}

The instance carries its attribute values and generates its codec inline — no type registry entry is created. This is convenient for one-off constraints that don't need to be reused elsewhere.

When to use each approach
ApproachWhen to use
String name ('email')Referencing a registered built-in or custom type
JS class (String)Shorthand for primitive built-ins
Instance (new StringType(…))One-off constraint on a single field
Named custom typeConstraint shared across multiple fields or types

Creating a Custom Simple Type

Use the @SimpleType() decorator on a class that extends one of the built-in type classes.

import { SimpleType } from "@opra/common";
import { StringType } from "@opra/common";

@SimpleType({
name: "slug",
description:
"URL-friendly identifier consisting of lowercase letters, numbers and hyphens",
})
class SlugType extends StringType {
@SimpleType.Attribute()
minLength = 3;

@SimpleType.Attribute()
maxLength = 64;

@SimpleType.Attribute()
pattern = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
}

Register the type in your ApiDocument and use it from any field:

@ComplexType()
class Article {
@ApiField({ type: "slug" })
slug: string;
}
tip

The name option in @SimpleType() becomes the string identifier used in ApiField({ type: '...' }). If omitted, OPRA derives the name from the class name.


Type Attributes

Attributes are per-field constraints applied at encode/decode time. They are declared with @SimpleType.Attribute() inside the type class and can be overridden per-field when referencing the type.

String attributes

AttributeTypeDescription
minLengthnumberMinimum character count
maxLengthnumberMaximum character count
patternstring | RegExpRegex the value must match
patternNamestringHuman-readable name for the pattern (used in error messages)
@SimpleType({ name: "username" })
class UsernameType extends StringType {
@SimpleType.Attribute()
minLength = 3;

@SimpleType.Attribute()
maxLength = 32;

@SimpleType.Attribute()
pattern = /^[a-zA-Z0-9_]+$/;

@SimpleType.Attribute()
patternName = "alphanumeric with underscores";
}

Number / Integer attributes

AttributeTypeDescription
minValuenumberMinimum allowed value (inclusive)
maxValuenumberMaximum allowed value (inclusive)
@SimpleType({ name: "percentage" })
class PercentageType extends NumberType {
@SimpleType.Attribute()
minValue = 0;

@SimpleType.Attribute()
maxValue = 100;
}

@SimpleType({ name: "positive-integer" })
class PositiveIntegerType extends IntegerType {
@SimpleType.Attribute()
minValue = 1;
}

Date / DateTime attributes

AttributeTypeDescription
minValuestringMinimum date/datetime value
maxValuestringMaximum date/datetime value
precisionMinPrecisionMinimum required precision (year, month, day, hours, minutes, seconds, ms)
precisionMaxPrecisionMaximum allowed precision
@SimpleType({ name: "future-date" })
class FutureDateType extends DateType {
@SimpleType.Attribute()
minValue = new Date().toISOString().slice(0, 10); // today
}

@SimpleType({ name: "birth-date" })
class BirthDateType extends DateType {
@SimpleType.Attribute()
precisionMin = "day";

@SimpleType.Attribute()
precisionMax = "day";

@SimpleType.Attribute()
maxValue = "2010-01-01";
}

Email attributes

AttributeTypeDescription
allowDisplayNamebooleanAllow "Display Name <email>" format
requireDisplayNamebooleanRequire display name prefix
utf8LocalPartbooleanAllow UTF-8 characters in the local part
allowIpDomainbooleanAllow IP address as domain
ignoreMaxLengthbooleanSkip the 254-character length limit
domainSpecificValidationbooleanEnable domain-specific rules
hostWhiteliststring[]Only allow these domains
hostBlackliststring[]Reject these domains
blacklistedCharsstringCharacters not allowed in the local part
@SimpleType({ name: "work-email" })
class WorkEmailType extends EmailType {
@SimpleType.Attribute()
hostWhitelist = ["company.com", "company.io"];
}

@SimpleType({ name: "public-email" })
class PublicEmailType extends EmailType {
@SimpleType.Attribute()
hostBlacklist = ["mailinator.com", "guerrillamail.com"];
}

UUID attributes

AttributeTypeDescription
version1 | 2 | 3 | 4 | 5 | 'all'Accepted UUID version(s)
@SimpleType({ name: "uuid-v4" })
class UuidV4Type extends UuidType {
@SimpleType.Attribute()
version = 4;
}

Extending a Custom Type

A custom SimpleType can itself be extended further, allowing layered constraints:

// Base: any text
@SimpleType({ name: "trimmed-string" })
class TrimmedStringType extends StringType {}

// More specific: short trimmed text
@SimpleType({ name: "short-text" })
class ShortTextType extends TrimmedStringType {
@SimpleType.Attribute()
maxLength = 255;
}

// Even more specific: a product title
@SimpleType({ name: "product-title" })
class ProductTitleType extends ShortTextType {
@SimpleType.Attribute()
minLength = 5;
}

Use sealed: true on an attribute to prevent downstream types from overriding it:

@SimpleType({ name: "iso-country-code" })
class IsoCountryCodeType extends StringType {
@SimpleType.Attribute({ sealed: true })
minLength = 2;

@SimpleType.Attribute({ sealed: true })
maxLength = 2;

@SimpleType.Attribute({ sealed: true })
pattern = /^[A-Z]{2}$/;
}

Check the inheritance chain at runtime with extendsFrom():

const slugType = document.node.getDataType("slug");
slugType.extendsFrom("string"); // true
slugType.extendsFrom(StringType); // true

Custom Codec Logic (DECODER / ENCODER)

For types that need fully custom validation or transformation logic — not just attribute constraints — you can implement [DECODER] and [ENCODER] symbol methods directly on the class.

These methods are called internally by generateCodec() and must return a Validator function from the valgen library.

import { DECODER, ENCODER } from '@opra/common';
import type { Validator } from 'valgen';
import { vg } from 'valgen';
import { SimpleType } from '@opra/common';
import { StringType } from '@opra/common';

@SimpleType({
name: 'creditcard',
description: 'A credit card number',
nameMappings: { js: 'string', json: 'string' },
})
class CreditCardType {
@SimpleType.Attribute({ description: 'Card provider (visa, mastercard …)' })
provider?: string;

protected [DECODER](properties?: Partial<this>): Validator {
return vg.isCreditCard({ coerce: true, provider: properties?.provider });
}

protected [ENCODER](properties?: Partial<this>): Validator {
return vg.isCreditCard({ coerce: true, provider: properties?.provider });
}
}

How it works

When generateCodec('decode' | 'encode') is called on a SimpleType, the framework:

  1. Walks up the inheritance chain starting from the current type.
  2. Calls the first [DECODER] (or [ENCODER]) method it finds.
  3. Passes the merged attribute properties as the first argument.
  4. Uses the returned Validator to process runtime values.

This means you can override just the decoder, just the encoder, or both — and the parent's implementation is the automatic fallback.

Decoder vs Encoder

[DECODER][ENCODER]
DirectionInbound (request parsing)Outbound (response serialization)
Typical useValidate + coerce user inputSerialize to wire format
DefaultFalls back to isAny if not foundFalls back to isAny if not found

In many types the encoder simply reuses the decoder (they validate the same way in both directions):

protected [ENCODER](properties?: Partial<this>): Validator {
return this[DECODER](properties);
}

Combining attributes with custom logic

You can mix @SimpleType.Attribute() declarations with [DECODER]/[ENCODER] — the attributes are passed in as properties at runtime:

import { DECODER, ENCODER } from '@opra/common';
import { vg } from 'valgen';
import { SimpleType, StringType } from '@opra/common';

@SimpleType({ name: 'phone' })
class PhoneType extends StringType {
@SimpleType.Attribute({ description: 'BCP 47 locale, e.g. "tr-TR"' })
locale?: string;

@SimpleType.Attribute({ description: 'Allow only mobile numbers' })
mobileOnly?: boolean;

protected [DECODER](properties?: Partial<this>): Validator {
return vg.isMobilePhone({
locale: properties?.locale,
strictMode: properties?.mobileOnly,
coerce: true,
});
}

protected [ENCODER](properties?: Partial<this>): Validator {
return this[DECODER](properties);
}
}

Attribute overrides passed to generateCodec() flow into properties unchanged:

const dt = document.node.getSimpleType('phone');

// Decode any phone number
const decode = dt.generateCodec('decode');

// Decode only Turkish mobile numbers
const decodeTR = dt.generateCodec('decode', undefined, {
locale: 'tr-TR',
mobileOnly: true,
});

Extending a type with a custom codec

When you extend a type that has a [DECODER], the child class inherits the parent's codec unless it defines its own:

@SimpleType({ name: 'uuid' })
class UuidType extends StringType {
@SimpleType.Attribute()
version?: 1 | 2 | 3 | 4 | 5 | 'all';

protected [DECODER](properties?: Partial<this>): Validator {
return vg.isUUID({ version: properties?.version, coerce: true });
}

protected [ENCODER](properties?: Partial<this>): Validator {
return this[DECODER](properties);
}
}

// Child inherits [DECODER] from UuidType, restricts version via attribute
@SimpleType({ name: 'uuid-v4' })
class UuidV4Type extends UuidType {
@SimpleType.Attribute({ sealed: true })
version = 4 as const;
}
note

DECODER is defined with Symbol.for('opra.type.decoder') (global registry) and ENCODER with Symbol('opra.type.encoder') (module-scoped). Always import them from @opra/common rather than re-creating the symbols.


Decorator Options Reference

@SimpleType(options?: SimpleType.Options)
OptionTypeDescription
namestringRegistry name. Defaults to the class name (lowercased).
descriptionstringHuman-readable description, included in the API schema export.
abstractbooleanMark as abstract — cannot be used directly in fields, only extended.
nameMappingsRecord<string, string>Maps the OPRA type name to language/format-specific names (e.g. { js: 'string', json: 'string' }).
examplesDataTypeExample[]Example values shown in schema documentation.
scopePatternstring | RegExp | (string | RegExp)[]Restricts which API scopes can use this type.
embeddedbooleanMark as embedded — not exposed as a standalone named type in the schema.
@(SimpleType({
name: "email",
description: "RFC 5321 email address",
nameMappings: { js: "string", json: "string" },
abstract: false,
embedded: false,
}).Example("user@example.com", "Standard email"))
class EmailType extends StringType {
@SimpleType.Attribute({ description: 'Allow "Display Name <email>" format' })
allowDisplayName?: boolean;
}
info

The .Example() chain method can be called multiple times to register multiple example values.