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
ApiDocumentand referenced by name from any field
Built-in Simple Types
OPRA ships two groups of built-in simple types.
Primitive types
| Name | JS equivalent | Description |
|---|---|---|
any | any | Accepts any value without validation |
bigint | bigint | Arbitrary-precision integer |
boolean | boolean | true / false |
integer | number | Whole numbers (no decimal part) |
null | null | The null value |
number | number | Integer and floating-point numbers |
object | object | Plain object (no field constraints) |
string | string | Sequence of characters |
Extended types
| Name | Description |
|---|---|
base64 | Base64-encoded binary data |
credit-card | Credit card number |
date | Calendar date (YYYY-MM-DD) |
datetime | Date and time without timezone |
datetime-tz | Date and time with timezone offset |
ean | European Article Number (barcode) |
email | RFC 5321 email address |
field-path | Dot-notation field path (e.g. address.city) |
filter | OPRA filter expression string |
iban | International Bank Account Number |
ip | IPv4 or IPv6 address |
mobile-phone | Mobile phone number |
object-id | Database object identifier (e.g. MongoDB ObjectId) |
operation-result | Structured operation result envelope |
time | Time of day (HH:mm:ss) |
url | URL (RFC 3986) |
uuid | Universally 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.
| Approach | When 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 type | Constraint 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;
}
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
| Attribute | Type | Description |
|---|---|---|
minLength | number | Minimum character count |
maxLength | number | Maximum character count |
pattern | string | RegExp | Regex the value must match |
patternName | string | Human-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
| Attribute | Type | Description |
|---|---|---|
minValue | number | Minimum allowed value (inclusive) |
maxValue | number | Maximum 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
| Attribute | Type | Description |
|---|---|---|
minValue | string | Minimum date/datetime value |
maxValue | string | Maximum date/datetime value |
precisionMin | Precision | Minimum required precision (year, month, day, hours, minutes, seconds, ms) |
precisionMax | Precision | Maximum 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
| Attribute | Type | Description |
|---|---|---|
allowDisplayName | boolean | Allow "Display Name <email>" format |
requireDisplayName | boolean | Require display name prefix |
utf8LocalPart | boolean | Allow UTF-8 characters in the local part |
allowIpDomain | boolean | Allow IP address as domain |
ignoreMaxLength | boolean | Skip the 254-character length limit |
domainSpecificValidation | boolean | Enable domain-specific rules |
hostWhitelist | string[] | Only allow these domains |
hostBlacklist | string[] | Reject these domains |
blacklistedChars | string | Characters 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
| Attribute | Type | Description |
|---|---|---|
version | 1 | 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:
- Walks up the inheritance chain starting from the current type.
- Calls the first
[DECODER](or[ENCODER]) method it finds. - Passes the merged attribute properties as the first argument.
- Uses the returned
Validatorto 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] | |
|---|---|---|
| Direction | Inbound (request parsing) | Outbound (response serialization) |
| Typical use | Validate + coerce user input | Serialize to wire format |
| Default | Falls back to isAny if not found | Falls 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;
}
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)
| Option | Type | Description |
|---|---|---|
name | string | Registry name. Defaults to the class name (lowercased). |
description | string | Human-readable description, included in the API schema export. |
abstract | boolean | Mark as abstract — cannot be used directly in fields, only extended. |
nameMappings | Record<string, string> | Maps the OPRA type name to language/format-specific names (e.g. { js: 'string', json: 'string' }). |
examples | DataTypeExample[] | Example values shown in schema documentation. |
scopePattern | string | RegExp | (string | RegExp)[] | Restricts which API scopes can use this type. |
embedded | boolean | Mark 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,
class EmailType extends StringType {
@SimpleType.Attribute({ description: 'Allow "Display Name <email>" format' })
allowDisplayName?: boolean;
}
The .Example() chain method can be called multiple times to register multiple example values.