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
@fielddecorator, including runtime type safety benefits and detailed usage patterns, see Field Decorator Documentation.
Overview
The frontend DerivedTypes system consists of:
@derivedTypedecorator: Marks classes with unique identifiers matching the backendDerivedTypeclass: Manages metadata for derived type identifiersJsonSerializer: Handles serialization/deserialization with type information@fielddecorator: Defines fields with optional derivative type supportFieldclass: Represents field metadata including derivative type information
Key Concepts
Type Metadata System
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
Decorator-Based Configuration
Unlike the backend's attribute-based approach, the frontend uses decorators to mark classes and fields with metadata.
Uniqueness Requirements
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.
Usage
1. Define Your Interface
export interface IPaymentMethod {
amount: number;
}
2. Create Derived Types
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
export class Order {
@field(String)
orderId!: string;
@field(Object, false, [CreditCard, PayPal])
paymentMethod!: IPaymentMethod;
@field(Object, true, [CreditCard, PayPal])
alternativePayments!: IPaymentMethod[];
}
Serialization and Deserialization
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
_derivedTypeIdduring serialization based on the@derivedTypedecorator - Using
_derivedTypeIdto instantiate the correct derived type during deserialization
Field Decorator Integration
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.
Serialization Format
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
@derivedTypedecorator - Used during deserialization to instantiate the correct class
Advanced Scenarios
Complex Nested Objects
export class ShoppingCart {
@field(String)
cartId!: string;
@field(Order, true)
orders!: Order[];
@field(Object, false, [CreditCard, PayPal])
defaultPayment!: IPaymentMethod;
}
Mixed Collections
export class PaymentHistory {
@field(Object, true, [CreditCard, PayPal, BankTransfer])
transactions!: IPaymentMethod[];
}
Well-Known Types
The system automatically handles common types:
@field(Date)
createdAt!: Date;
@field(Guid)
id!: Guid;
@field(Boolean)
isActive!: boolean;
Error Handling
Common Issues
- Missing
@derivedTypedecorator: Classes won't be recognized during deserialization - Mismatched IDs: Frontend and backend derived type IDs must match exactly
- Missing field decorators: Properties without
@fieldwon't be serialized/deserialized - Missing derivatives list: Polymorphic fields need the derivatives parameter
Debugging Tips
// 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);
Best Practices
1. Consistent Identifiers
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 { }
2. Centralize Type Identifiers
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.
3. Complete Field Decoration
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;
}
4. Explicit Derivative Lists
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;
5. Interface Consistency
Keep interfaces synchronized between frontend and backend:
// Frontend interface should match backend interface
export interface IPaymentMethod {
amount: number; // matches decimal Amount in C#
}
6. Testing
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](./json_serializer.md) for comprehensive testing examples.
Type System Integration
Working with TypeScript's Type System
// 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 Serialization
// Generic deserialization
function deserializePayments<T extends IPaymentMethod>(
json: string,
targetType: Constructor<T>
): T[] {
return JsonSerializer.deserializeArray(targetType, json);
}
const creditCards = deserializePayments(json, CreditCard);
Runtime Type Safety
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
Performance Considerations
- 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
Integration with Build Tools
TypeScript Configuration
Ensure your tsconfig.json includes:
{
"compilerOptions": {
"experimentalDecorators": true,
"emitDecoratorMetadata": true
}
}
Bundling Considerations
When using module bundlers, ensure reflect-metadata is properly included:
import 'reflect-metadata';
// Import this before any decorated classes
See Also
- JsonSerializer - Core serialization utility for type-safe JSON conversion
- Field Decorator - Comprehensive guide to the
@fielddecorator system and runtime type safety