Skip to content

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.

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

Instead of plain JavaScript objects from JSON.parse(), the JsonSerializer creates actual class instances:

import { JsonSerializer } from '@cratis/fundamentals';
// Traditional approach - loses type information
const json = `{"name":"John","age":30}`;
const user = JSON.parse(json); // Plain object, not a User
// With JsonSerializer - maintains type information
const user = JsonSerializer.deserialize(User, json); // Real User instance
console.log(user instanceof User); // ✅ true

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 immediately
console.log(user.getDisplayName()); // ✅ "John (30)"

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); // ✅ true

The runtime resolution flow is:

  1. Read _derivedTypeId from the payload.
  2. Build candidates from both field.derivatives and DerivedType.getDerivedTypesFor(field.type).
  3. 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).

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); // ✅ true
console.log(order.customerId instanceof Guid); // ✅ true

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}

Convert JSON strings back to typed instances:

const json = `{"name":"John","age":30}`;
const user = JsonSerializer.deserialize(User, json);
console.log(user instanceof User); // ✅ true
console.log(user.name); // "John"

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 objects

The JsonSerializer relies on the @field decorator to understand your class structure. For comprehensive details about decorators, field configuration, and advanced patterns, see:

For polymorphic serialization and working with multiple implementations of the same interface, see:

Ensure all serializable properties are decorated:

// ✅ Good - all fields decorated
export class Product {
@field(String)
name!: string;
@field(Number)
price!: number;
@field(Date)
createdAt!: Date;
}

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());

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 instances

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);
}

Deserialize data received from your .NET backend:

// Response from backend
const response = await fetch('/api/users/123');
const json = await response.json();
// Convert to typed instance
const user = JsonSerializer.deserialize(User, JSON.stringify(json));
const usersJson = await (await fetch('/api/users')).json();
const users = JsonSerializer.deserializeArray(User, JSON.stringify(usersJson));
// Single polymorphic field
const paymentJson = `{"method":{"amount":99.99,"_derivedTypeId":"..."}}`;
const payment = JsonSerializer.deserialize(Payment, paymentJson);
// Array of polymorphic types
const methodsJson = `[
{"amount":50,"_derivedTypeId":"credit-card"},
{"amount":40,"_derivedTypeId":"paypal"}
]`;
const methods = JsonSerializer.deserializeArray(IPaymentMethod, methodsJson);