payload-vars

Package version: v2.4.1

Imagine a DTO with validation and transformation rules, all expressed in plain, human-readable JSON. Templates live in data, so you can store them, inspect their input contracts, and render them into payloads with runtime values.

The package has no runtime dependencies and supports browsers and Node.js 18+, with ES module and CommonJS entry points.

Install

npm install payload-vars

Basic usage

import { PayloadTemplate } from 'payload-vars';

1. JSON with variables

Define the payload structure with variable expressions as ordinary JSON strings.

const rawTemplate = {
  orderId: '{{ orderId : string }}',
  products: '{{products:string[??omit]??throw}}',
};

2. Compilation

The constructor validates the template and compiles a reusable contract:

const template = new PayloadTemplate(rawTemplate);

The template provides these methods:

Method Returns
variables() Variable contracts in first occurrence order, with matching declarations deduplicated.
toJSON() A copy of the normalized template.
render(values, options?) The payload with runtime values filled in; passthrough can retain unused variables.
tokenizePayloadExpression() Highlighting tokens for each normalized placeholder occurrence.

Inspect the normalized template:

const normalized = template.toJSON();
// {
//   orderId: '{{orderId:string}}',
//   products: '{{products:string[ ?? omit ] ?? throw}}',
// }

Inspect the compiled variable contracts:

const variables = template.variables();
// [
//   {
//     name: 'orderId',
//     type: 'string',
//     declaration: '{{orderId:string}}',
//     paths: ['$.orderId'],
//   },
//   {
//     name: 'products',
//     type: 'string[]',
//     memberFallback: { operator: '??', action: 'omit' },
//     valueFallback: { operator: '??', action: 'throw' },
//     declaration: '{{products:string[ ?? omit ] ?? throw}}',
//     paths: ['$.products'],
//   },
// ]

3. Rendering

Render the resulting object with runtime values.

const payload = template.render({
  orderId: 'ORD-123',
  products: ['A', null, 'B'],
});
// { orderId: 'ORD-123', products: ['A', 'B'] }

template.toJSON() returns the normalized template; render() returns the payload with values filled in.

Unused input variables are discarded by default. Set passthrough: true to copy them onto an object-shaped rendered payload:

template.render({
  orderId: 'ORD-123',
  products: ['A'],
  debugId: 'test-123',
}, { passthrough: true });
// { debugId: 'test-123', orderId: 'ORD-123', products: ['A'] }

Template properties win when an unused variable has the same name. Passthrough values must be valid JSON values, and passthrough requires the rendered root to be an object. The option defaults to false.

Allowed values

Declare a closed set directly in the template when a value must come from a fixed domain:

const template = new PayloadTemplate({
  requiredColor: '{{requiredColor:string in [black, white, green]}}',
  optionalColor: '{{optionalColor:string in [black, white, green] ?? omit}}',
  requiredColors: '{{requiredColors:string[black, white, green] || throw}}',
  optionalColors: '{{optionalColors:string[black, white, green] ?? omit}}',
});

template.variables();
// The contracts include allowedValues: ['black', 'white', 'green'].

template.render({ requiredColor: 'black', requiredColors: ['white', 'green'] }); // accepted
template.render({ requiredColor: 'black', requiredColors: [] }); // throws FALLBACK_THROW

type in [a, b] constrains a scalar to an enum-like set of literals. type[a, b] constrains every array member to that set. An outer ?? omit makes the value optional; || throw requires a non-empty array. Consumers can inspect the same literals through allowedValues from variables().

Interaction hints

Add a final @ name suffix when a consumer should treat a variable as interactive:

const form = new PayloadTemplate({
  comment: '{{comment:string ?? omit @ text.multiline}}',
  period: '{{period:string in [monthly, annual] @ choice}}',
});

form.variables();
// Contracts include interaction: 'text.multiline' and interaction: 'choice'.

Interaction names are opaque consumer metadata, not a payload-vars component catalogue. They match lowercase category or category.style syntax. The hint never changes types, requiredness, allowed values, plugins, or render() behavior; consumers define and document their own vocabulary.

Placeholders occupy an entire string. Types are string, number, boolean, string[], and number[]. Use ?? for nullish fallback or || for falsy fallback, with actions null, omit, or throw. Invalid templates throw PayloadTemplateError; inputs are never mutated.

TypeScript infers render() inputs from literal templates. Use as const on separately declared templates to preserve their literal types; see TypeScript input inference.

Plugins: your rules, inside plain JSON

Validate an email. Extract its domain. Turn calendar dates into UTC timestamps. All in the template.

Chain ! validators, ? conditional validators, and > transformers to express what a value must satisfy and how it should change. Built-in plugins cover dates, email addresses, and collections. Add your own reusable business rules with a few functions; the template stays portable JSON.

new PayloadTemplate('{{value:string ! email.email > email.domain}}')
  .render({ value: 'user@Example.com' }); // 'example.com'

new PayloadTemplate('{{value:string ? date.dateonly > date.isodatetime}}')
  .render({ value: 'not a date' }); // 'not a date'

new PayloadTemplate('{{value:string[ ! date.dateonly > date.isodatetime ] ! collection.range}}')
  .render({ value: ['2026-01-01', '2026-12-31'] });
// ['2026-01-01T00:00:00.000Z', '2026-12-31T00:00:00.000Z']

Use ! to throw on validation failure. Use ? to transform with > on success, or with the optional ~ alternative on failure. Without ~, failed conditional validation leaves the value unchanged. ?? and || still handle only nullish/falsy inputs before operations run. Built-in operations are always available, including with plugins: []. Register custom extensions with { plugins: [customPlugin] } and reference their operations as ! customPlugin.validateSomething or > customPlugin.transformSomething, using the plugin’s name as its namespace. All operations require namespace.operation. Custom plugins cannot use the 17 reserved built-in namespaces or override built-ins. The style namespace handles named casing and word styles; text handles trimming and whitespace normalization.

Explore Plugins → — built-ins, execution order, and a complete custom-plugin example.

Derived values and output omission

const template = new PayloadTemplate({
  date1: '{{date1:string > date.isodatetime > core.omit}}',
  date2: '{{date2:string > date.isodatetime > core.omit}}',
  hoursDifference: '{{hoursDifference:number = date.interval(date1,date2) > date.msToHours}}',
});
template.render({ date1: '2026-01-01', date2: '2026-01-02' });
// { hoursDifference: 24 }

Breaking change: bare function arguments now read evaluated template properties, including properties hidden by core.omit. Use date.interval($.date1,$.date2) to retain the original-input behavior, or date.interval(date1,$.date2) to mix sources. Raw date-only strings still fail date.interval validation; transformed ISO datetimes succeed.

Bare references are root-relative placeholder locations (date1, dates.start, items[0].date), not the input names inside placeholders. Array indices refer to template positions before omissions. References evaluate by dependency, once per location per render, regardless of property order; derived-property chains are supported, and unknown references and cycles fail at construction. Each ordinary occurrence starts from its original input. Derived output names are never required inputs. See function syntax for path restrictions and the function guide.

core.omit removes the containing property or array entry after evaluation. It must be an unconditional, terminal whole-value operation; root omission throws CANNOT_OMIT_ROOT. See derived functions for custom signatures, execution order, and extraction metadata.

Syntax highlighting

const expressions = template.tokenizePayloadExpression();
// expressions[0]:
// {
//   path: '$.orderId',
//   expression: '{{orderId:string}}',
//   tokens: [
//     { kind: 'delimiter', text: '{{', start: 0, end: 2 },
//     { kind: 'variable', text: 'orderId', start: 2, end: 9 },
//     { kind: 'punctuation', text: ':', start: 9, end: 10 },
//     { kind: 'type', text: 'string', start: 10, end: 16 },
//     { kind: 'delimiter', text: '}}', start: 16, end: 18 },
//   ],
// }

Each token has kind, text, start, and end. Map kinds to your own styles.

See the syntax guide and Development for the API, errors, TypeScript, and migration guidance.

Module formats

The package supports both ES modules and CommonJS, with matching TypeScript declarations.

// ES modules
import { PayloadTemplate } from 'payload-vars';

// CommonJS
const { PayloadTemplate } = require('payload-vars');

Import from the package root so your runtime selects the appropriate entry point.

Guides

Plugin namespaces

View the source on GitHub