Querying and Transforming JSON Using JSON-fx
By: Robb Eads
Tags: Coding, JavaScript, Programming, Web Development, Json

JSON-fx is an object query and transform engine for JavaScript. It defines an intuitive, declarative syntax for expressing an output value as a function of one or more input values.

Use cases could include any scenario in which the query of values from JSON data, or transformation of entire JSON objects, is best implemented through runtime configuration rather than hard-coded into application logic.

JSON-fx expressions are embedded in JSON objects known as templates, which are parsed and compiled at runtime into expression trees. These are efficient and lightweight wrappers around the native JavaScript code they execute. The library includes a large collection of built-in functions and operators and is easily extensible.

The JSON-fx core library, which includes the parser/compiler and a built-in set of functions, has no external runtime dependencies, giving it an extremely compact footprint.

Installation

Like many modern JavaScript libraries, JSON-fx is distributed as an NPM package. To install JSON-fx into a Node.js project or a modern browser client project that uses NPM (such as an Angular or React project):

npm install @mindsung/json-fx

A simple example

This will all be explained later, but let’s start with a very simple example that demonstrates the basic usage of the library.

import { JsonFx } from "@mindsung/json-fx"; const myInput = { n1: 10, n2: 5, s: "world!" }; const myTemplate = {

mySum: "$.n1 + $.n2",

myText: "'Hello, ' + $.s"

}; const fx = new JsonFx();

const compiled = fx.compile(myTemplate);

const output = compiled.evaluate({ name: "$", value: myInput });

Yields the output:

{

mySum: 15,

myText: "Hello, world!"

}

Template inputs

Template evaluation will typically include one or more input values. Often, there will be only one input, commonly named "$" as shown in the basic usage example above. However, any number of named input values may be supplied. These input variable names must begin with "$" , and they may be of any type. For example:

const inputs = [{

name: "$myInput",

value: 1000

}, {

name: "$myOtherInput",

value: {

someProp: {

anEvenDeeperProp: "some value"

}

}

}];

const output = compiled.evaluate(...inputs);

All inputs may then be referenced by their variable names within a template:

{

anOutputProp: "$myInput / 2",

anotherOutputProp: "$myOtherInput.someProp.anEvenDeeperProp + ' was input'"

}

Which would yield the output:

{

anOutputProp: 500,

anotherOutputProp: "some value was input"

}

Template expression syntax

Templates may consist of any valid JavaScript value or object. A template could be as simple as a single constant value (which would, of course, be useless, as no transformation or evaluation of any kind would take place), or a single JSON-fx string expression, or a complex JSON-fx object expression.

String expressions

A JSON-fx string expression is, as the name suggests, a string value, which contains one or more function calls and/or operators, resulting in exactly one output value or object. It will be parsed by the JSON-fx runtime when the template is compiled, creating an efficient expression tree that can be evaluated against any number of different input values.

const template = "math~floor($.someNumber / 10) % 10";

const compiled = fx.compile(template);

const input = {

name: "$",

value: { someNumber: 1234 }

};

const output1 = compiled.evaluate(input);

// output1 = 3

input.value = { someNumber: 2468 };

const output2 = compiled.evaluate(input);

// output2 = 6

In this example, the input $ is an object containing a single property someNumber . The template string defines a JSON-fx expression, math~floor($.someNumber / 10) % 10 , which effectively outputs the tens digit of $.someNumber using integer division and the modulus operator. The template is passed to fx.compile(...) which converts the string into a compiled expression. Next, compiled.evaluate(input) executes the expression against the provided input and returns the result, in this case the number 3 . Then, the value of the input is changed, and the expression is evaluated again, this time resulting in the number 6 .

Functions and operators

Function calls are the core components of any JSON-fx string expression. All string expressions are essentially a combination of one or more function evaluations. Even operators are a syntactic shortcut representing a call to an associated function. For example, the expression $a + $b is just a shorthand representation of the function call add($a, $b) .

In JSON-fx expressions, functions may be called using standard JavaScript function call syntax, i.e. the function name, opening parentheses, comma-separated arguments, and closing parentheses.

add($a, $b)

Functions may also be chained to other values, functions, or expressions using the infix : operator:

$a:add($b) is equivalent to add($a, $b)

The left-hand value $a is implicitly passed to add($b) as the first argument, displacing the other arguments. When chaining function calls together, the infix operator can provide better clarity in many situations:

toString(add($a, $b)) is better expressed as add($a, $b):toString() , or even $a:add($b):toString()

If the right-hand function requires only one argument (which is implicitly provided by the left-hand operand), then the parentheses may be omitted:

$a:add($b):toString

A comprehensive set of built-in JSON-fx functions provides the core functionality, exposing and extending many of the most useful JavaScript functions. Additionally, extension functions and operators can be injected to extend the template language by passing an array of definitions to the JsonFx class constructor.

Click here for a reference of all JSON-fx built-in functions and operators

Extension functions

A function definition is an object with the two properties, name and evaluate . The name property is the function name that will be used in expressions, and the evaluate property is a JavaScript function definition. (JSON-fx function definitions may also include an operator property, see JSON-fx source for examples. Custom extension functions will not typically include operator definitions, as most commonly-used operator symbols are already in use by built-in functions.)

const fx = new JsonFx([

{

name: "addFive",

evaluate: n => n + 5

}

]);

Value literals

String expressions may contain value literals, including numeric, string, and boolean values. Numeric literals don’t require any special notation. String literals must be contained within single quotes ' . Boolean values may be true or false . Additionally, the special values null and undefined may be used.

Array literals may also be embedded in string expressions using opening/closing square brackets [] .

const template = "(1.5 + 3):toString + ['hello', 2, true, null]:first";

Yields the value "4.5hello" when evaluated against any input (the expression includes no input variables).

Property accessor dot notation

As in JavaScript and many other programming languages, dot notation is used in string expressions to access object properties.

const compiled = fx.compile("$.someProp.someDeeperProp");

const output = compiled.evaluate({

name: "$",

value: {

someProp: {

someDeeperProp: "hello"

}

}

});

Yields the output "hello"

The null-conditional operator

JSON-fx defines a built-in null-conditional operator that allows many templates to appear cleaner and more readable, and avoid using frequent, verbose conditional logic for null checking and handling. The null-conditional operator ? may be used inline preceding any property dot accessor or function call. If the value of the expression immediately preceding the ? symbol is null or undefined, the expression will return null.

const template = "$.someProp?.someMissingProp?:substr(0, 3)?:toUpperCase";

When evaluated against any input $ , if the input value does not include a property someProp or a property someProp.someMissingProp , the expression will happily return a value of null rather than throwing a runtime error. This behavior can be especially useful when input objects can be expected to have varying structures or optional properties.

Anonymous (lambda) functions

Some functions require arguments that are expected to themselves be functions, such as an array find function, which expects an argument that is a predicate function, and finds the first member of the array matching a condition checked by the specified function. When calling such functions, the function-type arguments may be expressed using an anonymous function, or lambda, syntax.

Consistent with many programming languages, JSON-fx lambda syntax consists of one or more function argument variables, which must begin with "$" , followed by => , followed by an expression. If more than one argument is declared, the comma-separated argument list must be enclosed in parentheses. When only one argument is declared, the parentheses are optional.

const template = "[2, 4, 6, 8]:find(($n) => $n > 5)"

Yields the value 6 when evaluated against any input.

Object expressions

JSON-fx object expressions provide a powerful means by which objects of virtually any complexity may be transformed and queried. An object expression is a JavaScript object that includes properties representing some combination of JSON-fx variable declarations, JSON-fx runtime user-defined function declarations, and outputs. The value of each property on the object may be a constant (single value, object, or array), a JSON-fx string expression, or another JSON-fx object expression.

All properties on the object, except for those with the special notations discussed below, will also be present on the resulting output value.

const compiled = fx.compile({

someProp: "$ + 1",

anotherProp: {

deeperProp: "$ * 2"

}

});

const output = compiled.evaluate({ name: "$", value: 5 });

Yields the output value { someProp: 6, anotherProp: { deeperProp: 10 } }

Variables

In addition to the named input variables passed into a template evaluation, local variables may be declared as well. Variables in object expressions are properties beginning with the symbol "$" , and may be used anywhere in subsequent expressions. The value assigned to a variable property may be any constant or expression value.

const template = {

"$pi": 3.14159,

"$radiusSquared": "$radius * $radius", radius: "$radius",

area: "($pi * $radiusSquared * 100):math~round / 100"

};

When evaluated with an input $radius of value 3 will yield { radius: 3, area: 28.27 }

Runtime user-defined functions

While similar in behavior to extension and built-in functions, runtime user-defined functions are declared and implemented within the expression templates. This allows the capabilities of your templates to be extended even further without rebuilding and redeploying your application, and also provides a more compact and efficient way to reuse expression logic within your template.

Runtime user-defined functions in object expressions are properties beginning with the symbol "@" , and may be used anywhere in subsequent expressions. The value assigned to the property may technically be any constant or expression value, although only an expression value would make the function useful as opposed to a variable. Similarly, while function arguments aren't strictly required, a function without arguments would serve no advantage over simply declaring a variable instead. Function arguments, as with all JSON-fx variables, must begin with "$" .

const template = {

"@rectArea($length, $width)": "$length * $width",

area: "@rectArea($.length, $.width)"

};

When evaluated with an input $ of value { length: 3, width: 4 } will yield { area: 12 } .

Note that, as shown in the example above, when calling runtime user-defined functions, the function name must include the beginning @ symbol.

User-defined functions may also be used, instead of lambda functions, as arguments to other functions (either built-in, extension, or runtime user-defined) that expect function-type arguments. In this case, the function name, including @ , is passed as the argument value.

const template = {

"@isBigNumber($n)": "$n >= 100",

"$numbers": [50, 75, 100, 200],

firstBigNumber: "$numbers:find(@isBigNumber)"

};

Yields the value { firstBigNumber: 100 } when evaluated against any input.

Variable and user-defined function scope

The scope of any variable or user-defined function within an object expression is the object in which it is declared, including any number of levels of inner, nested objects. Because templates are compiled before they are evaluated, the actual declaration order of properties on the object is not significant. In other words, so long as it is in the same (or nested) object scope, it is not necessary that the property declaring a variable or user-defined function appear before it is used (although it will often improve readability).

const template = {

rectInfo: {

// @rectArea is in scope here because it was declared in

// the parent object.

area: "@rectArea($.length, $.width)"

},

// Valid to declare @rectArea here, even though it is used

// above this in the logical property order.

"@rectArea($length, $width)": "$length * $width"

};

Object value promotion

In addition to variable "$" and user-defined function "@" notation, a property name of "()" (empty parentheses) promotes the single value of that property to effectively become the value of its parent. Value promotion is useful when a single value is required, not an object value, but it is necessary or helpful to arrive at that value by using a more complex object expression, so that variables and/or user-defined functions may be used.

const template = {

"$pi": 3.14159,

"@area($radius)": "$pi * $radius:pow(2)",

"()": "@area($)"

};

When the above template is evaluated with an input $ value of 3 , the result is simply the number 28.27431 (not an object). Value promotion may be used in any nested level of object expressions, not just at the top level of the template object.

Note that when using value promotion notation, the "()" property must be the only property, aside from variable and user-defined function declarations, on the object. If other normal object properties exist on the object expression alongside the special value promotion property, it will result in a compile-time error.

Conclusion

There are several tools and libraries available in the JavaScript space for querying and transforming JSON data. JSON-fx was designed to address a level of performance, comprehensive capability, and extensibility not available in other offerings. The examples shown in this article will get you started, but only represent a tiny sampling of what is possible. I hope you’re able to put this tool to use and get as much value from it in your work as I have in mine!

Disclosure: I am a contributor to the JSON-fx project.