Custom Converters
Custom converters allow you to define how specific properties are serialized and deserialized. This is particularly useful for third-party types like DateTime from Luxon, Decimal from decimal.js, or any custom data structures that require special handling.
Basic Converter
A converter is an object with two methods:
serialize: Converts a TypeScript value to its JSON representationdeserialize: Converts a JSON value back to its TypeScript representation
import { Converter, ConverterContext } from 's7e';
const dateConverter: Converter<Date, string> = {
serialize: (value: Date, context: ConverterContext) => value.toISOString(),
deserialize: (value: string, context: ConverterContext) => new Date(value)
};Using Converters with @JsonProperty
Simply add the converter option to your @JsonProperty decorator:
import { JsonProperty, S7e, Converter, ConverterContext } from 's7e';
// Define your converter
const dateConverter: Converter<Date, string> = {
serialize: (value: Date, context: ConverterContext) => value.toISOString(),
deserialize: (value: string, context: ConverterContext) => new Date(value)
};
class Meeting {
@JsonProperty({ name: 'title', type: String })
public title: string;
@JsonProperty({ name: 'scheduledAt', converter: dateConverter })
public scheduledAt: Date;
constructor(title: string = '', scheduledAt: Date = new Date()) {
this.title = title;
this.scheduledAt = scheduledAt;
}
}
// Usage
const meeting = new Meeting('Team Standup', new Date('2024-12-25T10:00:00Z'));
const obj = S7e.serialize(meeting);
console.log(obj);
// { title: 'Team Standup', scheduledAt: '2024-12-25T10:00:00.000Z' }
const deserialized = S7e.deserialize(obj, Meeting);
console.log(deserialized.scheduledAt instanceof Date); // trueThird-Party Libraries
Luxon DateTime
import { DateTime } from 'luxon';
import { Converter, ConverterContext, JsonProperty, S7e } from 's7e';
const dateTimeConverter: Converter<DateTime, string> = {
serialize: (value: DateTime, context: ConverterContext) => value.toISO(),
deserialize: (value: string, context: ConverterContext) => DateTime.fromISO(value)
};
class Event {
@JsonProperty({ name: 'name', type: String })
public name: string;
@JsonProperty({ name: 'eventDate', converter: dateTimeConverter })
public eventDate: DateTime;
constructor(name: string = '', eventDate?: DateTime) {
this.name = name;
this.eventDate = eventDate ?? DateTime.now();
}
}
const event = new Event('Conference', DateTime.fromObject({ year: 2024, month: 6, day: 15 }));
const obj = S7e.serialize(event);
const restored = S7e.deserialize(obj, Event);Decimal.js
import { Decimal } from 'decimal.js';
import { Converter, ConverterContext, JsonProperty, S7e } from 's7e';
const decimalConverter: Converter<Decimal, string> = {
serialize: (value: Decimal, context: ConverterContext) => value.toString(),
deserialize: (value: string, context: ConverterContext) => new Decimal(value)
};
class Product {
@JsonProperty({ name: 'name', type: String })
public name: string;
@JsonProperty({ name: 'price', converter: decimalConverter })
public price: Decimal;
constructor(name: string = '', price?: Decimal) {
this.name = name;
this.price = price ?? new Decimal(0);
}
}Arrays with Converters
Converters work seamlessly with arrays:
import { Converter, ConverterContext, JsonProperty, S7e } from 's7e';
const dateConverter: Converter<Date, string> = {
serialize: (value: Date, context: ConverterContext) => value.toISOString(),
deserialize: (value: string, context: ConverterContext) => new Date(value)
};
class Schedule {
@JsonProperty({ name: 'name', type: String })
public name: string;
@JsonProperty({ name: 'dates', converter: dateConverter })
public dates: Date[];
constructor(name: string = '', dates: Date[] = []) {
this.name = name;
this.dates = dates;
}
}
const schedule = new Schedule('Project Milestones', [
new Date('2024-01-01'),
new Date('2024-06-15'),
new Date('2024-12-31')
]);
const obj = S7e.serialize(schedule);
console.log(obj);
// {
// name: 'Project Milestones',
// dates: ['2024-01-01T00:00:00.000Z', '2024-06-15T00:00:00.000Z', '2024-12-31T00:00:00.000Z']
// }Optional Properties with Converters
Converters work with optional properties:
import { Converter, ConverterContext, JsonProperty, S7e } from 's7e';
const dateConverter: Converter<Date, string> = {
serialize: (value: Date, context: ConverterContext) => value.toISOString(),
deserialize: (value: string, context: ConverterContext) => new Date(value)
};
class Task {
@JsonProperty({ name: 'title', type: String })
public title: string;
@JsonProperty({ name: 'dueDate', converter: dateConverter, optional: true })
public dueDate?: Date;
constructor(title: string = '', dueDate?: Date) {
this.title = title;
this.dueDate = dueDate;
}
}
// Without due date
const task1 = new Task('Buy groceries');
const obj1 = S7e.serialize(task1);
console.log(obj1); // { title: 'Buy groceries' }
// With due date
const task2 = new Task('Submit report', new Date('2024-12-31'));
const obj2 = S7e.serialize(task2);
console.log(obj2); // { title: 'Submit report', dueDate: '2024-12-31T00:00:00.000Z' }Complex Type Conversion
You can use converters for any type conversion:
import { Converter, JsonProperty, S7e } from 's7e';
// Convert numbers to formatted strings
const currencyConverter: Converter<number, string> = {
serialize: (value: number, context: ConverterContext) => `$${value.toFixed(2)}`,
deserialize: (value: string, context: ConverterContext) => parseFloat(value.replace('$', ''))
};
class Invoice {
@JsonProperty({ name: 'id', type: Number })
public id: number;
@JsonProperty({ name: 'amount', converter: currencyConverter })
public amount: number;
constructor(id: number = 0, amount: number = 0) {
this.id = id;
this.amount = amount;
}
}
const invoice = new Invoice(1, 99.99);
const obj = S7e.serialize(invoice);
console.log(obj); // { id: 1, amount: '$99.99' }
const restored = S7e.deserialize(obj, Invoice);
console.log(restored.amount); // 99.99Converter Priority
When both type and converter are specified, the converter takes precedence:
import { Converter, JsonProperty, S7e } from 's7e';
const customConverter: Converter<string, number> = {
serialize: (value: string, context) => value.length,
deserialize: (value: number, context) => 'x'.repeat(value)
};
class Example {
// Converter takes precedence over type
@JsonProperty({ name: 'data', type: String, converter: customConverter })
public data: string;
constructor(data: string = '') {
this.data = data;
}
}
const example = new Example('hello');
const obj = S7e.serialize(example);
console.log(obj); // { data: 5 }
const restored = S7e.deserialize(obj, Example);
console.log(restored.data); // 'xxxxx'Context-Aware Converters
Converters always receive context information, including the parent object and property name. This enables advanced scenarios where conversion logic depends on other properties of the object.
Converter Context
The context object is always provided with both required properties:
parent: The parent object containing the property being convertedpropertyName: The name of the property being converted (JSON name from the decorator)
Note: Both parent and propertyName are always provided, including for array elements.
Basic Context Usage
import { Converter, ConverterContext, JsonProperty, S7e } from 's7e';
const contextAwareConverter: Converter<string, string> = {
serialize: (value: string, context: ConverterContext) => {
// Access context information - always available
console.log('Parent object:', context.parent);
console.log('Property name:', context.propertyName);
return value.toUpperCase();
},
deserialize: (value: string, context: ConverterContext) => {
return value.toLowerCase();
}
};
class Document {
@JsonProperty({ name: 'title', type: String })
public title: string;
@JsonProperty({ name: 'content', converter: contextAwareConverter })
public content: string;
constructor(title: string = '', content: string = '') {
this.title = title;
this.content = content;
}
}
const doc = new Document('Test', 'hello');
const obj = S7e.serialize(doc);
// Context will include parent (the doc instance) and propertyName ('content')Conditional Conversion Based on Parent
A practical example where conversion logic depends on other properties:
import { Converter, ConverterContext, JsonProperty, S7e } from 's7e';
const conditionalConverter: Converter<number, string> = {
serialize: (value: number, context: ConverterContext) => {
// Access parent object to make conversion decision
const parent = context.parent as any;
if (parent && parent.formatAsPercentage) {
return `${(value * 100).toFixed(0)}%`;
}
return value.toString();
},
deserialize: (value: string, context: ConverterContext) => {
if (value.endsWith('%')) {
return parseFloat(value.replace('%', '')) / 100;
}
return parseFloat(value);
}
};
class Report {
@JsonProperty({ name: 'name', type: String })
public name: string;
@JsonProperty({ name: 'value', converter: conditionalConverter })
public value: number;
// This property affects how 'value' is serialized
public formatAsPercentage: boolean;
constructor(name: string = '', value: number = 0, formatAsPercentage: boolean = false) {
this.name = name;
this.value = value;
this.formatAsPercentage = formatAsPercentage;
}
}
// With percentage formatting
const report1 = new Report('Sales', 0.85, true);
const obj1 = S7e.serialize(report1);
console.log(obj1); // { name: 'Sales', value: '85%' }
// Without percentage formatting
const report2 = new Report('Count', 42, false);
const obj2 = S7e.serialize(report2);
console.log(obj2); // { name: 'Count', value: '42' }Context Validation Example
Use context to validate relationships between properties:
import { Converter, ConverterContext, JsonProperty, S7e } from 's7e';
const validatedConverter: Converter<number, number> = {
serialize: (value: number, context: ConverterContext) => {
const parent = context.parent as any;
if (parent && parent.maxValue && value > parent.maxValue) {
throw new Error(`Value ${value} exceeds maximum ${parent.maxValue}`);
}
return value;
},
deserialize: (value: number, context: ConverterContext) => {
const parent = context.parent as any;
if (parent && parent.maxValue && value > parent.maxValue) {
throw new Error(`Value ${value} exceeds maximum ${parent.maxValue}`);
}
return value;
}
};
class RangedValue {
@JsonProperty({ name: 'maxValue', type: Number })
public maxValue: number;
@JsonProperty({ name: 'currentValue', converter: validatedConverter })
public currentValue: number;
constructor(maxValue: number = 100, currentValue: number = 0) {
this.maxValue = maxValue;
this.currentValue = currentValue;
}
}
// Valid
const valid = new RangedValue(100, 50);
S7e.serialize(valid); // Works fine
// Invalid - throws error
const invalid = new RangedValue(100, 150);
// S7e.serialize(invalid); // Throws: Value 150 exceeds maximum 100Best Practices
Type Safety: Use TypeScript generics to ensure type safety:
typescriptconst converter: Converter<MyType, string> = { ... };Error Handling: Consider adding error handling in converters for invalid data:
typescriptconst safeConverter: Converter<Date, string> = { serialize: (value: Date, context: ConverterContext) => value.toISOString(), deserialize: (value: string, context: ConverterContext) => { const date = new Date(value); if (isNaN(date.getTime())) { throw new Error(`Invalid date string: ${value}`); } return date; } };Reusability: Define converters once and reuse them across your application:
typescript// converters.ts export const dateConverter: Converter<Date, string> = { ... }; export const dateTimeConverter: Converter<DateTime, string> = { ... };Null Handling: The library automatically handles null values in arrays, but you may want to handle them explicitly in your converters if needed.
Notes
- Converters work with both single values and arrays
- When using converters, the
typeparameter becomes optional (though you can still specify it for documentation) - Converters are applied before any type validation
- Undefined optional properties are skipped during serialization, even with converters