@karmaniverous/jsonmap
v2.1.1
Published
A hyper-generic JSON mapping library.
Readme
JsonMap
JsonMap is a JSON mapping library that facilitates the transformation of an input JSON object according to a set of declarative rules.
npm install @karmaniverous/jsonmapJsonMap is hyper-generic: you bring your own mapping functions, which may be async and may be combined into complex transformation logic.
Why?
Mapping data from one form into another is a critical requirement of virtually every application.
JsonMap decouples mapping structure from mapping logic — and drives that decoupling deep into the logic layer.
The lib object contains your mapping functions, organized however you like. The map object is a plain JSON object (POJO) that expresses your mapping rules declaratively.
Because the map is a POJO:
- It can be stored in a database or config file.
- It does NOT express code as text, exposing a minimal threat surface.
- It transforms application logic into structured configuration data, enabling more generic, flexible applications.
Quick Start
import _ from 'lodash';
import numeral from 'numeral';
import { JsonMap } from '@karmaniverous/jsonmap';
// 1. Create a lib object with your mapping functions.
const lib = { _, numeral };
// 2. Define a map — a POJO expressing your transformation rules.
const map = {
name: {
$: { method: '$.lib._.get', params: ['$.input', 'user.name'] },
},
greeting: {
$: { method: '$.lib._.toUpper', params: '$.output.name' },
},
};
// 3. Create a JsonMap instance and transform your input.
const jsonMap = new JsonMap(map, lib);
const output = await jsonMap.transform({ user: { name: 'Alice' } });
// → { name: 'Alice', greeting: 'ALICE' }Map Structure
The transformation output mirrors the structure of your map object. Values in the map can be:
- Static values — passed through to output unchanged.
- Dynamic nodes — objects with a single
$key, containing one or more transformation steps. - Nested objects/arrays — recursively processed.
Dynamic Nodes
A dynamic node is an object with a single $ key. Its value is either a single transform or an array of transforms executed in sequence:
// Single transform
{ $: { method: '$.lib._.get', params: ['$.input', 'some.path'] } }
// Transform pipeline — output of each step feeds into the next
{
$: [
{ method: '$.lib._.get', params: ['$.input', 'value'] },
{ method: '$.lib.numeral', params: '$[0]' },
{ method: '$[0].format', params: '$0,0.00' },
],
}Each transform step has:
| Property | Type | Description |
| --- | --- | --- |
| method | string | Path to the function to call (see Path Syntax) |
| params | string \| string[] | One or more paths resolved as arguments to the method |
Path Syntax
All method and params values use lodash-style dot paths with special root prefixes:
| Prefix | Resolves to |
| --- | --- |
| $.lib.* | Your lib object (e.g. $.lib._.get) |
| $.input.* | The original input data |
| $.output.* | The output built so far (enables progressive transforms) |
| $[i].* | Result of the i-th previous transform step in the current pipeline (0 = most recent) |
Paths without a $ prefix are treated as literal strings.
Progressive Transformations
Because transforms are processed in key order and $.output.* references the output built so far, later keys can reference earlier ones:
const map = {
firstName: {
$: { method: '$.lib._.get', params: ['$.input', 'first'] },
},
// This runs AFTER firstName because keys are sorted
fullGreeting: {
$: { method: '$.lib._.toUpper', params: '$.output.firstName' },
},
};Private Keys ($-prefixed)
Keys starting with $ are stripped from the final output but are available during transformation via $.output.*. This enables intermediate computations:
const map = {
// Private: used for an API call, then stripped from output
$apiParams: {
merchantId: {
$: { method: '$.lib._.get', params: ['$.input', 'merchant.id'] },
},
},
// Public: references the private key's output
merchantName: {
$: {
method: '$.lib.fetchMerchant',
params: '$.output.$apiParams.merchantId',
},
},
};Controlling Key Stripping with ignore
The ignore option (a string or RegExp) controls which keys are stripped. The default is /^\$/ (all $-prefixed keys). You can override it to keep specific keys:
// Keep $metadata in output, strip all other $-prefixed keys
const jsonMap = new JsonMap(map, lib, { ignore: '^\\$(?!metadata)' });Recursive Evaluation
If a dynamic node's output is itself a dynamic node (an object with a single $ key), it will be re-evaluated recursively until a non-dynamic value is produced.
API
new JsonMap(map, lib, options?)
| Parameter | Type | Description |
| --- | --- | --- |
| map | JsonMapMap | The map definition (POJO) |
| lib | JsonMapLib | Object containing your mapping functions |
| options | JsonMapOptions | Optional. { ignore?: string \| RegExp } — pattern for keys to strip from output (default: /^\$/) |
jsonMap.transform(input): Promise<Json>
Transforms the input data according to the map. The transformation is asynchronous — your lib functions may be async.
Full Example
import _ from 'lodash';
import numeral from 'numeral';
import { JsonMap } from '@karmaniverous/jsonmap';
const lib = { _, numeral };
const map = {
foo: 'static value passed directly to output',
bar: [
{
static: 'another static value',
$remove: 'stripped from output (private key)',
dynamic: {
$: [
{
method: '$.lib._.get',
params: ['$.input', 'dynamodb.NewImage.roundup.N'],
},
{ method: '$.lib.numeral', params: '$[0]' },
{ method: '$[0].format', params: '$0,0.00' },
],
},
},
],
progressive: {
$: {
method: '$.lib._.toUpper',
params: '$.output.bar[0].static',
},
},
};
const jsonMap = new JsonMap(map, lib);
const output = await jsonMap.transform(someInput);JSON Schema & Zod Schemas
This package exports Zod schemas as the source of truth for all map-related types, plus a generated JSON Schema file for editor tooling and cross-language validation.
IDE autocomplete for config files
Point your JSON map config file at the published schema:
{
"$schema": "node_modules/@karmaniverous/jsonmap/jsonmap.schema.json",
"foo": "static value",
"bar": {
"$": {
"method": "$.lib._.get",
"params": ["$.input", "some.path"]
}
}
}Referencing the schema from other JSON Schema files
Use $ref to compose the JsonMap schema into your own:
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"type": "object",
"properties": {
"mappings": {
"$ref": "node_modules/@karmaniverous/jsonmap/jsonmap.schema.json"
}
}
}Composing the Zod schemas in TypeScript
Import the exported Zod schemas to build on top of them:
import { z } from 'zod';
import {
jsonMapMapSchema,
jsonMapTransformSchema,
jsonMapDynamicSchema,
jsonMapOptionsSchema,
} from '@karmaniverous/jsonmap';
// Extend with your own config shape
const myConfigSchema = z.object({
name: z.string(),
map: jsonMapMapSchema,
options: jsonMapOptionsSchema.optional(),
});
type MyConfig = z.infer<typeof myConfigSchema>;
// Validate at runtime
const config = myConfigSchema.parse(untrustedInput);Exported Schemas
| Schema | Describes |
| --- | --- |
| jsonMapTransformSchema | A single { method, params } transform step |
| jsonMapDynamicSchema | A { $: ... } dynamic value node |
| jsonMapMapSchema | A full recursive map definition (literals, objects, arrays) |
| jsonMapOptionsSchema | Constructor options ({ ignore?: string \| RegExp }) |
Exported Types
All types are derived from their Zod schemas via z.infer<>:
| Type | Description |
| --- | --- |
| JsonMapTransform | A single transform step |
| JsonMapDynamic | A dynamic value node |
| JsonMapMap | A recursive map definition |
| JsonMapOptions | Constructor options |
| JsonMapLib | Library of mapping functions |
| Json | Any valid JSON value |
| JsonFn | JSON replacer/reviver function |
| PathResolutionMap | Map of path patterns to resolver functions |
| PathResolutionParams | Parameters for path resolution |
Built for you with ❤️ on Bali! Find more great tools & templates on my GitHub Profile.
