Skip to Content
  • Website
Codsen
  • Home
  • Open Source
  • Articles
  • About

prevOpen Source→json-variablesnext

json-variables12.2.1

Resolves custom-marked, cross-referenced paths in parsed JSON

Downloads per monthChangelogMIT Licenselibera manifesto
  • the top
  • Installation
  • Quick Take
  • Examples
  • PURPOSE
  • FEATURES
  • API — JVAR()
  • API — DEFAULTS
  • API — VERSION
  • THE USE EXAMP…
  • DATA CONTAIN…
  • IGNORES WITH…
  • WRAPPING
  • IN PRACTICE
  • MIXING BOOLEA…
  • Changelog

Installation

Quick Take

Examples

  • Allow a resolved value to be wrapped twice
  • Allow variables that cannot be resolved
  • Control how booleans resolve
  • Use a custom data container suffix
  • Use custom variable markers
  • Configure markers that resolve variables without output wrapping
  • Resolve variables from a data container
  • Switch off all wrapping
  • Ignore data containers
  • Leave the selected variables unwrapped
  • Skip wrapping using alternative markers
  • Reject a number inserted into a string
  • Reject values containing a lone marker
  • Resolve booleans to empty text
  • Replace unresolved variables with a fallback
  • A whole number keeps its type
  • Wrap the resolved values

Purpose

At the tactical level, this program lets you DRYopens in a new tab the plain objects (JSON files contents) — any special marker in any value can reference other values by path. As a result, there’s only one reference of each value, even though, values are used in many places.

At the strategic level, it’s used in email templating where templates (for example, Nunjucks) are separate from data, kept in JSON. We would tackle the complexity two ways:

  1. by separating data layers (static data “hi” and placeholders “John Smith” from ESP mappings ”{{ data.firstName }}“) into multiple JSON files
  2. by ensuring any value is used only once, even across multiple files (because otherwise it will be a liability when you’ll need to amend something)

json-variables helps with this second point.

Features

  • object-path notation (arrays use dots too: data.array.1 instead of data.array[1])
  • *_data keys to dump the data nearby (customisable naming pattern)
  • Detects existing heads and tails — it won’t wrap twice
  • Battle-tested, used in production to manage email templates

API — jVar()

The main function jVar() is imported like this:

It’s a function which takes two input arguments:

Input argumentTypeObligatoryDescription
input
Type: Plain object (likely, a result of parsed JSON)
Obligatory: yes
inputPlain object (likely, a result of parsed JSON)yesThe source to work upon.
opts
Type: Plain object
Obligatory: no
optsPlain objectnoAn Optional Options Object.

Input can’t be an array, the program will throw.

The Optional Options Object has the following shape:

KeyTypeDefaultDescription
heads
Type: String
Default: %%_
headsString%%_How do you want to mark the beginning of a variable?
tails
Type: String
Default: _%%
tailsString_%%How do you want to mark the ending of a variable?
headsNoWrap
Type: String
Default: %%-
headsNoWrapString%%-How do you want to mark the beginning of a variable, which you definitely don’t want to be wrapped?
tailsNoWrap
Type: String
Default: -%%
tailsNoWrapString-%%How do you want to mark the ending of a variable, which you definitely don’t want to be wrapped?
lookForDataContainers
Type: Boolean
Default: true
lookForDataContainersBooleantrueYou can put a separate dedicated key, named similarly, where the values for variables are placed.
dataContainerIdentifierTails
Type: String
Default: _data
dataContainerIdentifierTailsString_dataIf you do put your variables in dedicated keys besides, those keys will have to be different somehow. We recommend appending a string to the key’s name — tell here what string.
wrapHeadsWith
Type: String
Default: n/a
wrapHeadsWithStringn/aYou can optionally wrap each resolved string with a string. One to the left is called “heads”, please tell what string to use.
wrapTailsWith
Type: String
Default: n/a
wrapTailsWithStringn/aYou can optionally wrap each resolved string with a string. One to the right is called “tails”, please tell what string to use.
dontWrapVars
Type: Array of strings OR String
Default: n/a
dontWrapVarsArray of strings OR Stringn/aIf any of the variables (surrounded by heads and tails) can be matched by string(s) given here, it won’t be wrapped with wrapHeadsWith and wrapTailsWith. You can put wildcards (*) to note zero or more characters.
preventDoubleWrapping
Type: Boolean
Default: true
preventDoubleWrappingBooleantrueIf you use wrapHeadsWith and wrapTailsWith, the program can make sure the existing string does not contain these already. It’s to prevent double/triple/multiple wrapping.
wrapGlobalFlipSwitch
Type: Boolean
Default: true
wrapGlobalFlipSwitchBooleantrueGlobal flip switch to turn off the variable wrapping function completely, everywhere.
noSingleMarkers
Type: Boolean
Default: false
noSingleMarkersBooleanfalseIf any value in the source object has only and exactly heads or tails: a) do throw mismatched marker error (true) or b) don’t (false)
resolveToBoolIfAnyValuesContainBool
Type: Boolean
Default: true
resolveToBoolIfAnyValuesContainBoolBooleantrueThe very first moment Boolean is merged into a string value, it turns the whole value to its value. Permanently. Nothing else matters. When false and there’s a mix of Strings and Booleans, Boolean is resolved into empty string. When the value is just a reference marker, upon resolving it will be intact Boolean. This setting is relevant when there’s mixing of strings and Booleans — what to do in those cases.
resolveToFalseIfAnyValuesContainBool
Type: Boolean
Default: true
resolveToFalseIfAnyValuesContainBoolBooleantrueWhen there’s a mix of string and Boolean, resolve to false, no matter if the first encountered value is true. When there’s no mix with strings, the value is retained as it was.
throwWhenNonStringInsertedInString
Type: Boolean
Default: false
throwWhenNonStringInsertedInStringBooleanfalseBy default, if you want you can put objects as values into a string, you’ll get text text ... [object Object] text text .... If you want the renderer to throw an error instead when this happens, set this setting’s key to true.
allowUnresolved
Type: Boolean or String
Default: false
allowUnresolvedBoolean or StringfalseNormally, if a variable can’t be resolved, program throws an error. If you prefer missing values to resolve to a string, set it here. Putting true resolves to an empty string.

Here are all defaults in one place for copying:

The function returns the same input but with all and any variables resolved.

API — defaults

You can import defaults:

It's a plain object:

The main function calculates the options to be used by merging the options you passed with these defaults.

API — version

You can import version:

The use examples

If you don’t care how to mark the variables, use our notation, %%_, to mark a beginning of a variable (further called heads) and _%% to mark ending (further called tails).

Check this:

import { jVar } from "json-variables";
const res = jVar({
  a: "some text %%_var1_%% more text %%_var2_%%",
  b: "something",
  var1: "value1",
  var2: "value2",
});
console.log("res = " + JSON.stringify(res, null, 4));
// ==> {
//       a: 'some text value1 more text value2',
//       b: 'something',
//       var1: 'value1',
//       var2: 'value2'
//     }

You can declare your way to mark variables, your own heads and tails. For example, { and } (that’s a bad idea for production code, but it’s here to show it’s possible):

import { jVar } from "json-variables";
const res = jVar(
  {
    a: "some text {var1} more text {var2}",
    b: "something",
    var1: "value1",
    var2: "value2",
  },
  {
    heads: "{",
    tails: "}",
  },
);
console.log("res = " + JSON.stringify(res, null, 4));
// => {
//      a: 'some text value1 more text value2',
//      b: 'something',
//      var1: 'value1',
//      var2: 'value2'
//    }

You can also wrap all resolved variables with strings, a new pair of heads and tails, using opts.wrapHeadsWith and opts.wrapTailsWith. For example, bake some Java, wrap your variables with ${ and }:

import { jVar } from "json-variables";
const res = jVar(
  {
    a: "some text %%_var1_%% more text %%_var2_%%",
    b: "something",
    var1: "value1",
    var2: "value2",
  },
  {
    wrapHeadsWith: "${",
    wrapTailsWith: "}",
    dontWrapVars: ["*zzz", "*3", "*6"],
  },
);
console.log("res = " + JSON.stringify(res, null, 4));
// => {
//      a: 'some text ${value1} more text ${value2}',
//      b: 'something',
//      var1: 'value1',
//      var2: 'value2'
//    }

If variables reference keys which have values that reference other keys, that’s fine. Just ensure there’s no closed loop. Otherwise, renderer will throw and error.

import { jVar } from "json-variables";
const res = jVar({
  a: "%%_b_%%",
  b: "%%_c_%%",
  c: "%%_d_%%",
  d: "%%_e_%%",
  e: "%%_b_%%",
});
console.log("res = " + JSON.stringify(res, null, 4));
// THROWS because "e" loops to "b" forming an infinite loop.

This one’s OK:

import { jVar } from "json-variables";
const res = jVar({
  a: "%%_b_%%",
  b: "%%_c_%%",
  c: "%%_d_%%",
  d: "%%_e_%%",
  e: "zzz",
});
console.log("res = " + JSON.stringify(res, null, 4));
// => {
//      a: 'zzz',
//      b: 'zzz',
//      c: 'zzz',
//      d: 'zzz',
//      e: 'zzz'
//    }

Variables can also reference deeper levels within objects and arrays — just put dot like variable.key.subkey:

import { jVar } from "json-variables";
const res = jVar(
  {
    a: "some text %%_var1.key1_%% more text %%_var2.key2_%%",
    b: "something",
    var1: { key1: "value1" },
    var2: { key2: "value2" },
  },
  {
    wrapHeadsWith: "%%=",
    wrapTailsWith: "=%%",
    dontWrapVars: ["*zzz", "*3", "*6"],
  },
);
console.log("res = " + JSON.stringify(res, null, 4));
// => {
//      a: 'some text %%=value1=%% more text %%=value2=%%',
//      b: 'something',
//      var1: {key1: 'value1'},
//      var2: {key2: 'value2'}
//    }

Data containers

Data-wise, if you looked at a higher level, it might appear clunky to put values as separate values, like in examples above. Saving you time scrolling up, check this out:

{
  a: 'some text %%_var1_%% more text %%_var2_%%',
  b: 'something',
  var1: 'value1',
  var2: 'value2'
}

Does this look like clean data arrangement? Hell no. It’s convoluted and nasty. The keys var1 and var2 are not of the same status as an a and b, therefore can’t be mashed together at the same level, can it?

What if you placed all key’s a variables within a separate key, a_data — it starts with the same letter, so it will end up being nearby the a after sorting. Observe:

{
  a: 'some text %%_var1_%% more text %%_var2_%%',
  a_data: {
    var1: 'value1',
    var2: 'value2'
  },
  b: 'something'
}

That’s better, isn’t it? We think so too.

To set this up, you can rely on our default way of naming data keys (appending _data) or you can customise how to call data keys using opts.dataContainerIdentifierTails. On the other hand, you can also turn off this function completely via opts.lookForDataContainers and force all values to be the keys at the same level as the current variable’s key.

import { jVar } from "json-variables";
const res = jVar({
  a: "some text %%_var1_%% more text %%_var3_%%.",
  a_data: {
    var1: "value1",
    var3: "333333",
  },
  b: "something",
});
console.log("res = " + JSON.stringify(res, null, 4));
// => {
//      a: 'some text value1 more text 333333.',
//      b: 'something',
//      a_data: {
//        var1: 'value1',
//        var3: '333333'
//      }
//    }

Data container keys can also contain objects or arrays. Just query the whole path:

import { jVar } from "json-variables";
const res = jVar({
  a: "some text %%_var1.key1.key2.key3_%% more text %%_var3_%%.",
  a_data: {
    var1: { key1: { key2: { key3: "value1" } } },
    var3: "333333",
  },
  b: "something",
});
console.log("res = " + JSON.stringify(res, null, 4));
// => {
//      a: 'some text value1 more text 333333.',
//      b: 'something',
//      a_data: {
//        var1: {key1: {key2: {key3: 'value1'}}},
//        var3: '333333'
//      }
//    }

Ignores with wildcards

You can ignore the wrapping on any keys by supplying their name patterns in the options array, dontWrapVars value. It can be array or string and also it can contain wildcards:

import { jVar } from "json-variables";
const res = jVar(
  {
    a: "%%_b_%%",
    b: "%%_c_%%",
    c: "val",
  },
  {
    wrapHeadsWith: "{",
    wrapTailsWith: "}",
    dontWrapVars: ["b*", "c*"],
  },
);
console.log("res = " + JSON.stringify(res, null, 4));
// => {
//      a: 'val', <<< didn't get wrapped
//      b: 'val', <<< also didn't get wrapped
//      c: 'val'
//    }

Wrapping

Challenge:

How do you wrap one instance of a variable, but not another, when both are in the same string?

Solution: Alternative heads and tails, which are always non-wrapping: opts.headsNoWrap and opts.tailsNoWrap. Default values are: %%- and -%%. You can customise them to anything you want.

For example:

{
  "key": "%%-firstName-%% will not get wrapped but this one will: %%_firstName_%%",
  "firstName": "John"
}

When processed with options { wrapHeadsWith: '{{ ', wrapTailsWith: ' }}' }, it will be:

{
  "key": "John will not get wrapped but this one will: {{ John }}",
  "firstName": "John"
}

In practice

Wrapping of the variables is an essential feature when working with data structures that need to be adapted for both back-end and front-end. For the development, preview build you might want John as a first name, but for back-end build, you might want {{ user.firstName }}.

The following example shows how to “bake” HTML sprinkled with Nunjucksopens in a new tab notation (or any members of Jinja-like templating languages that use double curly braces):

HTML template:

<div>{{ hero_title_wrapper }}</div>

JSON for DEV build (a preview build to check how everything looks):

{
  "hero_title_wrapper": "%%_hero_title_%%",
  "hero_title": "Hi %%_first_name_%%, check out our seasonal offers!",
  "hero_title_alt": "Hi, check out our seasonal offers!",
  "first_name": "John"
}

In the above, hero_title_wrapper basically redirects to hero_title, which pulls John as a first name. The alternative title’s text is used when first_name is missing.

JSON for PROD version is minimal, only overwriting what’s different/new (to keep it DRY):

{
  "first_name": "user.firstName"
}

Now process the merged object of DEV and PROD JSON contents using { wrapHeadsWith: '{{ ', wrapTailsWith: ' }}' }, which instructs to wrap any resolved variables with {{ and }}.

In the end, our baked HTML template, ready to be put on the back-end will look like:

<div>{{ user.firstName }}, check out our seasonal offers!</div>

So far so good, but what happens if you want to add a check, does first_name exist? Again in a Nunjucks templating language, it would be something like:

content JSON for PROD build:

{
  "hero_title_wrapper": "{% if %%_first_name_%% %}%%_hero_title_%%{% else %}%%_hero_title_alt_%%{% endif %}",
  "first_name": "user.firstName"
}

with intention to bake the following HTML:

HTML template:

<div>{% if user.firstName %}Hi {{ user.firstName }}, check out our seasonal offers!{% else %}Hi, check out our seasonal offers!{% endif %}</div>

Now notice that in the example above, the first first_name does not need to be wrapped with {{ and }} because it’s already in a Nunjucks statement, but the second one does need to be wrapped.

You solve this by using non-wrapping heads and tails. Keeping default values opts.wrapHeadsWith and opts.wrapTailsWith it would look like:

content JSON for PROD build:

{
  "hero_title_wrapper": "{% if %%-first_name-%% %}%%_hero_title_%%{% else %}%%_hero_title_alt_%%{% endif %}",
  "first_name": "user.firstName"
}

Notice %%-first_name-%% above. The non-wrapping heads and tails instruct the program to skip wrapping, no matter what.

Mixing Booleans and strings

Very often, in email templating, the inactive modules are marked with Boolean false. When modules have content, they are marked with strings. There are cases when you want to resolve the whole variable to Boolean if upon resolving you end up with a mix of strings and Booleans.

When opts.resolveToBoolIfAnyValuesContainBool is set to true (default), it will always resolve to the value of the first encountered Boolean value. When set to false, it will resolve Booleans to empty strings.

When opts.resolveToFalseIfAnyValuesContainBool and opts.resolveToBoolIfAnyValuesContainBool are set to true (both defaults), every mix of string(s) and Boolean(s) will resolve to Boolean false. If opts.resolveToBoolIfAnyValuesContainBool is set to false, but opts.resolveToFalseIfAnyValuesContainBool to true, the mixes of strings and Booleans will resolve to the value of the first encountered Boolean variable’s value.

Observe:

const res = jVar({
  a: "zzz %%_b_%% zzz",
  b: true,
});
console.log("res = " + JSON.stringify(res, null, 4));
// => {
//      a: false, // <<< It's because opts.resolveToFalseIfAnyValuesContainBool is default, true
//      b: true
//    }
const res = jVar(
  {
    a: "zzz %%_b_%% zzz",
    b: true,
  },
  {
    resolveToFalseIfAnyValuesContainBool: false,
  },
);
console.log("res = " + JSON.stringify(res, null, 4));
// => {
//      a: true, <<< It's because there's a mix of string and Boolean, and first encountered Boolean value is `true`
//      b: true
//    }

Changelog

Open Changelog
↑ back to top
prev next

Copyright

All rights reserved © Roy Revelt 2026
All our open source packages are under MIT licenceopens in a new tab

Activities

🐛 See a bug? Raise an issueopens in a new tab
💘 Check out the Indiewebopens in a new tab and Libera manifestoopens in a new tab