
Product
Introducing Tier 1 Reachability: Precision CVE Triage for Enterprise Teams
Socket’s new Tier 1 Reachability filters out up to 80% of irrelevant CVEs, so security teams can focus on the vulnerabilities that matter.
Jempl is a JSON templating engine with conditionals, loops, partials, and custom functions.
npm install jempl
import { parseAndRender } from 'jempl';
const template = {
name: "${user.name}",
greeting: "Hello ${user.name}!",
"$if user.age >= 18": {
status: "adult"
},
"$else": {
status: "minor"
}
};
const data = {
user: { name: "John", age: 25 }
};
const result = parseAndRender(template, data);
// Output: { name: "John", greeting: "Hello John!", status: "adult" }
There is a Parse and Render phase
Let:
$D$ - Data
$T$ - Template
$F$ - Custom Functions
$A$ - AST (Abstract Syntax Tree)
$R$ - Result
Then:
$A = \mathtt{Parse}(T, F)$ - Parse template with functions to create AST
$R = \mathtt{Render}(A, D, F)$ - Render AST with data and functions
Or by composition:
$R = \mathtt{Render}(\mathtt{Parse}(T, F), D, F)$ - Parse and render in one step
During Parse
phase, the objective is to do all the performance critical work and validation. Parse
only makes use of Custom Functions
for validation purpose. This should be done at build time. The AST
should require minimal time to be rendered.
During Render
phase, the objective is to do the actual rendering. This should be done at runtime, and should be as fast as possible.
For more details about the AST structure, see AST Documentation.
Variables are referenced using ${variableName}
syntax. The library preserves the original data type when replacing standalone variables.
Notice that
age: "${age}"
), the original type is preserved"I am ${age} years old"
), it's converted to string"input placeholder=\"${placeholderText}\"":
)Variable names support various special characters:
${user-name}
${user:id}
${user@email}
${items[0]}
${user.profile.name}
Note: Variables with parentheses like ${func()}
are parsed as function calls, not variables.
template:
fullName: "${fullName.firstName} ${fullName.lastName}"
age: "${age}"
city: "I live in ${city}"
isAdult: ${isAdult}
firstHobby: "${hobbies[0]}"
allHobbies: "${hobbies}"
input placeholder="${placeholderText}":
cases:
- data:
fullName:
firstName: "John"
lastName: "Doe"
age: 30
city: "New York"
isAdult: true
hobbies: ["reading", "writing", "coding"]
placeholderText: "Enter your name"
output:
fullName: "John Doe"
age: 30
city: "I live in New York"
isAdult: true
firstHobby: "reading"
allHobbies: ["reading", "writing", "coding"]
input placeholder="Enter your name":
When a conditional evaluates to true, its properties are merged directly into the parent object. The key insight is that $if
, $elif
, and $else
are special keys that get evaluated and removed during processing.
template:
name: "${name}"
$if isAdult:
welcome: "You are an adult"
$elif age > 60:
welcome: "You are too old"
$else:
welcome: "You are too young"
cases:
- data:
name: "John"
age: 30
isAdult: true
output:
name: "John"
welcome: "You are an adult"
in case we want to have more than one conditional, we can use the #1
syntax, it works as long as property name is unique.
template:
name: "${name}"
$if#1 isAdult:
welcome: "You are an adult"
$elif#1 age > 60:
welcome: "You are too old"
$else#1:
welcome: "You are too young"
$if#2 isAdult:
welcome2: "You are an adult"
$elif#2 age > 60:
welcome2: "You are too old"
$else#2:
welcome2: "You are too young"
cases:
- data:
name: "John"
age: 30
isAdult: true
output:
name: "John"
welcome: "You are an adult"
welcome2: "You are an adult"
template:
name: "${name}"
$if isAdult:
$if age > 60:
welcome: "You are too old"
$else:
welcome: "You are too young"
cases:
- data:
name: "John"
age: 30
isAdult: true
output:
name: "John"
welcome: "You are too young"
- data:
name: "John"
age: 70
isAdult: false
output:
name: "John"
The $when
directive conditionally includes or excludes entire objects based on a condition. Unlike $if
which merges properties into the parent, $when
controls whether the entire object exists.
template:
# Object is included only if condition is true
user:
$when: showUserInfo
name: "${name}"
email: "${email}"
age: "${age}"
cases:
- data:
showUserInfo: true
name: "Alice"
email: "alice@example.com"
age: 30
output:
user:
name: "Alice"
email: "alice@example.com"
age: 30
- data:
showUserInfo: false
name: "Bob"
email: "bob@example.com"
age: 25
output: {} # user object is excluded entirely
$when
is particularly useful for filtering arrays - objects with false conditions are automatically excluded:
template:
menu:
- $when: true
label: "Home"
path: "/"
- $when: isLoggedIn
label: "Dashboard"
path: "/dashboard"
- $when: isAdmin
label: "Admin Panel"
path: "/admin"
- $when: false
label: "Hidden"
path: "/hidden"
cases:
- data:
isLoggedIn: true
isAdmin: false
output:
menu:
- label: "Home"
path: "/"
- label: "Dashboard"
path: "/dashboard"
# Admin and Hidden items are excluded
$when
supports all the same operators and expressions as $if
:
template:
permissions:
- $when: userRole == "admin" || userRole == "moderator"
action: "delete"
resource: "posts"
- $when: age >= 18 && hasConsent
action: "purchase"
resource: "products"
- $when: !isBlocked && emailVerified
action: "comment"
resource: "articles"
cases:
- data:
userRole: "moderator"
age: 25
hasConsent: true
isBlocked: false
emailVerified: true
output:
permissions:
- action: "delete"
resource: "posts"
- action: "purchase"
resource: "products"
- action: "comment"
resource: "articles"
You can use $when
and $if
together - $when
is evaluated first:
template:
card:
$when: showCard
title: "User Card"
$if isPremium:
badge: "Premium"
features: ["Feature A", "Feature B", "Feature C"]
$else:
badge: "Basic"
features: ["Feature A"]
cases:
- data:
showCard: true
isPremium: true
output:
card:
title: "User Card"
badge: "Premium"
features: ["Feature A", "Feature B", "Feature C"]
- data:
showCard: false
isPremium: true
output: {} # Entire card is excluded, $if is never evaluated
template:
app:
$when: appEnabled
header:
$when: showHeader
title: "My App"
logo: "logo.png"
content:
$when: hasContent
text: "Welcome"
footer:
copyright: "2024"
cases:
- data:
appEnabled: true
showHeader: true
hasContent: false
output:
app:
header:
title: "My App"
logo: "logo.png"
footer:
copyright: "2024"
# content is excluded
- data:
appEnabled: false
showHeader: true
hasContent: true
output: {} # Entire app is excluded, nested conditions are not evaluated
==
- Equal to!=
- Not equal to>
- Greater than>=
- Greater than or equal to<
- Less than<=
- Less than or equal toin
- Array/string contains value&&
- Logical AND||
- Logical OR!
- Logical NOT (negation)template:
name: "${name}"
# Equality and inequality
$if name == "John":
welcome1: "You are John"
$if name != "John":
welcome2: "You are not John"
# Numeric comparisons
$if age == 30:
welcome3: "You are exactly 30"
$if age >= 18:
welcome4: "You are an adult"
$if age > 65:
welcome5: "You are a senior"
$if age < 18:
welcome6: "You are a minor"
$if age <= 12:
welcome7: "You are a child"
# Boolean operations
$if isAdult:
welcome8: "You are an adult"
$if !isAdult:
welcome9: "You are not an adult"
# Array/string membership
$if "reading" in hobbies:
welcome10: "You like reading"
$if "o" in name:
welcome11: "Your name contains 'o'"
# Logical combinations
$if name == "John" && age >= 18:
welcome12: "You are adult John"
$if age < 18 || age > 65:
welcome13: "You get a discount"
cases:
- data:
name: "John"
age: 30
isAdult: true
hobbies: ["reading", "writing"]
output:
name: "John"
welcome1: "You are John"
welcome3: "You are exactly 30"
welcome4: "You are an adult"
welcome8: "You are an adult"
welcome10: "You like reading"
welcome11: "Your name contains 'o'"
welcome12: "You are adult John"
Loop through arrays using the $for
directive:
data:
people:
- name: "John"
age: 30
- name: "May"
age: 20
- name: "June"
age: 10
template:
people:
$for p, i in people:
- name: "${p.name}"
age: "${p.age}"
index: "${i}"
output:
people:
- name: "John"
age: 30
index: 0
- name: "May"
age: 20
index: 1
- name: "June"
age: 10
index: 2
You can use functions to transform arrays before iteration:
template:
# Sort posts by date
recentPosts:
$for post in sortDate(posts):
- title: "${post.title}"
date: "${post.date}"
# Filter products by a property
inStockItems:
$for item in filterBy(products, 'inStock', true):
- name: "${item.name}"
price: "${item.price}"
# Combine multiple functions
topPosts:
$for post in take(sortBy(posts, 'views'), 5):
- title: "${post.title}"
views: "${post.views}"
Custom functions for loops:
import { parseAndRender } from "jempl";
const customFunctions = {
// Sort array by date property
sortDate: (posts) => [...posts].sort((a, b) =>
new Date(a.date) - new Date(b.date)
),
// Sort by any property
sortBy: (arr, key) => [...arr].sort((a, b) =>
b[key] - a[key]
),
// Filter by property value
filterBy: (arr, key, value) =>
arr.filter(item => item[key] === value),
// Take first n items
take: (arr, n) => arr.slice(0, n),
};
const template = {
recent: {
$for: "post in sortDate(posts)",
items: [{
title: "${post.title}",
date: "${post.date}"
}]
}
};
const data = {
posts: [
{ title: "Third", date: "2024-03-15" },
{ title: "First", date: "2024-01-10" },
{ title: "Second", date: "2024-02-20" }
]
};
const result = parseAndRender(template, data, { functions: customFunctions });
// Output: sorted posts by date
# Filter active users
activeUsers:
$for user in filterActive(users):
- name: "${user.name}"
status: "online"
# Sort and limit results
topScores:
$for score, rank in take(sortBy(scores, 'points'), 10):
- rank: "${rank + 1}"
player: "${score.player}"
points: "${score.points}"
# Transform items during iteration
formattedDates:
$for item in transformDates(events):
- event: "${item.name}"
when: "${item.formattedDate}"
# Nested function calls
filteredAndSorted:
$for item in sortBy(filterBy(items, 'active', true), 'priority'):
- id: "${item.id}"
name: "${item.name}"
Note: Functions used in loops must return arrays. If a function returns a non-array value, a clear error message will be displayed.
Path references provide a way to get the full path to a loop variable rather than its value. This is useful for data binding in UI frameworks that need to know the location of data, not just its value.
Path references use #{variableName}
syntax and are only valid within loops:
template:
items:
$for item in items:
- path: "#{item}"
value: "${item}"
data:
items: ["first", "second", "third"]
output:
items:
- path: "items[0]"
value: "first"
- path: "items[1]"
value: "second"
- path: "items[2]"
value: "third"
You can reference properties of loop variables:
template:
products:
$for product in products:
- binding: "#{product.price}"
name: "${product.name}"
price: "${product.price}"
data:
products:
- { name: "Widget", price: 9.99 }
- { name: "Gadget", price: 19.99 }
output:
products:
- binding: "products[0].price"
name: "Widget"
price: 9.99
- binding: "products[1].price"
name: "Gadget"
price: 19.99
Path references work correctly in nested loops, maintaining the full path from root:
template:
$for category in categories:
- name: "${category.name}"
products:
$for product in category.products:
- path: "#{product}"
name: "${product.name}"
data:
categories:
- name: "Electronics"
products:
- { name: "Phone" }
- { name: "Laptop" }
- name: "Books"
products:
- { name: "Novel" }
output:
- name: "Electronics"
products:
- path: "categories[0].products[0]"
name: "Phone"
- path: "categories[0].products[1]"
name: "Laptop"
- name: "Books"
products:
- path: "categories[1].products[0]"
name: "Novel"
Path references are particularly useful for:
#{}
) only work with loop variables, not with global data${}
)#{i}
→ "0"
, "1"
, etc.Syntax | Purpose | Example Input | Example Output |
---|---|---|---|
${item} | Get the value | item = {id: 1} | {id: 1} |
#{item} | Get the path | Loop variable item | "items[0]" |
${item.id} | Get property value | item.id = 1 | 1 |
#{item.id} | Get property path | Loop variable item | "items[0].id" |
======= |
Partials allow you to define reusable template fragments that can be included and parameterized throughout your templates. This enables better code organization and reusability.
Partials are defined in the partials
section of the options and referenced using $partial
:
import { parseAndRender } from 'jempl';
const template = {
header: {
$partial: "greeting"
}
};
const data = {
name: "World"
};
const partials = {
greeting: {
message: "Hello ${name}!"
}
};
const result = parseAndRender(template, data, { partials });
// Output: { header: { message: "Hello World!" } }
You can pass data directly to a partial, which overrides the context data:
template:
card:
$partial: "userCard"
name: "Alice"
role: "Admin"
data:
name: "Bob"
role: "User"
partials:
userCard:
name: "${name}"
role: "${role}"
display: "${name} (${role})"
output:
card:
name: "Alice"
role: "Admin"
display: "Alice (Admin)"
Partials work seamlessly within arrays and loops:
template:
menu:
- $partial: "menuItem"
label: "Home"
path: "/"
- $partial: "menuItem"
label: "About"
path: "/about"
- $partial: "menuItem"
label: "Contact"
path: "/contact"
partials:
menuItem:
label: "${label}"
path: "${path}"
active: false
output:
menu:
- label: "Home"
path: "/"
active: false
- label: "About"
path: "/about"
active: false
- label: "Contact"
path: "/contact"
active: false
Combine partials with $for
loops for dynamic content generation:
template:
items:
$for item, i in items:
- $partial: "itemCard"
data:
items:
- id: 1
name: "First"
- id: 2
name: "Second"
partials:
itemCard:
id: "${item.id}"
label: "${item.name}"
index: "${i}"
output:
items:
- id: 1
label: "First"
index: 0
- id: 2
label: "Second"
index: 1
Partials can include other partials, enabling complex component hierarchies:
template:
page:
$partial: "layout"
title: "My Page"
data:
user: "John"
partials:
layout:
header:
$partial: "header"
content: "Welcome to ${title}"
header:
title: "${title}"
user: "${user}"
greeting: "Hello ${user}!"
output:
page:
header:
title: "My Page"
user: "John"
greeting: "Hello John!"
content: "Welcome to My Page"
Use conditionals within partials for dynamic rendering:
template:
users:
- $partial: "userStatus"
name: "Alice"
age: 25
- $partial: "userStatus"
name: "Bob"
age: 16
partials:
userStatus:
name: "${name}"
age: "${age}"
$if age >= 18:
status: "adult"
canVote: true
$else:
status: "minor"
canVote: false
output:
users:
- name: "Alice"
age: 25
status: "adult"
canVote: true
- name: "Bob"
age: 16
status: "minor"
canVote: false
Combine partials with $when
for conditional inclusion:
template:
menu:
- $when: true
$partial: "menuItem"
label: "Home"
path: "/"
- $when: isLoggedIn
$partial: "menuItem"
label: "Dashboard"
path: "/dashboard"
- $when: isAdmin
$partial: "menuItem"
label: "Admin"
path: "/admin"
data:
isLoggedIn: true
isAdmin: false
partials:
menuItem:
label: "${label}"
path: "${path}"
active: false
output:
menu:
- label: "Home"
path: "/"
active: false
- label: "Dashboard"
path: "/dashboard"
active: false
Partials support escaped dollar properties for keys that start with $
:
template:
pricing:
$partial: "pricing"
\$price: 99.99
$$currency: "USD"
displayPrice: "$99.99"
partials:
pricing:
price: "${$price}"
currency: "${$currency}"
display: "${displayPrice}"
output:
pricing:
price: 99.99
currency: "USD"
display: "$99.99"
Here's a comprehensive example showing partials used in a dashboard:
template:
dashboard:
$partial: "dashboard"
widgets:
- $partial: "widget"
type: "chart"
data: [10, 20, 30]
- $partial: "widget"
type: "table"
data: ["A", "B", "C"]
data:
user: "Admin"
partials:
dashboard:
title: "Dashboard for ${user}"
widgets: "${widgets}"
widget:
type: "${type}"
data: "${data}"
$if type == "chart":
visualization: "bar"
$elif type == "table":
visualization: "grid"
output:
dashboard:
title: "Dashboard for Admin"
widgets:
- type: "chart"
data: [10, 20, 30]
visualization: "bar"
- type: "table"
data: ["A", "B", "C"]
visualization: "grid"
Jempl provides clear error messages for common partial issues:
Render Error: Partial 'nonexistent' is not defined
Render Error: Circular partial reference detected: recursive
Parse Error: $partial value must be a string
Parse Error: Cannot use $partial with $if at the same level
$partial
with $if
, $elif
, $else
, or $for
at the same levelTo output literal ${
or #{
in strings, use backslash escaping:
data:
price: 100
items: ["first"]
template:
# Variable escaping
varLiteral: "The price is \\${price} (literal)"
varActual: "The actual price is ${price}"
varDouble: "Backslash and variable: \\\\${price}"
# Path reference escaping (within loops)
items:
$for item in items:
- pathLiteral: "Use \\#{item} for path references"
pathActual: "Path is #{item}"
pathDouble: "Backslash and path: \\\\#{item}"
output:
varLiteral: "The price is ${price} (literal)"
varActual: "The actual price is 100"
varDouble: "Backslash and variable: \\100"
items:
- pathLiteral: "Use #{item} for path references"
pathActual: "Path is items[0]"
pathDouble: "Backslash and path: \\items[0]"
The parser distinguishes between functions and variables based on parentheses:
${variableName}
- Parsed as a variable reference${functionName()}
- Parsed as a function call (even with no arguments)${add(5, 3)}
- Parsed as a function call with argumentsJempl includes one built-in function:
template:
timestamp: "${now()}"
message: "Generated at ${now()}"
output:
timestamp: 1640995200000
message: "Generated at 1640995200000"
Available Built-in Function:
now()
- Returns current timestamp in millisecondsCustom functions provide an escape hatch for advanced use cases while maintaining security and performance.
The library comes with built-in functions and allows registering custom ones.
Functions can return any JSON-serializable value, including objects and arrays.
Custom functions can be passed to the template engine and used in expressions:
import { parseAndRender } from "jempl";
const customFunctions = {
add: (a, b) => Number(a) + Number(b),
multiply: (a, b) => Number(a) * Number(b),
uppercase: (str) => String(str).toUpperCase(),
capitalize: (str) => {
const s = String(str);
return s.charAt(0).toUpperCase() + s.slice(1).toLowerCase();
},
};
const template = {
sum: "${add(10, 20)}",
greeting: "Hello ${capitalize(name)}!",
result: "${multiply(add(a, b), c)}",
};
const data = { name: "john", a: 5, b: 3, c: 2 };
const result = parseAndRender(template, data, { functions: customFunctions });
// Output: { sum: 30, greeting: "Hello John!", result: 16 }
Functions can return complex data structures including objects and arrays:
const customFunctions = {
createUser: (name, age) => ({
name: String(name),
age: Number(age),
isAdult: Number(age) >= 18,
metadata: {
createdAt: Date.now(),
version: 1,
},
}),
getStats: (items) => ({
count: Array.isArray(items) ? items.length : 0,
isEmpty: !Array.isArray(items) || items.length === 0,
summary: `${Array.isArray(items) ? items.length : 0} items`,
}),
};
template:
user: "${createUser(name, age)}"
stats: "${getStats(hobbies)}"
profile:
info: "${createUser(firstName, userAge)}"
activity: "${getStats(activities)}"
data:
name: "Alice"
age: 25
firstName: "Bob"
userAge: 17
hobbies: ["reading", "coding"]
activities: []
output:
user:
name: "Alice"
age: 25
isAdult: true
metadata:
createdAt: 1640995200000
version: 1
stats:
count: 2
isEmpty: false
summary: "2 items"
profile:
info:
name: "Bob"
age: 17
isAdult: false
metadata:
createdAt: 1640995200000
version: 1
activity:
count: 0
isEmpty: true
summary: "0 items"
When a function returns an object or array, it replaces the entire template value. This allows you to dynamically generate complex nested structures based on your data and logic.
now()
and random()
can be made deterministic via options for testingFunctions can be nested and combined with other expressions:
template:
# Nested function calls
formatted: "${formatDate(parseDate(dateString), 'YYYY-MM-DD')}"
The library will try to throw errors whenever an invalid expression is encountered. The library will try to give as much information as possible when an error occurs.
Jempl is designed for high-performance template rendering with ultra-fast execution suitable for real-time browser applications.
Template Type | Performance | Renders/sec | Use Case |
---|---|---|---|
Simple variables | ~0.001ms | 1,000,000+ | Basic interpolation |
Loop with 100 items | ~0.029ms | 34,000+ | Data lists |
Nested loops (10x10) | ~0.033ms | 30,000+ | Complex structures |
Conditionals in loops | ~0.004ms | 250,000+ | Dynamic filtering 🏆 |
Todo app template | ~0.139ms | 7,000+ | Real-world apps |
Jempl achieves sub-millisecond rendering for most templates, making it suitable for:
📖 Full Performance Documentation: See PERFORMANCE.md for detailed benchmarks, optimization techniques, and performance tuning tips.
For developers contributing to Jempl, please refer to our Development Guide which covers:
If you are looking for a more battle tested and feature rich library, we recommend json-e.
We were using json-e
before, and the reason we decided to build our own library was because of following limitations with json-e
:
if, elseif, else
functionality. $switch
came close, but it allows only one truthy condition.FAQs
A JSON templating engine with conditionals, loops, and custom functions
The npm package jempl receives a total of 108 weekly downloads. As such, jempl popularity was classified as not popular.
We found that jempl demonstrated a healthy version release cadence and project activity because the last version was released less than a year ago. It has 1 open source maintainer collaborating on the project.
Did you know?
Socket for GitHub automatically highlights issues in each pull request and monitors the health of all your open source dependencies. Discover the contents of your packages and block harmful activity before you install or update your dependencies.
Product
Socket’s new Tier 1 Reachability filters out up to 80% of irrelevant CVEs, so security teams can focus on the vulnerabilities that matter.
Research
/Security News
Ongoing npm supply chain attack spreads to DuckDB: multiple packages compromised with the same wallet-drainer malware.
Security News
The MCP Steering Committee has launched the official MCP Registry in preview, a central hub for discovering and publishing MCP servers.