Skip to content

Formatters ​

The decorators @Query(), @Param(), @Body(), @ObjectBody(), and @Header() provide the formatters option to process values before they are serialized.

Processing steps differ slightly depending on the decorator:

  • For @Query(), @Param(), and @Header():
    Collect value → apply formatter (sequential chain) → apply array options (comma/bit) → URL encoding
  • For @Body() and @ObjectBody():
    Collect value → apply formatter (sequential chain)

Rules

  • Returning undefined or null will cause the key to be omitted.
  • For array fields, formatters are applied individually to each element.
  • formatters can be a single object or an array of objects applied sequentially.
  • Return values can be primitives (string, number, boolean) or arrays.
  • Example: transformations like string → Date → epoch time can be chained together.

Formatter Options ​

FieldTypeDescription
order('string' | 'number' | 'dateTime')[]Specifies the order in which formatters are applied. Default is ['number', 'string', 'dateTime'].
ignoreErrorbooleanDetermines whether to discard the value on error. If true, errors are ignored and the value is excluded; if false, an exception is thrown.
number(value: number) => number | Date | stringFormatter for numeric values. May return number, Date, or string.
string(value: string) => string | DateFormatter for string values. May return string or Date.
dateTime(value: Date) => stringFormatter for Date values. Must return a string.

Notes ​

Formatters operate only if the input type matches (string, number, or Date). If the input type does not match, the transformation may be skipped or the value may be omitted.

  • If a transformation does not work as expected, first check that the input type is correct.
  • If necessary, use the ignoreError option to suppress errors and safely exclude invalid values.

Single Value Formatting ​

ts
@Get({ host: 'https://api.example.com', path: '/search' })
export class FormatterSingleFrame extends JinFrame {
  @Query({
    formatters: { string: (v: string) => v.trim().toLowerCase() }, // string → lowercase
  })
  declare readonly q?: string;

  @Query({
    formatters: { number: (v: number) => Number(v.toFixed(2)) }, // number → round to 2 decimals
  })
  declare readonly price?: number;

  @Query({
    formatters: {
      dateTime: (d: Date) =>
        `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}-${String(d.getDate()).padStart(2, '0')}`,
    },
    // Date → yyyy-MM-dd
  })
  declare readonly date?: Date;
}

const reply = await FormatterSingleFrame.of({
  q: '  Pikachu ',
  price: 12.345,
  date: new Date('2025-08-21'),
}).execute();
// → ?q=pikachu&price=12.35&date=2025-08-21

Array Element Formatting (per element) ​

ts
@Get({ host: 'https://api.example.com', path: '/filter' })
export class FormatterArrayFrame extends JinFrame {
  @Query({
    formatters: { string: (tag: string) => tag.trim().toLowerCase() }, // applied to each array element
  })
  declare readonly tags?: string[];
}

const reply = await FormatterArrayFrame.of({ tags: ['  RED ', '  Blue'] }).execute();
// → ?tags=red&tags=blue

Multiple Formatters (chained) ​

Pass an array of formatters ​

ts
@Get({ host: 'https://api.example.com', path: '/convert' })
export class FormatterChainFrame extends JinFrame {
  @Query({
    formatters: [
      { string: (s: string) => s.trim() }, // string → trimmed string
      { string: (s: string) => new Date(s) }, // string → Date
      { dateTime: (d: Date) => `${Math.floor(d.getTime())}` }, // Date → epoch(ms)
    ],
  })
  declare readonly dates?: string[];
}

const reply = await FormatterChainFrame.of({ dates: ['2025-08-01', '2025-08-02'] }).execute();
// → ?dates=1754006400000&dates=1754092800000

Use the order option ​

ts
@Get({ host: 'https://api.example.com', path: '/convert' })
export class FormatterChainFrame extends JinFrame {
  @Query({
    formatters: [
      {
        order: ['number', 'string', 'dateTime'], // number → string → dateTime
        string: (s: string) => new Date(s.trim()), // string → Date
        dateTime: (d: Date) => `${Math.floor(d.getTime())}`, // Date → epoch(ms)
      },
    ],
  })
  declare readonly dates?: string[];
}

Arrays + Comma Serialization with Formatters ​

ts
@Get({ host: 'https://api.example.com', path: '/filter' })
export class FormatterCommaFrame extends JinFrame {
  @Query({
    comma: true, // serialize array with commas
    formatters: { string: (tag: string) => `c-${tag.trim()}` }, // add prefix to each element
  })
  declare readonly tags?: string[];
}

const reply = await FormatterCommaFrame.of({ tags: ['red', ' blue ', 'green,'] }).execute();
// → ?tags=c-red,c-blue,c-green

Numeric Arrays + Bitwise OR with Normalization ​

ts
@Get({ host: 'https://api.example.com', path: '/flags' })
export class FormatterBitFrame extends JinFrame {
  @Query({
    bit: { enable: true }, // enable bitwise OR
    formatters: { number: (f?: number) => (Number.isFinite(f) && f! >= 0 ? Number(f) : undefined) },
    // exclude negative or NaN values
  })
  declare readonly flags?: number[];
}

const reply = await FormatterBitFrame.of({ flags: [1, 2, 4] }).execute();
// → ?flags=7

Enum / Mapping Conversion ​

ts
const TAG_MAP: Record<string, string> = { red: 'R', blue: 'B', green: 'G' };

@Get({ host: 'https://api.example.com', path: '/filter' })
export class FormatterMapFrame extends JinFrame {
  @Query({
    formatters: { string: (tag?: string) => (tag ? TAG_MAP[tag] : undefined) }, // mapping conversion
  })
  declare readonly tags?: string[];
}

const reply = await FormatterMapFrame.of({ tags: ['red', 'blue', 'green'] }).execute();
// → ?tags=R&tags=B&tags=G

Formatter in @Body and @ObjectBody ​

@Body() and @ObjectBody() work with objects, so you must explicitly specify which field the formatter should apply to.
Use the findFrom option to designate the path.

ts
@Post({ host: 'http://some.api.google.com/jinframe/:passing' })
class ObjectFormatterExample extends JinFrame {
  @Param()
  declare public readonly passing: string;

  @ObjectBody({
    formatters: [
      {
        findFrom: 'name', // apply formatter to the "name" field
        string(value) {
          return `${value}::111`;
        },
      },
      {
        findFrom: 'date', // apply formatter to the "date" field
        dateTime(value) {
          const f = lightFormat(value, 'yyyy-MM-dd');
          return f;
        },
      },
    ],
  })
  declare public readonly ability: {
    name: string;
    date: Date;
    desc: string;
  };
}

Summary ​

Formatters can be used for various purposes, such as:

  • String normalization
  • Date transformation
  • Array serialization optimization
  • Bitmask handling
  • Enum/mapping conversion

Especially in @Body and @ObjectBody, you can apply fine-grained control at the DTO object level, making it possible to manage even complex API requests declaratively and cleanly.