Serializable Objects

Declare what crosses the wire, field by field, typed in both directions.

Serialization is how raw JSON becomes a typed class instance on the way in, and how your objects become JSON on the way out. It's opt-in per field: only @SerializableField properties cross the wire, so internal state can never leak by accident.

Declare a shape

import { SerializableField } from '@system-inc/base-foundation/serialization/decorators/SerializableField';
import { SerializableObject } from '@system-inc/base-foundation/serialization/decorators/SerializableObject';

@SerializableObject()
export class OrderInput {
    @SerializableField(() => String)
    productId: string;

    @SerializableField(() => Number)
    quantity: number;

    @SerializableField(() => String, { optional: true })
    couponCode?: string;

    @SerializableField(() => [String])
    tags: string[];

    @SerializableField(() => ShippingAddress)
    shipping: ShippingAddress;
}
  • The thunk (() => Type) declares the field's type: primitives, [Type] arrays, enums, or other serializable classes (nesting recurses).
  • Fields without the decorator simply don't exist on the wire: in either direction.
  • These classes power @HttpBody, query/cookie object binding, and RPC arguments, and stack cleanly with validation rules.

Field options

@SerializableField(typeFunc, options?) — the full option set:

OptionEffect
namethe JSON key, when it differs from the property name
optionalthe field may be absent (pair with ? on the property)
defaultValueused when the incoming JSON omits the field
transformercustom value conversion — see Transformers
descriptiondocumentation metadata

A Date field shows name and transformer together — stored as a Date, wired as an ISO string under a different key:

import { DateJsonValueTransformer } from '@system-inc/base-common/json/value-transformer/DateJsonValueTransformer';

    @SerializableField(() => Date, {
        name: 'placedAt',
        transformer: DateJsonValueTransformer,
    })
    placedAtDate: Date;

Failure semantics

Deserialization failures are HTTP 400 (SERIALIZATION_ERROR) — malformed JSON, an un-coercible value, an invalid enum member. This is deliberately distinct from validation:

  • Serialization answers "is this the right shape?" → 400.
  • Validation answers "is this acceptable?" → 422, with per-field details.

The order is fixed: deserialize first, validate second, handler third. A malformed payload never reaches your rules; a well-formed but unacceptable one never reaches your code.

Wire interfaces

When a shape is shared with callers (RPC contracts especially) pair the class with a plain interface and implements it:

import { StrictJsonInterface } from '@system-inc/base-common/json/StrictJson';

export type OrderJson = StrictJsonInterface<{
    productId: string;
    quantity: number;
}>;
@SerializableObject()
export class OrderInput implements OrderJson { ... }

StrictJsonInterface is a compile-time guard: it errors at the type definition if any property isn't JSON-representable (a raw Date, a function), and unlike index-signature approaches it admits only the keys you declared. The interface travels to the frontend (Share Types); the class stays on the server.