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#

terminal
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#

example.ts
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'
//   }
// }
Tip: Want to experiment without installing anything? Open the interactive playground and paste your own payload and spec.

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:

transform(source, spec, options?)#

Transform a source object according to a mapping spec. Returns the transformed result.

transform.ts
import { transform } from "@westonfleming/ajsont";

const result = transform(source, spec);
const result2 = transform(source, spec, { onMissing: "null" });

Parameters

ParameterTypeDescription
sourceunknownThe source JSON object to transform.
specSpecValueThe mapping specification (target-shaped template).
optionsTransformOptionsOptional global settings.

Options

OptionTypeDefaultDescription
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).

validate.ts
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.

error.ts
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:

OperatorPurpose
$pathExtract a value via JSONPath.
$literalReturn a value as-is, uninterpreted.
$concatJoin values into a single string.
$coalesceFirst non-null value from a list.
$lower / $upper / $trimString transforms.
$ifConditional mapping.
$mapTransform each item in an array.
$filterKeep only array items matching a condition.
$findExtract 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.

spec.json
{ "$path": "$.user.name" }

Combine with $default or $onMissing to control behavior when the path doesn't exist:

spec.json
{ "$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 $.

spec.json
{ "$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.

spec.json
{ "$concat": ["$.person.firstName", " ", "$.person.lastName"] }

$coalesce — First available value#

Return the first non-null, non-undefined value from a list of JSONPath expressions.

spec.json
{ "$coalesce": ["$.user.preferredName", "$.user.firstName", "$.user.id"] }

Supports $default as a final fallback:

spec.json
{ "$coalesce": ["$.primary", "$.secondary"], "$default": "unknown" }

$lower / $upper / $trim — String transforms#

Resolve a JSONPath and transform the resulting string.

spec.json
{ "$lower": "$.user.email" }
{ "$upper": "$.event.type" }
{ "$trim": "$.input.rawName" }

$if — Conditional mapping#

Evaluate a condition and resolve either the then or else branch.

spec.json
{
  "$if": { "exists": "$.subscription" },
  "then": "premium",
  "else": "free"
}

Supported conditions: the same condition shape is shared by $if, $filter, and $find.

ConditionMeaning
{ "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:

spec.json
{
  "$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.

spec.json
{
  "$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.

spec.json
{ "$path": "$.order.items", "$filter": { "gt": ["$.quantity", 0] } }

Chaining $path$filter$map builds a normalized list in a single declarative step:

spec.json
{
  "$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.

spec.json
{
  "$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

spec.json
{ "$path": "$.optional.field", "$onMissing": "null" }

Global: options.onMissing

transform.ts
transform(source, spec, { onMissing: "null" });
StrategyBehavior
'omit'Key is excluded from output (default).
'null'Key is included with value null.
'error'Throws AjsontError.
Priority: Per-field $onMissing always takes priority over the global option, and $default takes priority over both.
spec.json
{ "$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:

validate.ts
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:

types.ts
import type {
  TransformOptions,
  OnMissing,
  SpecValue,
  Condition,
  IfCondition,
  ArrayCondition,
} from "@westonfleming/ajsont";
Ready to try it? Head to the playground →