API Stability Notice
Macroforge is under active development. The API is not yet stable and may change between versions. Some documentation sections may be outdated.
Deserialize
The Deserialize macro generates JSON deserialization methods with cycle and
forward-reference support, plus comprehensive runtime validation. This enables
safe parsing of complex JSON structures including circular references.
Generated Output
| Type | Generated Code | Description |
|---|---|---|
| Class | classNameDeserialize(input) + static deserialize(input) | Standalone function + static factory method |
| Enum | enumNameDeserialize(input), enumNameDeserializeWithContext(data), enumNameIs(value) | Standalone functions |
| Interface | interfaceNameDeserialize(input), etc. | Standalone functions |
| Type Alias | typeNameDeserialize(input), etc. | Standalone functions |
Return Type
All public deserialization methods return Result<T, Array<{ field: string; message: string }>>:
Result.ok(value)- Successfully deserialized valueResult.err(errors)- Array of validation errors with field names and messages
Cycle/Forward-Reference Support
Uses deferred patching to handle references:
- When encountering
{ "__ref": id }, returns aPendingRefmarker - Continues deserializing other fields
- After all objects are created,
ctx.applyPatches()resolves all pending references
References only apply to object-shaped, serializable values. The generator avoids probing for __ref on primitive-like fields (including literal unions and T | null where T is primitive-like),
and it parses Date / Date | null from ISO strings without treating them as references.
Validation
The macro supports 30+ validators via @serde(validate(...)):
String Validators
email,url,uuid- Format validationminLength(n),maxLength(n),length(n)- Length constraintspattern("regex")- Regular expression matchingnonEmpty,trimmed,lowercase,uppercase- String properties
Number Validators
gt(n),gte(n),lt(n),lte(n),between(min, max)- Range checksint,positive,nonNegative,finite- Number properties
Array Validators
minItems(n),maxItems(n),itemsCount(n)- Collection size
Date Validators
validDate,afterDate("ISO"),beforeDate("ISO")- Date validation
Field-Level Options
The @serde decorator supports:
skip/skipDeserializing- Exclude field from deserializationrename = "jsonKey"- Read from different JSON propertydefault/default = expr- Use default value if missingflatten- Read fields from parent object levelvalidate(...)- Apply validators
Container-Level Options
denyUnknownFields- Error on unrecognized JSON propertiesrenameAll = "camelCase"- Apply naming convention to all fields
Union Type Deserialization
Union types are deserialized based on their member types:
Literal Unions
For unions of literal values ("A" | "B" | 123), the value is validated against
the allowed literals directly.
Primitive Unions
For unions containing primitive types (string | number), the deserializer uses typeof checks to validate the value type. No __type discriminator is needed.
Class/Interface Unions
For unions of serializable types (User | Admin), the deserializer requires a __type field in the JSON to dispatch to the correct type’s deserializeWithContext method.
Generic Type Parameters
For generic unions like type Result<T> = T | Error, the generic type parameter T is passed through as-is since its concrete type is only known at the call site.
Mixed Unions
Mixed unions (e.g., string | Date | User) check in order:
- Literal values
- Primitives (via
typeof) - Date (via
instanceofor ISO string parsing) - Serializable types (via
__typedispatch) - Generic type parameters (pass-through)
Example
/** @derive(Deserialize) @serde({ denyUnknownFields: true }) */
class User {
id: number;
/** @serde({ validate: { email: true, maxLength: 255 } }) */
email: string;
/** @serde({ default: "guest" }) */
name: string;
/** @serde({ validate: { positive: true } }) */
age?: number;
}class User {
id: number;
email: string;
name: string;
age?: number;
}Generated output:
class User {
id: number;
email: string;
name: string;
age?: number;
}Generated output:
import { DeserializeContext } from 'macroforge/serde';
import { DeserializeError } from 'macroforge/serde';
import type { DeserializeOptions } from 'macroforge/serde';
import { PendingRef } from 'macroforge/serde';
/** @serde({ denyUnknownFields: true }) */
class User {
id: number;
email: string;
name: string;
age?: number;
constructor(props: {
id: number;
email: string;
name?: string;
age?: number;
}) {
this.id = props.id;
this.email = props.email;
this.name = props.name as string;
this.age = props.age as number;
}
/**
* Deserializes input to an instance of this class.
* Automatically detects whether input is a JSON string or object.
* @param input - JSON string or object to deserialize
* @param opts - Optional deserialization options
* @returns Result containing the deserialized instance or validation errors
*/
static deserialize(
input: unknown,
opts?: DeserializeOptions
): Result<
User,
Array<{
field: string;
message: string;
}>
> {
try {
// Auto-detect: if string, parse as JSON first
const data = typeof input === 'string' ? JSON.parse(input) : input;
const ctx = DeserializeContext.create();
const resultOrRef = User.deserializeWithContext(data, ctx);
if (PendingRef.is(resultOrRef)) {
return Result.err([
{
field: '_root',
message: 'User.deserialize: root cannot be a forward reference'
}
]);
}
ctx.applyPatches();
if (opts?.freeze) {
ctx.freezeAll();
}
return Result.ok(resultOrRef);
} catch (e) {
if (e instanceof DeserializeError) {
return Result.err(e.errors);
}
const message = e instanceof Error ? e.message : String(e);
return Result.err([
{
field: '_root',
message
}
]);
}
}
/** @internal */
static deserializeWithContext(value: any, ctx: DeserializeContext): User | PendingRef {
if (value?.__ref !== undefined) {
return ctx.getOrDefer(value.__ref);
}
if (typeof value !== 'object' || value === null || Array.isArray(value)) {
throw new DeserializeError([
{
field: '_root',
message: 'User.deserializeWithContext: expected an object'
}
]);
}
const obj = value as Record<string, unknown>;
const errors: Array<{
field: string;
message: string;
}> = [];
const knownKeys = new Set(['__type', '__id', '__ref', 'id', 'email', 'name', 'age']);
for (const key of Object.keys(obj)) {
if (!knownKeys.has(key)) {
errors.push({
field: key,
message: 'unknown field'
});
}
}
if (!('id' in obj)) {
errors.push({
field: 'id',
message: 'missing required field'
});
}
if (!('email' in obj)) {
errors.push({
field: 'email',
message: 'missing required field'
});
}
if (errors.length > 0) {
throw new DeserializeError(errors);
}
const instance = Object.create(User.prototype) as User;
if (obj.__id !== undefined) {
ctx.register(obj.__id as number, instance);
}
ctx.trackForFreeze(instance);
{
const __raw_id = obj['id'] as number;
instance.id = __raw_id;
}
{
const __raw_email = obj['email'] as string;
instance.email = __raw_email;
}
if ('name' in obj && obj['name'] !== undefined) {
const __raw_name = obj['name'] as string;
instance.name = __raw_name;
} else {
instance.name = "guest";
}
if ('age' in obj && obj['age'] !== undefined) {
const __raw_age = obj['age'] as number;
instance.age = __raw_age;
}
if (errors.length > 0) {
throw new DeserializeError(errors);
}
return instance;
}
static validateField<K extends keyof User>(
field: K,
value: User[K]
): Array<{
field: string;
message: string;
}> {
return [];
}
static validateFields(partial: Partial<User>): Array<{
field: string;
message: string;
}> {
return [];
}
static hasShape(obj: unknown): boolean {
if (typeof obj !== 'object' || obj === null || Array.isArray(obj)) {
return false;
}
const o = obj as Record<string, unknown>;
return 'id' in o && 'email' in o;
}
static is(obj: unknown): obj is User {
if (obj instanceof User) {
return true;
}
if (!User.hasShape(obj)) {
return false;
}
const result = User.deserialize(obj);
return Result.isOk(result);
}
}
// Usage:
const result = User.deserialize('{"id":1,"email":"[email protected]"}');
if (Result.isOk(result)) {
const user = result.value;
} else {
console.error(result.error); // [{ field: "email", message: "must be a valid email" }]
}Generated output:
import { DeserializeContext } from 'macroforge/serde';
import { DeserializeError } from 'macroforge/serde';
import type { DeserializeOptions } from 'macroforge/serde';
import { PendingRef } from 'macroforge/serde';
/** @serde({ denyUnknownFields: true }) */
class User {
id: number;
email: string;
name: string;
age?: number;
constructor(props: {
id: number;
email: string;
name?: string;
age?: number;
}) {
this.id = props.id;
this.email = props.email;
this.name = props.name as string;
this.age = props.age as number;
}
/**
* Deserializes input to an instance of this class.
* Automatically detects whether input is a JSON string or object.
* @param input - JSON string or object to deserialize
* @param opts - Optional deserialization options
* @returns Result containing the deserialized instance or validation errors
*/
static deserialize(
input: unknown,
opts?: DeserializeOptions
): Result<
User,
Array<{
field: string;
message: string;
}>
> {
try {
// Auto-detect: if string, parse as JSON first
const data = typeof input === 'string' ? JSON.parse(input) : input;
const ctx = DeserializeContext.create();
const resultOrRef = User.deserializeWithContext(data, ctx);
if (PendingRef.is(resultOrRef)) {
return Result.err([
{
field: '_root',
message: 'User.deserialize: root cannot be a forward reference'
}
]);
}
ctx.applyPatches();
if (opts?.freeze) {
ctx.freezeAll();
}
return Result.ok(resultOrRef);
} catch (e) {
if (e instanceof DeserializeError) {
return Result.err(e.errors);
}
const message = e instanceof Error ? e.message : String(e);
return Result.err([
{
field: '_root',
message
}
]);
}
}
/** @internal */
static deserializeWithContext(value: any, ctx: DeserializeContext): User | PendingRef {
if (value?.__ref !== undefined) {
return ctx.getOrDefer(value.__ref);
}
if (typeof value !== 'object' || value === null || Array.isArray(value)) {
throw new DeserializeError([
{
field: '_root',
message: 'User.deserializeWithContext: expected an object'
}
]);
}
const obj = value as Record<string, unknown>;
const errors: Array<{
field: string;
message: string;
}> = [];
const knownKeys = new Set(['__type', '__id', '__ref', 'id', 'email', 'name', 'age']);
for (const key of Object.keys(obj)) {
if (!knownKeys.has(key)) {
errors.push({
field: key,
message: 'unknown field'
});
}
}
if (!('id' in obj)) {
errors.push({
field: 'id',
message: 'missing required field'
});
}
if (!('email' in obj)) {
errors.push({
field: 'email',
message: 'missing required field'
});
}
if (errors.length > 0) {
throw new DeserializeError(errors);
}
const instance = Object.create(User.prototype) as User;
if (obj.__id !== undefined) {
ctx.register(obj.__id as number, instance);
}
ctx.trackForFreeze(instance);
{
const __raw_id = obj['id'] as number;
instance.id = __raw_id;
}
{
const __raw_email = obj['email'] as string;
instance.email = __raw_email;
}
if ('name' in obj && obj['name'] !== undefined) {
const __raw_name = obj['name'] as string;
instance.name = __raw_name;
} else {
instance.name = 'guest';
}
if ('age' in obj && obj['age'] !== undefined) {
const __raw_age = obj['age'] as number;
instance.age = __raw_age;
}
if (errors.length > 0) {
throw new DeserializeError(errors);
}
return instance;
}
static validateField<K extends keyof User>(
field: K,
value: User[K]
): Array<{
field: string;
message: string;
}> {
return [];
}
static validateFields(partial: Partial<User>): Array<{
field: string;
message: string;
}> {
return [];
}
static hasShape(obj: unknown): boolean {
if (typeof obj !== 'object' || obj === null || Array.isArray(obj)) {
return false;
}
const o = obj as Record<string, unknown>;
return 'id' in o && 'email' in o;
}
static is(obj: unknown): obj is User {
if (obj instanceof User) {
return true;
}
if (!User.hasShape(obj)) {
return false;
}
const result = User.deserialize(obj);
return Result.isOk(result);
}
}
// Usage:
const result = User.deserialize('{"id":1,"email":"[email protected]"}');
if (Result.isOk(result)) {
const user = result.value;
} else {
console.error(result.error); // [{ field: "email", message: "must be a valid email" }]
}Generated output:
import { DeserializeContext } from 'macroforge/serde';
import { DeserializeError } from 'macroforge/serde';
import type { DeserializeOptions } from 'macroforge/serde';
import { PendingRef } from 'macroforge/serde';
/** @serde({ denyUnknownFields: true }) */
class User {
id: number;
email: string;
name: string;
age?: number;
constructor(props: {
id: number;
email: string;
name?: string;
age?: number;
}) {
this.id = props.id;
this.email = props.email;
this.name = props.name as string;
this.age = props.age as number;
}
/**
* Deserializes input to an instance of this class.
* Automatically detects whether input is a JSON string or object.
* @param input - JSON string or object to deserialize
* @param opts - Optional deserialization options
* @returns Result containing the deserialized instance or validation errors
*/
static deserialize(
input: unknown,
opts?: DeserializeOptions
): Result<
User,
Array<{
field: string;
message: string;
}>
> {
try {
// Auto-detect: if string, parse as JSON first
const data = typeof input === 'string' ? JSON.parse(input) : input;
const ctx = DeserializeContext.create();
const resultOrRef = User.deserializeWithContext(data, ctx);
if (PendingRef.is(resultOrRef)) {
return Result.err([
{
field: '_root',
message: 'User.deserialize: root cannot be a forward reference'
}
]);
}
ctx.applyPatches();
if (opts?.freeze) {
ctx.freezeAll();
}
return Result.ok(resultOrRef);
} catch (e) {
if (e instanceof DeserializeError) {
return Result.err(e.errors);
}
const message = e instanceof Error ? e.message : String(e);
return Result.err([
{
field: '_root',
message
}
]);
}
}
/** @internal */
static deserializeWithContext(value: any, ctx: DeserializeContext): User | PendingRef {
if (value?.__ref !== undefined) {
return ctx.getOrDefer(value.__ref);
}
if (typeof value !== 'object' || value === null || Array.isArray(value)) {
throw new DeserializeError([
{
field: '_root',
message: 'User.deserializeWithContext: expected an object'
}
]);
}
const obj = value as Record<string, unknown>;
const errors: Array<{
field: string;
message: string;
}> = [];
const knownKeys = new Set(['__type', '__id', '__ref', 'id', 'email', 'name', 'age']);
for (const key of Object.keys(obj)) {
if (!knownKeys.has(key)) {
errors.push({
field: key,
message: 'unknown field'
});
}
}
if (!('id' in obj)) {
errors.push({
field: 'id',
message: 'missing required field'
});
}
if (!('email' in obj)) {
errors.push({
field: 'email',
message: 'missing required field'
});
}
if (errors.length > 0) {
throw new DeserializeError(errors);
}
const instance = Object.create(User.prototype) as User;
if (obj.__id !== undefined) {
ctx.register(obj.__id as number, instance);
}
ctx.trackForFreeze(instance);
{
const __raw_id = obj['id'] as number;
instance.id = __raw_id;
}
{
const __raw_email = obj['email'] as string;
instance.email = __raw_email;
}
if ('name' in obj && obj['name'] !== undefined) {
const __raw_name = obj['name'] as string;
instance.name = __raw_name;
} else {
instance.name = "guest";
}
if ('age' in obj && obj['age'] !== undefined) {
const __raw_age = obj['age'] as number;
instance.age = __raw_age;
}
if (errors.length > 0) {
throw new DeserializeError(errors);
}
return instance;
}
static validateField<K extends keyof User>(
field: K,
value: User[K]
): Array<{
field: string;
message: string;
}> {
return [];
}
static validateFields(partial: Partial<User>): Array<{
field: string;
message: string;
}> {
return [];
}
static hasShape(obj: unknown): boolean {
if (typeof obj !== 'object' || obj === null || Array.isArray(obj)) {
return false;
}
const o = obj as Record<string, unknown>;
return 'id' in o && 'email' in o;
}
static is(obj: unknown): obj is User {
if (obj instanceof User) {
return true;
}
if (!User.hasShape(obj)) {
return false;
}
const result = User.deserialize(obj);
return Result.isOk(result);
}
}
// Usage:
const result = User.deserialize('{"id":1,"email":"[email protected]"}');
if (Result.isOk(result)) {
const user = result.value;
} else {
console.error(result.error); // [{ field: "email", message: "must be a valid email" }]
}Generated output:
import { DeserializeContext } from 'macroforge/serde';
import { DeserializeError } from 'macroforge/serde';
import type { DeserializeOptions } from 'macroforge/serde';
import { PendingRef } from 'macroforge/serde';
/** @serde({ denyUnknownFields: true }) */
class User {
id: number;
email: string;
name: string;
age?: number;
constructor(props: {
id: number;
email: string;
name?: string;
age?: number;
}) {
this.id = props.id;
this.email = props.email;
this.name = props.name as string;
this.age = props.age as number;
}
/**
* Deserializes input to an instance of this class.
* Automatically detects whether input is a JSON string or object.
* @param input - JSON string or object to deserialize
* @param opts - Optional deserialization options
* @returns Result containing the deserialized instance or validation errors
*/
static deserialize(
input: unknown,
opts?: DeserializeOptions
): Result<
User,
Array<{
field: string;
message: string;
}>
> {
try {
// Auto-detect: if string, parse as JSON first
const data = typeof input === 'string' ? JSON.parse(input) : input;
const ctx = DeserializeContext.create();
const resultOrRef = User.deserializeWithContext(data, ctx);
if (PendingRef.is(resultOrRef)) {
return Result.err([
{
field: '_root',
message: 'User.deserialize: root cannot be a forward reference'
}
]);
}
ctx.applyPatches();
if (opts?.freeze) {
ctx.freezeAll();
}
return Result.ok(resultOrRef);
} catch (e) {
if (e instanceof DeserializeError) {
return Result.err(e.errors);
}
const message = e instanceof Error ? e.message : String(e);
return Result.err([
{
field: '_root',
message
}
]);
}
}
/** @internal */
static deserializeWithContext(value: any, ctx: DeserializeContext): User | PendingRef {
if (value?.__ref !== undefined) {
return ctx.getOrDefer(value.__ref);
}
if (typeof value !== 'object' || value === null || Array.isArray(value)) {
throw new DeserializeError([
{
field: '_root',
message: 'User.deserializeWithContext: expected an object'
}
]);
}
const obj = value as Record<string, unknown>;
const errors: Array<{
field: string;
message: string;
}> = [];
const knownKeys = new Set(['__type', '__id', '__ref', 'id', 'email', 'name', 'age']);
for (const key of Object.keys(obj)) {
if (!knownKeys.has(key)) {
errors.push({
field: key,
message: 'unknown field'
});
}
}
if (!('id' in obj)) {
errors.push({
field: 'id',
message: 'missing required field'
});
}
if (!('email' in obj)) {
errors.push({
field: 'email',
message: 'missing required field'
});
}
if (errors.length > 0) {
throw new DeserializeError(errors);
}
const instance = Object.create(User.prototype) as User;
if (obj.__id !== undefined) {
ctx.register(obj.__id as number, instance);
}
ctx.trackForFreeze(instance);
{
const __raw_id = obj['id'] as number;
instance.id = __raw_id;
}
{
const __raw_email = obj['email'] as string;
instance.email = __raw_email;
}
if ('name' in obj && obj['name'] !== undefined) {
const __raw_name = obj['name'] as string;
instance.name = __raw_name;
} else {
instance.name = 'guest';
}
if ('age' in obj && obj['age'] !== undefined) {
const __raw_age = obj['age'] as number;
instance.age = __raw_age;
}
if (errors.length > 0) {
throw new DeserializeError(errors);
}
return instance;
}
static validateField<K extends keyof User>(
field: K,
value: User[K]
): Array<{
field: string;
message: string;
}> {
return [];
}
static validateFields(partial: Partial<User>): Array<{
field: string;
message: string;
}> {
return [];
}
static hasShape(obj: unknown): boolean {
if (typeof obj !== 'object' || obj === null || Array.isArray(obj)) {
return false;
}
const o = obj as Record<string, unknown>;
return 'id' in o && 'email' in o;
}
static is(obj: unknown): obj is User {
if (obj instanceof User) {
return true;
}
if (!User.hasShape(obj)) {
return false;
}
const result = User.deserialize(obj);
return Result.isOk(result);
}
}
// Usage:
const result = User.deserialize('{"id":1,"email":"[email protected]"}');
if (Result.isOk(result)) {
const user = result.value;
} else {
console.error(result.error); // [{ field: "email", message: "must be a valid email" }]
}Generated output:
import { DeserializeContext } from 'macroforge/serde';
import { DeserializeError } from 'macroforge/serde';
import type { DeserializeOptions } from 'macroforge/serde';
import { PendingRef } from 'macroforge/serde';
/** @serde({ denyUnknownFields: true }) */
class User {
id: number;
email: string;
name: string;
age?: number;
constructor(props: {
id: number;
email: string;
name?: string;
age?: number;
}) {
this.id = props.id;
this.email = props.email;
this.name = props.name as string;
this.age = props.age as number;
}
/**
* Deserializes input to an instance of this class.
* Automatically detects whether input is a JSON string or object.
* @param input - JSON string or object to deserialize
* @param opts - Optional deserialization options
* @returns Result containing the deserialized instance or validation errors
*/
static deserialize(
input: unknown,
opts?: DeserializeOptions
): Result<
User,
Array<{
field: string;
message: string;
}>
> {
try {
// Auto-detect: if string, parse as JSON first
const data = typeof input === 'string' ? JSON.parse(input) : input;
const ctx = DeserializeContext.create();
const resultOrRef = User.deserializeWithContext(data, ctx);
if (PendingRef.is(resultOrRef)) {
return Result.err([
{
field: '_root',
message: 'User.deserialize: root cannot be a forward reference'
}
]);
}
ctx.applyPatches();
if (opts?.freeze) {
ctx.freezeAll();
}
return Result.ok(resultOrRef);
} catch (e) {
if (e instanceof DeserializeError) {
return Result.err(e.errors);
}
const message = e instanceof Error ? e.message : String(e);
return Result.err([
{
field: '_root',
message
}
]);
}
}
/** @internal */
static deserializeWithContext(value: any, ctx: DeserializeContext): User | PendingRef {
if (value?.__ref !== undefined) {
return ctx.getOrDefer(value.__ref);
}
if (typeof value !== 'object' || value === null || Array.isArray(value)) {
throw new DeserializeError([
{
field: '_root',
message: 'User.deserializeWithContext: expected an object'
}
]);
}
const obj = value as Record<string, unknown>;
const errors: Array<{
field: string;
message: string;
}> = [];
const knownKeys = new Set(['__type', '__id', '__ref', 'id', 'email', 'name', 'age']);
for (const key of Object.keys(obj)) {
if (!knownKeys.has(key)) {
errors.push({
field: key,
message: 'unknown field'
});
}
}
if (!('id' in obj)) {
errors.push({
field: 'id',
message: 'missing required field'
});
}
if (!('email' in obj)) {
errors.push({
field: 'email',
message: 'missing required field'
});
}
if (errors.length > 0) {
throw new DeserializeError(errors);
}
const instance = Object.create(User.prototype) as User;
if (obj.__id !== undefined) {
ctx.register(obj.__id as number, instance);
}
ctx.trackForFreeze(instance);
{
const __raw_id = obj['id'] as number;
instance.id = __raw_id;
}
{
const __raw_email = obj['email'] as string;
instance.email = __raw_email;
}
if ('name' in obj && obj['name'] !== undefined) {
const __raw_name = obj['name'] as string;
instance.name = __raw_name;
} else {
instance.name = "guest";
}
if ('age' in obj && obj['age'] !== undefined) {
const __raw_age = obj['age'] as number;
instance.age = __raw_age;
}
if (errors.length > 0) {
throw new DeserializeError(errors);
}
return instance;
}
static validateField<K extends keyof User>(
field: K,
value: User[K]
): Array<{
field: string;
message: string;
}> {
return [];
}
static validateFields(partial: Partial<User>): Array<{
field: string;
message: string;
}> {
return [];
}
static hasShape(obj: unknown): boolean {
if (typeof obj !== 'object' || obj === null || Array.isArray(obj)) {
return false;
}
const o = obj as Record<string, unknown>;
return 'id' in o && 'email' in o;
}
static is(obj: unknown): obj is User {
if (obj instanceof User) {
return true;
}
if (!User.hasShape(obj)) {
return false;
}
const result = User.deserialize(obj);
return Result.isOk(result);
}
}
// Usage:
const result = User.deserialize('{"id":1,"email":"[email protected]"}');
if (Result.isOk(result)) {
const user = result.value;
} else {
console.error(result.error); // [{ field: "email", message: "must be a valid email" }]
}Generated output:
import { DeserializeContext } from 'macroforge/serde';
import { DeserializeError } from 'macroforge/serde';
import type { DeserializeOptions } from 'macroforge/serde';
import { PendingRef } from 'macroforge/serde';
/** @serde({ denyUnknownFields: true }) */
class User {
id: number;
email: string;
name: string;
age?: number;
constructor(props: {
id: number;
email: string;
name?: string;
age?: number;
}) {
this.id = props.id;
this.email = props.email;
this.name = props.name as string;
this.age = props.age as number;
}
/**
* Deserializes input to an instance of this class.
* Automatically detects whether input is a JSON string or object.
* @param input - JSON string or object to deserialize
* @param opts - Optional deserialization options
* @returns Result containing the deserialized instance or validation errors
*/
static deserialize(
input: unknown,
opts?: DeserializeOptions
): Result<
User,
Array<{
field: string;
message: string;
}>
> {
try {
// Auto-detect: if string, parse as JSON first
const data = typeof input === 'string' ? JSON.parse(input) : input;
const ctx = DeserializeContext.create();
const resultOrRef = User.deserializeWithContext(data, ctx);
if (PendingRef.is(resultOrRef)) {
return Result.err([
{
field: '_root',
message: 'User.deserialize: root cannot be a forward reference'
}
]);
}
ctx.applyPatches();
if (opts?.freeze) {
ctx.freezeAll();
}
return Result.ok(resultOrRef);
} catch (e) {
if (e instanceof DeserializeError) {
return Result.err(e.errors);
}
const message = e instanceof Error ? e.message : String(e);
return Result.err([
{
field: '_root',
message
}
]);
}
}
/** @internal */
static deserializeWithContext(value: any, ctx: DeserializeContext): User | PendingRef {
if (value?.__ref !== undefined) {
return ctx.getOrDefer(value.__ref);
}
if (typeof value !== 'object' || value === null || Array.isArray(value)) {
throw new DeserializeError([
{
field: '_root',
message: 'User.deserializeWithContext: expected an object'
}
]);
}
const obj = value as Record<string, unknown>;
const errors: Array<{
field: string;
message: string;
}> = [];
const knownKeys = new Set(['__type', '__id', '__ref', 'id', 'email', 'name', 'age']);
for (const key of Object.keys(obj)) {
if (!knownKeys.has(key)) {
errors.push({
field: key,
message: 'unknown field'
});
}
}
if (!('id' in obj)) {
errors.push({
field: 'id',
message: 'missing required field'
});
}
if (!('email' in obj)) {
errors.push({
field: 'email',
message: 'missing required field'
});
}
if (errors.length > 0) {
throw new DeserializeError(errors);
}
const instance = Object.create(User.prototype) as User;
if (obj.__id !== undefined) {
ctx.register(obj.__id as number, instance);
}
ctx.trackForFreeze(instance);
{
const __raw_id = obj['id'] as number;
instance.id = __raw_id;
}
{
const __raw_email = obj['email'] as string;
instance.email = __raw_email;
}
if ('name' in obj && obj['name'] !== undefined) {
const __raw_name = obj['name'] as string;
instance.name = __raw_name;
} else {
instance.name = 'guest';
}
if ('age' in obj && obj['age'] !== undefined) {
const __raw_age = obj['age'] as number;
instance.age = __raw_age;
}
if (errors.length > 0) {
throw new DeserializeError(errors);
}
return instance;
}
static validateField<K extends keyof User>(
field: K,
value: User[K]
): Array<{
field: string;
message: string;
}> {
return [];
}
static validateFields(partial: Partial<User>): Array<{
field: string;
message: string;
}> {
return [];
}
static hasShape(obj: unknown): boolean {
if (typeof obj !== 'object' || obj === null || Array.isArray(obj)) {
return false;
}
const o = obj as Record<string, unknown>;
return 'id' in o && 'email' in o;
}
static is(obj: unknown): obj is User {
if (obj instanceof User) {
return true;
}
if (!User.hasShape(obj)) {
return false;
}
const result = User.deserialize(obj);
return Result.isOk(result);
}
}
// Usage:
const result = User.deserialize('{"id":1,"email":"[email protected]"}');
if (Result.isOk(result)) {
const user = result.value;
} else {
console.error(result.error); // [{ field: "email", message: "must be a valid email" }]
}Generated output:
import { DeserializeContext } from 'macroforge/serde';
import { DeserializeError } from 'macroforge/serde';
import type { DeserializeOptions } from 'macroforge/serde';
import { PendingRef } from 'macroforge/serde';
/** @serde({ denyUnknownFields: true }) */
class User {
id: number;
email: string;
name: string;
age?: number;
constructor(props: {
id: number;
email: string;
name?: string;
age?: number;
}) {
this.id = props.id;
this.email = props.email;
this.name = props.name as string;
this.age = props.age as number;
}
/**
* Deserializes input to an instance of this class.
* Automatically detects whether input is a JSON string or object.
* @param input - JSON string or object to deserialize
* @param opts - Optional deserialization options
* @returns Result containing the deserialized instance or validation errors
*/
static deserialize(
input: unknown,
opts?: DeserializeOptions
): Result<
User,
Array<{
field: string;
message: string;
}>
> {
try {
// Auto-detect: if string, parse as JSON first
const data = typeof input === 'string' ? JSON.parse(input) : input;
const ctx = DeserializeContext.create();
const resultOrRef = User.deserializeWithContext(data, ctx);
if (PendingRef.is(resultOrRef)) {
return Result.err([
{
field: '_root',
message: 'User.deserialize: root cannot be a forward reference'
}
]);
}
ctx.applyPatches();
if (opts?.freeze) {
ctx.freezeAll();
}
return Result.ok(resultOrRef);
} catch (e) {
if (e instanceof DeserializeError) {
return Result.err(e.errors);
}
const message = e instanceof Error ? e.message : String(e);
return Result.err([
{
field: '_root',
message
}
]);
}
}
/** @internal */
static deserializeWithContext(value: any, ctx: DeserializeContext): User | PendingRef {
if (value?.__ref !== undefined) {
return ctx.getOrDefer(value.__ref);
}
if (typeof value !== 'object' || value === null || Array.isArray(value)) {
throw new DeserializeError([
{
field: '_root',
message: 'User.deserializeWithContext: expected an object'
}
]);
}
const obj = value as Record<string, unknown>;
const errors: Array<{
field: string;
message: string;
}> = [];
const knownKeys = new Set(['__type', '__id', '__ref', 'id', 'email', 'name', 'age']);
for (const key of Object.keys(obj)) {
if (!knownKeys.has(key)) {
errors.push({
field: key,
message: 'unknown field'
});
}
}
if (!('id' in obj)) {
errors.push({
field: 'id',
message: 'missing required field'
});
}
if (!('email' in obj)) {
errors.push({
field: 'email',
message: 'missing required field'
});
}
if (errors.length > 0) {
throw new DeserializeError(errors);
}
const instance = Object.create(User.prototype) as User;
if (obj.__id !== undefined) {
ctx.register(obj.__id as number, instance);
}
ctx.trackForFreeze(instance);
{
const __raw_id = obj['id'] as number;
instance.id = __raw_id;
}
{
const __raw_email = obj['email'] as string;
instance.email = __raw_email;
}
if ('name' in obj && obj['name'] !== undefined) {
const __raw_name = obj['name'] as string;
instance.name = __raw_name;
} else {
instance.name = 'guest';
}
if ('age' in obj && obj['age'] !== undefined) {
const __raw_age = obj['age'] as number;
instance.age = __raw_age;
}
if (errors.length > 0) {
throw new DeserializeError(errors);
}
return instance;
}
static validateField<K extends keyof User>(
field: K,
value: User[K]
): Array<{
field: string;
message: string;
}> {
return [];
}
static validateFields(partial: Partial<User>): Array<{
field: string;
message: string;
}> {
return [];
}
static hasShape(obj: unknown): boolean {
if (typeof obj !== 'object' || obj === null || Array.isArray(obj)) {
return false;
}
const o = obj as Record<string, unknown>;
return 'id' in o && 'email' in o;
}
static is(obj: unknown): obj is User {
if (obj instanceof User) {
return true;
}
if (!User.hasShape(obj)) {
return false;
}
const result = User.deserialize(obj);
return Result.isOk(result);
}
}
// Usage:
const result = User.deserialize('{"id":1,"email":"[email protected]"}');
if (Result.isOk(result)) {
const user = result.value;
} else {
console.error(result.error); // [{ field: "email", message: "must be a valid email" }]
}Generated output:
import { DeserializeContext } from 'macroforge/serde';
import { DeserializeError } from 'macroforge/serde';
import type { DeserializeOptions } from 'macroforge/serde';
import { PendingRef } from 'macroforge/serde';
/** @serde({ denyUnknownFields: true }) */
class User {
id: number;
email: string;
name: string;
age?: number;
constructor(props: {
id: number;
email: string;
name?: string;
age?: number;
}) {
this.id = props.id;
this.email = props.email;
this.name = props.name as string;
this.age = props.age as number;
}
/**
* Deserializes input to an instance of this class.
* Automatically detects whether input is a JSON string or object.
* @param input - JSON string or object to deserialize
* @param opts - Optional deserialization options
* @returns Result containing the deserialized instance or validation errors
*/
static deserialize(
input: unknown,
opts?: DeserializeOptions
): Result<
User,
Array<{
field: string;
message: string;
}>
> {
try {
// Auto-detect: if string, parse as JSON first
const data = typeof input === 'string' ? JSON.parse(input) : input;
const ctx = DeserializeContext.create();
const resultOrRef = User.deserializeWithContext(data, ctx);
if (PendingRef.is(resultOrRef)) {
return Result.err([
{
field: '_root',
message: 'User.deserialize: root cannot be a forward reference'
}
]);
}
ctx.applyPatches();
if (opts?.freeze) {
ctx.freezeAll();
}
return Result.ok(resultOrRef);
} catch (e) {
if (e instanceof DeserializeError) {
return Result.err(e.errors);
}
const message = e instanceof Error ? e.message : String(e);
return Result.err([
{
field: '_root',
message
}
]);
}
}
/** @internal */
static deserializeWithContext(value: any, ctx: DeserializeContext): User | PendingRef {
if (value?.__ref !== undefined) {
return ctx.getOrDefer(value.__ref);
}
if (typeof value !== 'object' || value === null || Array.isArray(value)) {
throw new DeserializeError([
{
field: '_root',
message: 'User.deserializeWithContext: expected an object'
}
]);
}
const obj = value as Record<string, unknown>;
const errors: Array<{
field: string;
message: string;
}> = [];
const knownKeys = new Set(['__type', '__id', '__ref', 'id', 'email', 'name', 'age']);
for (const key of Object.keys(obj)) {
if (!knownKeys.has(key)) {
errors.push({
field: key,
message: 'unknown field'
});
}
}
if (!('id' in obj)) {
errors.push({
field: 'id',
message: 'missing required field'
});
}
if (!('email' in obj)) {
errors.push({
field: 'email',
message: 'missing required field'
});
}
if (errors.length > 0) {
throw new DeserializeError(errors);
}
const instance = Object.create(User.prototype) as User;
if (obj.__id !== undefined) {
ctx.register(obj.__id as number, instance);
}
ctx.trackForFreeze(instance);
{
const __raw_id = obj['id'] as number;
instance.id = __raw_id;
}
{
const __raw_email = obj['email'] as string;
instance.email = __raw_email;
}
if ('name' in obj && obj['name'] !== undefined) {
const __raw_name = obj['name'] as string;
instance.name = __raw_name;
} else {
instance.name = 'guest';
}
if ('age' in obj && obj['age'] !== undefined) {
const __raw_age = obj['age'] as number;
instance.age = __raw_age;
}
if (errors.length > 0) {
throw new DeserializeError(errors);
}
return instance;
}
static validateField<K extends keyof User>(
field: K,
value: User[K]
): Array<{
field: string;
message: string;
}> {
return [];
}
static validateFields(partial: Partial<User>): Array<{
field: string;
message: string;
}> {
return [];
}
static hasShape(obj: unknown): boolean {
if (typeof obj !== 'object' || obj === null || Array.isArray(obj)) {
return false;
}
const o = obj as Record<string, unknown>;
return 'id' in o && 'email' in o;
}
static is(obj: unknown): obj is User {
if (obj instanceof User) {
return true;
}
if (!User.hasShape(obj)) {
return false;
}
const result = User.deserialize(obj);
return Result.isOk(result);
}
}
// Usage:
const result = User.deserialize('{"id":1,"email":"[email protected]"}');
if (Result.isOk(result)) {
const user = result.value;
} else {
console.error(result.error); // [{ field: "email", message: "must be a valid email" }]
}Required Imports
The generated code automatically imports:
Resultfrommacroforge/utilsDeserializeContext,DeserializeError,PendingReffrommacroforge/serde