npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

payload-vars

v2.4.0

Published

Typed variables in JSON payload templates

Readme

payload-vars

Open the playground · Read the documentation

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: '[email protected]' }); // '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.