Documentation
Declarative JSON-to-JSON transformation using JSONPath.
Introduction#
ajsont lets you describe the shape of your desired output as a
template, reference source values with
JSONPath
expressions, and let the library produce the result. There is no custom query
language to learn — just standard JSONPath plus a small set of
$-prefixed operators.
It's a great fit for event normalization, API response remapping, webhook adapters, and anywhere you'd otherwise hand-write a brittle mapping function.
Installation#
npm install @westonfleming/ajsont
The package ships dual ESM/CJS builds with full TypeScript types and has a single runtime dependency (jsonpath-plus).
Quick start#
import { transform } from "@westonfleming/ajsont";
const source = {
person: { firstName: "Jane", lastName: "Doe" },
contact: { email: "jane@example.com" },
metadata: { uid: "u-123" },
subscription: { plan: "enterprise" },
};
const spec = {
user: {
fullName: { $concat: ["$.person.firstName", " ", "$.person.lastName"] },
email: { $path: "$.contact.email", $onMissing: "omit" },
id: { $path: "$.metadata.uid" },
region: { $path: "$.geo.region", $default: "US" },
tier: { $if: { exists: "$.subscription" }, then: "premium", else: "free" },
},
};
const result = transform(source, spec);
// {
// user: {
// fullName: 'Jane Doe',
// email: 'jane@example.com',
// id: 'u-123',
// region: 'US',
// tier: 'premium'
// }
// }
How it works#
The mapping spec is the target shape. Every key in the spec becomes a key in the output. ajsont walks the spec recursively:
- Plain values (strings, numbers, booleans,
null) pass through as literals. - Objects with a
$-prefixed key are treated as operator nodes and resolved against the source. - Arrays are mapped element-by-element.
- Plain objects are recursed into key-by-key.
transform(source, spec, options?)#
Transform a source object according to a mapping spec. Returns the transformed result.
import { transform } from "@westonfleming/ajsont";
const result = transform(source, spec);
const result2 = transform(source, spec, { onMissing: "null" });
Parameters
| Parameter | Type | Description |
|---|---|---|
source | unknown | The source JSON object to transform. |
spec | SpecValue | The mapping specification (target-shaped template). |
options | TransformOptions | Optional global settings. |
Options
| Option | Type | Default | Description |
|---|---|---|---|
onMissing | 'omit' | 'null' | 'error' | 'omit' | Global default when a source path is missing. |
validateSpec(spec)#
Validate a mapping spec without executing it. Returns an array of validation errors (empty if valid).
import { validateSpec } from "@westonfleming/ajsont";
const errors = validateSpec(spec);
if (errors.length > 0) {
console.error("Invalid spec:", errors);
}
Each error has { path: string, message: string } describing where in the spec the issue is and what's wrong.
AjsontError#
Thrown when $onMissing (or the global onMissing) is set to 'error' and a path cannot be resolved.
import { transform, AjsontError } from "@westonfleming/ajsont";
try {
transform(source, spec);
} catch (err) {
if (err instanceof AjsontError) {
console.error(err.message); // "Missing value at path: $.foo.bar"
console.error(err.jsonPath); // "$.foo.bar"
}
}
Operators#
An operator node is an object with at least one $-prefixed key.
Here's the full set:
| Operator | Purpose |
|---|---|
$path | Extract a value via JSONPath. |
$literal | Return a value as-is, uninterpreted. |
$concat | Join values into a single string. |
$coalesce | First non-null value from a list. |
$lower / $upper / $trim | String transforms. |
$if | Conditional mapping. |
$map | Transform each item in an array. |
$filter | Keep only array items matching a condition. |
$find | Extract the first matching item from an array. |
$path — Extract a value#
Resolve a JSONPath expression against the source and place the result in the output.
{ "$path": "$.user.name" }Combine with $default or $onMissing to control behavior when the path doesn't exist:
{ "$path": "$.user.nickname", "$default": "Anonymous" }
{ "$path": "$.user.nickname", "$onMissing": "null" }$literal — Escape hatch#
Return a value as-is, without interpreting $-prefixed keys as operators. Use this when your target output needs to contain keys that start with $.
{ "$literal": { "$path": "this is not an operator, just data" } }$concat — Concatenate values#
Join multiple values into a single string. Items that start with $. are resolved as JSONPath; everything else is treated as a literal string.
{ "$concat": ["$.person.firstName", " ", "$.person.lastName"] }$coalesce — First available value#
Return the first non-null, non-undefined value from a list of JSONPath expressions.
{ "$coalesce": ["$.user.preferredName", "$.user.firstName", "$.user.id"] }Supports $default as a final fallback:
{ "$coalesce": ["$.primary", "$.secondary"], "$default": "unknown" }$lower / $upper / $trim — String transforms#
Resolve a JSONPath and transform the resulting string.
{ "$lower": "$.user.email" }
{ "$upper": "$.event.type" }
{ "$trim": "$.input.rawName" }$if — Conditional mapping#
Evaluate a condition and resolve either the then or else branch.
{
"$if": { "exists": "$.subscription" },
"then": "premium",
"else": "free"
}
Supported conditions: the same condition shape is shared by
$if, $filter, and
$find.
| Condition | Meaning |
|---|---|
{ "exists": "$.path" } | True if the path resolves to any value. |
{ "eq": ["$.path", "value"] } | True if the resolved value equals the literal. |
{ "ne": ["$.path", "value"] } | True if the resolved value does not equal the literal. |
{ "gt": ["$.path", 0] } | True if the resolved number is greater than the value. |
{ "lt": ["$.path", 0] } | True if the resolved number is less than the value. |
{ "gte": ["$.path", 0] } | True if the resolved number is greater than or equal to the value. |
{ "lte": ["$.path", 0] } | True if the resolved number is less than or equal to the value. |
The numeric comparisons (gt, lt, gte, lte)
coerce both sides to numbers; a missing or non-numeric value evaluates to false.
The then and else branches can be literal values, JSONPath strings, or nested operator nodes:
{
"$if": { "exists": "$.user.fullName" },
"then": "$.user.fullName",
"else": { "$concat": ["$.user.first", " ", "$.user.last"] }
}$map — Transform each item in an array#
Apply a nested spec to every element of the array resolved by $path. Each element becomes the JSONPath root for paths inside the nested spec, and all operators (including a further $map) are available.
{
"$path": "$.order.items",
"$map": { "name": { "$path": "$.title" }, "cost": { "$path": "$.price" } }
}$filter — Keep matching items#
Drop array elements that don't match a condition. The condition uses the same shape as $if (including the gt / lt / gte / lte comparisons). Use it standalone, or combine it with $map — filtering happens first, then the remaining items are reshaped.
{ "$path": "$.order.items", "$filter": { "gt": ["$.quantity", 0] } }Chaining $path → $filter → $map builds a normalized list in a single declarative step:
{
"$path": "$.order.items",
"$filter": { "gt": ["$.quantity", 0] },
"$map": {
"name": { "$path": "$.title" },
"total": { "$concat": ["$", "$.price"] }
}
}$find — Extract the first matching item#
Return the first array element matching a condition — the element itself, not an array. Use $default (or $onMissing) for the no-match case. $find and $filter cannot be used together on the same node.
{
"$path": "$.contacts",
"$find": { "eq": ["$.role", "primary"] },
"$default": null
}Missing property handling#
When a JSONPath doesn't match anything in the source, behavior is controlled at two levels.
Per-field: $onMissing
{ "$path": "$.optional.field", "$onMissing": "null" }Global: options.onMissing
transform(source, spec, { onMissing: "null" });| Strategy | Behavior |
|---|---|
'omit' | Key is excluded from output (default). |
'null' | Key is included with value null. |
'error' | Throws AjsontError. |
$onMissing always takes priority over the global option, and
$default takes priority over both.
{ "$path": "$.missing", "$default": "fallback value" }Arrays: $filter and $find
A $filter that removes every element produces an empty array ([]) in the
output — the key is kept, not omitted. If the source array itself is missing, the usual
$default → $onMissing → global precedence applies.
$find with no matching element follows the same precedence as $path:
$default wins if present, otherwise $onMissing ('omit' by
default, 'null', or 'error') and finally the global option.
Spec validation#
Use validateSpec to catch issues in a mapping spec before execution:
import { validateSpec } from "@westonfleming/ajsont";
const errors = validateSpec({
x: { $unknownOp: "$.a" },
y: { $if: { invalid: true } },
});
// [
// { path: '$.x', message: 'Unknown operator: $unknownOp' },
// { path: '$.y', message: '$if condition must have one of: exists, eq, ne' },
// { path: '$.y', message: '$if requires a "then" property' }
// ]TypeScript#
The package ships with full type definitions. Key exported types:
import type {
TransformOptions,
OnMissing,
SpecValue,
Condition,
IfCondition,
ArrayCondition,
} from "@westonfleming/ajsont";