In real-time software development, we often execute a set of statements to perform a task. Most of the time, those statements are common and do a single job, so we naturally move them into a function.
A function starts clean and simple. It takes a minimal set of inputs, performs its operations, and returns a predictable result. For straightforward requirements, this pattern works reliably.
However, real-world requirements are rarely static. Soon, that same functionality needs to vary slightly based on different conditions, specific flags, or changing business rules. To handle these variations, we begin introducing parameters to the function.
At first, it is just one additional parameter. Then another. Then an optional options object. As the number of parameters increases, the complexity of the function compounds rapidly.
When a function accepts too many parameters, maintaining and testing it becomes extremely difficult. The function is no longer just executing its core task; it is now branching on flags, validating combinations of arguments, and guarding against undefined values.
As systems grow, inputs stop being simple values and start becoming complex data structures. At that point, passing a large object into a function stops being a good design.
In real applications, incoming payloads contain dozens or hundreds of fields: customers, orders, voucher lines, stock allocations, tax rules, and metadata. When developers pass this entire payload into a single function, the function is forced to act as a tree navigator, drilling into nested keys just to find the few values it actually needs.
This is where the story of json-transformer begins. Despite its name, the true thought process of this repository is not monolithic JSON transformation — it is JSON Traversal to run functions.
Instead of passing a massive structure into a function and forcing the function to navigate it, we invert the responsibility: we navigate the structure from the outside, and attach small, focused functions only where actions need to happen.
The data exists in a JSON tree. A declarative instructions recipe defines where actions should take place. The traversal engine walks the data from the outside, and when it reaches an instructed node, it runs the function with only the minimal, focused inputs that node needs.
The function stays small, the parameters stay minimal, and the traversal engine never has to change.
The Transformation Function: How It Is Defined and How It Should Be Built
If the engine exists to run functions, how should a transformation function itself be defined?
In enterprise systems, transformation functions must obey strict invariants:
-
Single Configuration Input (
in...convention): A function must never accept loose positional parameters. It accepts a single named configuration object whose keys are prefixed within(e.g.{ inValue, inDirective }). -
Immediate Local Assignment (
local...convention): The top line of the function must unpack every input into an immutable local variable (e.g.const localValue = inValue;). This guarantees parameter immutability and instant visual traceability. - Pure Single Responsibility: One function does exactly one job. It never mixes key renaming, type coercion, recursion, and field extraction into a monolithic blob.
A Real-World Story: Normalizing the Company Tree
Consider an upstream accounting service that returns an enterprise company node:
// Source Data (The Food)
{
"COMPANY": {
"@_NAME": "mani9",
"@_RESERVEDNAME": ""
}
}
Downstream client systems expect two critical guarantees:
- Cardinality Normalization: The company value must be an array, because downstream consumers treat companies as a collection.
- Key Normalization: The root key must be idiomatic
Companies, and XML metadata keys like@_NAMEmust becomeName.
Traditional code would write a 40-line imperative function full of if (Array.isArray), Object.keys(), and mutations. In json-transformer, the transformation story is told declaratively with small functions:
// Transformation Recipe
{
"transform": {
"COMPANY": {
"alterKey": "Companies",
"valueType": "array"
}
},
"Companies": {
"transform": {
"@_NAME": { "alterKey": "Name" },
"@_RESERVEDNAME": { "alterKey": "ReserveName" }
}
}
}
Behind the Scenes: The Orchestrated Function Pipeline
Inside the engine (src/v9/ifTransformFound), instead of a bloated loop, each field passes through a pipeline of dedicated, single-purpose functions:
// src/v9/ifTransformFound/index.js
const directive = localTransform[key];
const newKey = resolveKey({ inKey: key, inDirective: directive });
let processedValue = value;
processedValue = applyTransform({ inValue: processedValue, inDirective: directive });
processedValue = normalizeValueType({ inValue: processedValue, inDirective: directive });
processedValue = extractValueKey({ inValue: processedValue, inDirective: directive });
newElement[newKey] = processedValue;
Notice how readable the story becomes:
resolveKey: TakesinKeyandinDirective→ decides the new property name (Companies).applyTransform: If a nested recipe exists → recurses cleanly throughtraverse.normalizeValueType: IfvalueType === "array"→ wraps the object into a guaranteed array[{ ... }].extractValueKey: IfvalueKeyis specified → unwraps child XML text.
The result is clean, predictable, and pure:
// Result JSON
{
"Companies": [
{
"Name": "mani9",
"ReserveName": ""
}
]
}
The engine orchestrates; the small functions execute. That is the complete story of the transformation function.