Skip to content

DerivedTypes Serialization (Frontend)

The DerivedTypes serialization system in the TypeScript frontend provides seamless polymorphic serialization that works hand-in-hand with the backend .NET implementation. This system allows you to deserialize JSON objects into their correct TypeScript class instances based on type identifiers.

Note: This documentation focuses on derived types and polymorphic serialization. For comprehensive information about the @field decorator, including runtime type safety benefits and detailed usage patterns, see Field Decorator Documentation.

The frontend DerivedTypes system consists of:

  • @derivedType decorator: Marks classes with unique identifiers matching the backend
  • DerivedType class: Manages metadata for derived type identifiers
  • JsonSerializer: Handles serialization/deserialization with type information
  • @field decorator: Defines fields with optional derivative type support
  • Field class: Represents field metadata including derivative type information

The frontend uses TypeScript decorators and reflect-metadata to store type information:

  • Derived Type IDs: Unique string identifiers that must match exactly with backend [DerivedType] attributes
  • Field Metadata: Information about properties including their types and possible derivatives
  • Runtime Type Resolution: Dynamic instantiation of correct classes during deserialization

At runtime, the deserializer resolves derived types from two candidate sources:

  • field.derivatives provided by @field(..., derivatives)
  • Runtime registrations from DerivedType.getDerivedTypesFor(field.type)

The runtime registrations are populated by the @derivedType decorator by walking the prototype chain. Because of this, class-based hierarchies (base class + subclasses) are discovered automatically.

For interface-shaped polymorphism, remember that TypeScript interfaces are erased at runtime. In those cases, use explicit field.derivatives, or pass targetType to @derivedType so the type is registered against a concrete runtime constructor.

Unlike the backend’s attribute-based approach, the frontend uses decorators to mark classes and fields with metadata.

Derived type IDs must be unique per interface to enable proper polymorphic deserialization. While you can use any string format, using descriptive identifiers is recommended for readability:

Good approach - Descriptive and unique:

@derivedType('credit-card')
export class CreditCard implements IPaymentMethod { }
@derivedType('paypal')
export class PayPal implements IPaymentMethod { }

Alternative approach - Using GUIDs for maximum uniqueness:

@derivedType('550e8400-e29b-41d4-a716-446655440001')
export class CreditCard implements IPaymentMethod { }
@derivedType('550e8400-e29b-41d4-a716-446655440002')
export class PayPal implements IPaymentMethod { }

Note on GUIDs: While GUIDs guarantee uniqueness, they sacrifice readability. If using GUIDs, ensure they are stored in shared constants or configuration to maintain consistency across your codebase and backend services.

export interface IPaymentMethod {
amount: number;
}
import { derivedType } from '@cratis/fundamentals';
import { field } from '@cratis/fundamentals';
@derivedType('credit-card')
export class CreditCard implements IPaymentMethod {
@field(Number)
amount!: number;
@field(String)
cardNumber!: string;
@field(String)
expiryDate!: string;
}
@derivedType('paypal')
export class PayPal implements IPaymentMethod {
@field(Number)
amount!: number;
@field(String)
email!: string;
}

3. Configure Parent Classes with Derivatives

Section titled “3. Configure Parent Classes with Derivatives”
export class Order {
@field(String)
orderId!: string;
@field(Object, false, [CreditCard, PayPal])
paymentMethod!: IPaymentMethod;
@field(Object, true, [CreditCard, PayPal])
alternativePayments!: IPaymentMethod[];
}
If `paymentMethod` is typed as a concrete base class (instead of interface-only shape), runtime
registration from `@derivedType` can often resolve the correct subtype without an explicit derivatives list.
Keeping the list is still valid and can be useful for clarity.

For detailed information about serialization and deserialization operations, including usage examples and API details, see the JsonSerializer Documentation.

When working with polymorphic types, the JsonSerializer automatically handles:

  • Adding _derivedTypeId during serialization based on the @derivedType decorator
  • Using _derivedTypeId to instantiate the correct derived type during deserialization

The @field decorator is essential for defining serializable properties. For comprehensive documentation on the @field decorator including all parameters, usage patterns, and runtime type safety benefits, see Field Decorator Documentation.

The frontend produces the same JSON format as the backend:

{
"paymentMethod": {
"amount": 99.99,
"cardNumber": "****-1234",
"expiryDate": "12/25",
"_derivedTypeId": "credit-card"
}
}

The _derivedTypeId property is automatically:

  • Added during serialization based on the class’s @derivedType decorator
  • Used during deserialization to instantiate the correct class
export class ShoppingCart {
@field(String)
cartId!: string;
@field(Order, true)
orders!: Order[];
@field(Object, false, [CreditCard, PayPal])
defaultPayment!: IPaymentMethod;
}
export class PaymentHistory {
@field(Object, true, [CreditCard, PayPal, BankTransfer])
transactions!: IPaymentMethod[];
}

The system automatically handles common types:

@field(Date)
createdAt!: Date;
@field(Guid)
id!: Guid;
@field(Boolean)
isActive!: boolean;
  1. Missing @derivedType decorator: Classes won’t be recognized during deserialization
  2. Mismatched IDs: Frontend and backend derived type IDs must match exactly
  3. Missing field decorators: Properties without @field won’t be serialized/deserialized
  4. Missing derivatives list: Polymorphic fields need the derivatives parameter
// Check if a type has derived type metadata
const derivedTypeId = DerivedType.get(CreditCard);
console.log(`CreditCard ID: ${derivedTypeId}`);
// Inspect field metadata
const fields = Fields.getFieldsForType(Order);
console.log('Order fields:', fields);

Ensure derived type IDs match exactly between frontend and backend:

// Frontend
@derivedType('credit-card')
export class CreditCard implements IPaymentMethod { }
// Backend
[DerivedType("credit-card")]
public class CreditCard : IPaymentMethod { }

Store derived type IDs in a single location to avoid duplication and ensure consistency:

constants/derivedTypeIds.ts
export const DERIVED_TYPE_IDS = {
CREDIT_CARD: 'credit-card',
PAYPAL: 'paypal',
BANK_TRANSFER: 'bank-transfer',
} as const;
// usage
@derivedType(DERIVED_TYPE_IDS.CREDIT_CARD)
export class CreditCard implements IPaymentMethod { }
@derivedType(DERIVED_TYPE_IDS.PAYPAL)
export class PayPal implements IPaymentMethod { }

Why centralize? These “magic strings” are critical for serialization consistency. Centralizing them in one place prevents duplicates, makes them easy to maintain, and ensures frontend and backend stay synchronized.

Decorate all serializable properties:

export class CreditCard implements IPaymentMethod {
@field(Number)
amount!: number;
@field(String)
cardNumber!: string;
// ❌ This won't be serialized without @field
private internalId: string = '';
// ✅ Private fields that should be serialized need @field too
@field(String)
private securityCode!: string;
}

Always specify derivatives for polymorphic fields:

// ✅ Good - explicit derivatives list
@field(Object, false, [CreditCard, PayPal])
paymentMethod!: IPaymentMethod;
// ❌ Bad - missing derivatives, won't deserialize correctly
@field(Object)
paymentMethod!: IPaymentMethod;

Keep interfaces synchronized between frontend and backend:

// Frontend interface should match backend interface
export interface IPaymentMethod {
amount: number; // matches decimal Amount in C#
}

Test serialization round-trips:

## Testing
For testing serialization round-trips with derived types, create tests that verify:
1. Polymorphic types deserialize to the correct class
2. Derived type IDs are preserved during serialization
3. Frontend IDs match backend IDs exactly
Refer to the [JsonSerializer Documentation](/fundamentals/typescript/serialization/json_serializer/) for comprehensive testing examples.
// Type-safe polymorphic handling
function processPayment(payment: IPaymentMethod): void {
if (payment instanceof CreditCard) {
// TypeScript knows this is CreditCard
console.log(`Processing card: ${payment.cardNumber}`);
} else if (payment instanceof PayPal) {
// TypeScript knows this is PayPal
console.log(`Processing PayPal: ${payment.email}`);
}
}
// Type guards for runtime checking
function isCreditCard(payment: IPaymentMethod): payment is CreditCard {
return payment instanceof CreditCard;
}
// Generic deserialization
function deserializePayments<T extends IPaymentMethod>(
json: string,
targetType: Constructor<T>
): T[] {
return JsonSerializer.deserializeArray(targetType, json);
}
const creditCards = deserializePayments(json, CreditCard);

The @field decorator system provides significant runtime type safety benefits beyond TypeScript’s compile-time checking. For detailed information about runtime type safety, including comprehensive examples and comparisons, see the Field Decorator Documentation.

With derived types, you benefit from:

  • Automatic polymorphic type resolution at runtime
  • Correct class instantiation based on _derivedTypeId
  • Full method access on all polymorphic instances
  • Type-safe instanceof checks across derived types
  • Reflection overhead: Metadata lookup happens at runtime
  • Type scanning: Keep derivative lists focused to avoid unnecessary type checking
  • Memory usage: Metadata is stored per-class, not per-instance

Ensure your tsconfig.json includes:

{
"compilerOptions": {
"experimentalDecorators": true,
"emitDecoratorMetadata": true
}
}

When using module bundlers, ensure reflect-metadata is properly included:

import 'reflect-metadata';
// Import this before any decorated classes
  • JsonSerializer - Core serialization utility for type-safe JSON conversion
  • Field Decorator - Comprehensive guide to the @field decorator system and runtime type safety