JsonSerializer
The JsonSerializer is the core utility for serializing and deserializing TypeScript objects to and from JSON. It works seamlessly with the @field decorator system and @derivedType decorators to provide type-safe, round-trip serialization that preserves runtime types and method access.
Overview
Section titled “Overview”The JsonSerializer bridges the gap between JSON data and strongly-typed TypeScript class instances, enabling:
- True Object Deserialization: JSON data becomes actual class instances, not plain objects
- Method Preservation: All class methods remain accessible on deserialized instances
- Type Safety: Automatic type resolution and validation during serialization/deserialization
- Polymorphic Support: Automatic handling of derived types and interface implementations
- Seamless Backend Integration: Compatible with .NET backend serialization format
Key Benefits
Section titled “Key Benefits”1. Type-Safe Runtime Objects
Section titled “1. Type-Safe Runtime Objects”Instead of plain JavaScript objects from JSON.parse(), the JsonSerializer creates actual class instances:
import { JsonSerializer } from '@cratis/fundamentals';
// Traditional approach - loses type informationconst json = `{"name":"John","age":30}`;const user = JSON.parse(json); // Plain object, not a User
// With JsonSerializer - maintains type informationconst user = JsonSerializer.deserialize(User, json); // Real User instanceconsole.log(user instanceof User); // ✅ true2. Business Logic Access
Section titled “2. Business Logic Access”Methods defined on your classes are immediately available on deserialized objects:
export class User { @field(String) name!: string;
@field(Number) age!: number;
getDisplayName(): string { return `${this.name} (${this.age})`; }}
const json = `{"name":"John","age":30}`;const user = JsonSerializer.deserialize(User, json);
// Method available immediatelyconsole.log(user.getDisplayName()); // ✅ "John (30)"3. Automatic Polymorphic Resolution
Section titled “3. Automatic Polymorphic Resolution”When working with interfaces or base classes, the JsonSerializer automatically deserializes to the correct derived type:
// Different payment types deserialized to their correct classes@derivedType('credit-card-id')export class CreditCard implements IPaymentMethod { }
@derivedType('paypal-id')export class PayPal implements IPaymentMethod { }
const json = `{"method":{"amount":99.99,"_derivedTypeId":"credit-card-id"}}`;const payment = JsonSerializer.deserialize(Payment, json);
console.log(payment.method instanceof CreditCard); // ✅ trueThe runtime resolution flow is:
- Read
_derivedTypeIdfrom the payload. - Build candidates from both
field.derivativesandDerivedType.getDerivedTypesFor(field.type). - Match candidate identifier and deserialize into the matching constructor.
This makes class inheritance work with runtime-only registration from @derivedType.
For interface-only polymorphism, include explicit derivatives on @field (or use @derivedType
with an explicit targetType constructor).
4. Proper Type Conversion
Section titled “4. Proper Type Conversion”Complex types like Date and Guid are properly converted during deserialization:
export class Order { @field(String) orderId!: string;
@field(Date) createdAt!: Date;
@field(Guid) customerId!: Guid;}
const json = `{"orderId":"123","createdAt":"2023-01-15T10:30:00Z","customerId":"550e8400-e29b-41d4-a716-446655440000"}`;const order = JsonSerializer.deserialize(Order, json);
console.log(order.createdAt instanceof Date); // ✅ trueconsole.log(order.customerId instanceof Guid); // ✅ trueBasic Serialization
Section titled “Basic Serialization”Convert class instances to JSON:
const user = new User();user.name = "John";user.age = 30;
const json = JsonSerializer.serialize(user);console.log(json); // {"name":"John","age":30}Basic Deserialization
Section titled “Basic Deserialization”Convert JSON strings back to typed instances:
const json = `{"name":"John","age":30}`;const user = JsonSerializer.deserialize(User, json);
console.log(user instanceof User); // ✅ trueconsole.log(user.name); // "John"Array Deserialization
Section titled “Array Deserialization”Handle arrays of typed objects:
const json = `[ {"name":"John","age":30}, {"name":"Jane","age":25}]`;
const users = JsonSerializer.deserializeArray(User, json);// Array of User instances, not plain objectsIntegration with Field Decorators
Section titled “Integration with Field Decorators”The JsonSerializer relies on the @field decorator to understand your class structure. For comprehensive details about decorators, field configuration, and advanced patterns, see:
- Field Decorator Documentation - Complete guide to the
@fielddecorator system, runtime type safety, and advanced serialization patterns
Integration with Derived Types
Section titled “Integration with Derived Types”For polymorphic serialization and working with multiple implementations of the same interface, see:
- DerivedTypes Serialization Documentation - Full guide to the
@derivedTypedecorator, polymorphic deserialization, and multi-type scenarios
Best Practices
Section titled “Best Practices”1. Complete Field Declaration
Section titled “1. Complete Field Declaration”Ensure all serializable properties are decorated:
// ✅ Good - all fields decoratedexport class Product { @field(String) name!: string;
@field(Number) price!: number;
@field(Date) createdAt!: Date;}2. Test Round-Trip Serialization
Section titled “2. Test Round-Trip Serialization”Verify your objects serialize and deserialize correctly:
const original = new User();original.name = "John";original.age = 30;
const json = JsonSerializer.serialize(original);const deserialized = JsonSerializer.deserialize(User, json);
expect(deserialized.name).toBe(original.name);expect(deserialized.age).toBe(original.age);expect(deserialized.getDisplayName()).toBe(original.getDisplayName());3. Handle Complex Nested Objects
Section titled “3. Handle Complex Nested Objects”The JsonSerializer automatically handles nested typed objects:
export class Order { @field(String) orderId!: string;
@field(Customer) // Nested typed object customer!: Customer;
@field(Product, true) // Array of typed objects items!: Product[];}
const order = JsonSerializer.deserialize(Order, json);// customer is a Customer instance, items are Product instances4. Error Handling
Section titled “4. Error Handling”Wrap serialization calls in try-catch blocks for production code:
try { const user = JsonSerializer.deserialize(User, jsonData); // Process user} catch (error) { console.error('Deserialization failed:', error);}Common Scenarios
Section titled “Common Scenarios”Backend Integration
Section titled “Backend Integration”Deserialize data received from your .NET backend:
// Response from backendconst response = await fetch('/api/users/123');const json = await response.json();
// Convert to typed instanceconst user = JsonSerializer.deserialize(User, JSON.stringify(json));Working with Collections
Section titled “Working with Collections”const usersJson = await (await fetch('/api/users')).json();const users = JsonSerializer.deserializeArray(User, JSON.stringify(usersJson));Polymorphic API Responses
Section titled “Polymorphic API Responses”// Single polymorphic fieldconst paymentJson = `{"method":{"amount":99.99,"_derivedTypeId":"..."}}`;const payment = JsonSerializer.deserialize(Payment, paymentJson);
// Array of polymorphic typesconst methodsJson = `[ {"amount":50,"_derivedTypeId":"credit-card"}, {"amount":40,"_derivedTypeId":"paypal"}]`;const methods = JsonSerializer.deserializeArray(IPaymentMethod, methodsJson);See Also
Section titled “See Also”- Field Decorator - Decorator system for field serialization configuration
- Derived Types - Polymorphic type handling and interface implementations
- Serialization Overview - Complete serialization system documentation