Jolt 2.5.0
dotnet add package Jolt --version 2.5.0
NuGet\Install-Package Jolt -Version 2.5.0
<PackageReference Include="Jolt" Version="2.5.0" />
<PackageVersion Include="Jolt" Version="2.5.0" />
<PackageReference Include="Jolt" />
paket add Jolt --version 2.5.0
#r "nuget: Jolt, 2.5.0"
#:package Jolt@2.5.0
#addin nuget:?package=Jolt&version=2.5.0
#tool nuget:?package=Jolt&version=2.5.0
<img src="https://raw.githubusercontent.com/Norhaven/Jolt/main/JoltLogo.png" width="100"/> Jolt - A JSON Transformation Language
Welcome! This is an imperative JSON transformation language inspired by XSLT and the wonderful .Net JSON adaptation of it over at JUST.Net. This project provides an expression-based interpreter for the language and a highly extensible way of approaching the same problem, namely How To Transform JSON Into Different JSON.
There are two packages generally available for this: Jolt.Json.Newtonsoft and Jolt.Json.DotNet. These use Newtonsoft or System.Text.Json functionality, respectively. All of the packages are multitargeted and support .Net Standard 2.0, .Net Standard 2.1, .Net 8.0, and .Net 10.0.
<h6>The third package, which the first two depend on, is called Jolt and should only need to be directly referenced if you are building your own integration on top of it as the other packages have done.</h6>
Disambiguation
Were you looking for the much older and unrelated .Net port of the Java JSON transformation library (by coincidence, also called Jolt) instead? That's over here!
Build/Release Status
| Package Name | Version |
|---|---|
| Jolt | |
| Jolt.Json.DotNet | |
| Jolt.Json.Newtonsoft | |
| Jolt Language | |
| Jolt Docs | Documentation + Jolt Playground |
<img align="left" src="https://raw.githubusercontent.com/Norhaven/Jolt/main/JoltLogo.png" width="40"/> To make Jolt development easier, we've created both a VSCode language extension that handles syntax coloring, code completions, on-hover documentation, and real-time validation using the Jolt library (available on the Visual Studio Marketplace and Open VSX) as well as a new documentation site with a playground where you can test out examples and try new things. Go check it out!
That's Great, But What Could I Use It For?
Let's say you have a third-party JSON-based REST API you need to call. Your data may already be in JSON, for instance in a NoSQL database, but the structure of your data doesn't match up with what the API endpoint is expecting. You need to transform your JSON into different JSON! That's an ideal use case for this project. You write a transformer in JSON that will take your data as input, make calls to any transformation business logic in your code to aid in the process, and provide you with the third-party API contract JSON as a result.
Any time you have JSON on hand that needs to be modified, whether it's merely adjusting the values and structure slightly or creating something completely different, that's an opportunity for a JSON transformer that uses Jolt.
Okay, So What Stops Me From Doing This Myself All In C#?
Absolutely nothing. If your changes are very minor, it may very well be easier and better to do that than to maintain transformer artifacts.
I would suggest, however, that there will be some future headaches in doing that as your transformation needs grow. The largest pain points with restructuring JSON are ensuring that 1) The values and structure are easily readable, maintainable, and reusable across time, and that 2) Your business logic is not mixed in with the restructuring logic. It's incredibly easy, especially taking AI into account, to just throw some code together that walks the JSON tree and makes a new thing. It's incredibly difficult to deal with the aftermath of doing that and ensure that you and other developers can understand and work with what you've done.
Let's take a quick tour of the language features and you can decide if this fits your own use case.
Syntax
The transformation language itself is pretty straightforward and primarily relies on defining and using methods to handle the transformation work needed, both as provided by the library within the package or external methods that you will create and register for use.
As a quick note before we move on, each property name or value that contains an expression must hold exactly one complete expression. Anything left over after it, such as an extra closing parenthesis (#valueOf($.a))) or a second expression without an operator between them (#valueOf($.a) @b), is a parsing error, as is an operator with nothing after it (#valueOf($.a) == ).
Methods
All method calls begin with a # symbol and are case-sensitive. Open and close parentheses surround the method arguments, which are comma-separated. Let's take a look at an example from the library.
{
"value": "#valueOf($.some.json.path)"
}
You'll notice the use of a JSON Path query as a parameter. This allows you to query the source document for the JSON value that exists at that path and use it in your transformation. Most of the library methods can take either a path, like above, or the value from the source document itself.
{
"valueExists": "#exists($.some.json.path)",
"otherValueExists": "#exists(#valueOf($.some.other.json.path))"
}
You can also optionally use the piped method syntax with an arrow in order to use the result of one method as the first parameter of another method. In that case, that parameter is removed from the second call.
{
"valueExists": "#valueOf($.some.json.path)->#exists()"
}
If you are familiar with C# extension methods, this is the same sort of concept. We can interchangeably use any method (including your own external methods) as either standalone or piped and pretend that a method call is an instance method for a little while to help with readability. We'll go into the available library methods further down, but let's talk first about how you'd possibly go about implementing your own method to use in a transformer.
External Methods
These are methods that you have created in your code. In order to use them, you will need to register them prior to using the transformer. Let's assume that you have the following C# method that you would like to be able to use. The easiest way to approach this is to use the JoltExternalMethod attribute from the Jolt.Library namespace. If the method name needs to be different when used from a transformer, specify an alias in the attribute constructor, such as [JoltExternalMethod("differentName")].
using Jolt.Library;
namespace JoltExample;
public class TransformerMethods
{
[JoltExternalMethod]
public static bool IsNotNull(string value) => value != null;
}
You will need to create a JSON transformer and register your methods with it prior to the actual transformation. There are two packages available by default: Jolt.Json.Newtonsoft and Jolt.Json.DotNet, which use either Newtonsoft or System.Text.Json as their particular flavor of implementation. Also, note that the Jolt.Json.DotNet package has an additional dependency, namely on JsonPath.Net, due to there being no default JSON Path support out of the box in System.Text.Json as of yet.
For our demonstration purposes here, we'll assume Newtonsoft.
using Jolt.Json.Newtonsoft;
// Omitting boilerplate class definition.
public string TransformationExample(string transformerJson, string sourceJsonDocument)
{
var transformer = JoltJsonTransformer.DefaultWith<TransformerMethods>(transformerJson);
return transformer.Transform(sourceJsonDocument);
}
Now that we've got the transformer set up in code and the method registered, let's create a JSON transformer that makes use of the new method.
{
"result": "#IsNotNull(#valueOf($.stringValue))"
}
You'll notice that the JSON path above is to a property called stringValue. This transformer assumes that you know that a string property with that name exists on the source document so, for example, this document here could be your source.
{
"stringValue": "not null"
}
That will execute your method with the value "not null" and create the following output.
{
"result": true
}
Your external methods that provide their output at the root level, as in the example above, must return a value that is either an acceptable JSON value (e.g. string, boolean), an IJsonToken instance, an EvaluationResult instance, or an instance that will be serialized to JSON.
The external methods you create may be either static or instance methods. We took a look at how a static method would be used above, and you can follow the same path for instance methods with one extra step. Let's assume we added the following instance method to the TransformerMethods class above.
[JoltExternalMethod]
public string ReverseString(string value) => new string(value.Reverse().ToArray());
In order to use the instance method, we need to pass an instance of TransformerMethods to the transformer creation call.
var transformer = JoltJsonTransformer.DefaultWith<TransformerMethods>(transformerJson, new TransformerMethods());
This instance will be used for all instance method calls during the transformation step, so you could easily modify your JSON transformer to include this in the same way as before. Keep in mind that your own methods can also take advantage of the piped method syntax, so let's demonstrate that here.
{
"reversed": "#valueOf($.stringValue)->#ReverseString()"
}
This will create the following output when used to transform the same source JSON we used earlier.
{
"reversed": "llun ton"
}
Math And Equality
The usual basic math operators are implemented, namely addition, subtraction, multiplication, and division, using +, -, *, and / respectively, along with comparison and equality as ==, !=, >, >=, <, and <=. Operations can be parenthesized as well. These are all used much like you're used to and can take the results of methods as operands. For example, assuming the following source JSON:
{
"first": 5,
"second": 4,
"third": 3,
"fourth": 2,
"fifth": 1
}
You could create a transformer that would look something like this:
{
"result": "#valueOf($.first) * #valueOf($.second) - #valueOf($.third) + #valueOf($.fourth) / #valueOf($.fifth) == 19"
}
And after transformation it would look like this:
{
"result": true
}
It's also worth noting that the addition operator + is overloaded and can serve as a shortcut to string concatenation, either with a source document value or a literal string. For example, in the following source JSON:
{
"value": "example"
}
And assuming that you have a transformer that looks like this:
{
"result": "#valueOf($.value) + #valueOf($.value) + 'hello'"
}
The output would look like this:
{
"result": "exampleexamplehello"
}
Logical Operations
The logical operators && and || are also implemented for use in your transformers. They take boolean operands and return a boolean result. For example, assuming the following source JSON:
{
"first": true,
"second": false,
"third": true
}
The logical OR operator || will return true if at least one of the operands is true (short-circuiting when it finds a true value), and false otherwise. The logical AND operator && will return true if both operands are true, and false otherwise (short-circuiting when it finds a false value). For example:
{
"orResult": "#valueOf($.first) || #valueOf($.second) || #valueOf($.third)",
"andResult": "#valueOf($.first) && #valueOf($.second) && #valueOf($.third)",
"combinedResult": "(#valueOf($.first) && #valueOf($.second)) || #valueOf($.third)"
}
The orResult property will evaluate to true because at least one of the operands is true, while the andResult property will evaluate to false because not all of the operands are true, and the combinedResult property will be true because the logical AND operators will be evaluated before the OR operator. The resulting JSON would look like this:
{
"orResult": true,
"andResult": false,
"combinedResult": true
}
And lastly, as you may be used to, you can also use a logical NOT operator ! to negate a boolean value. For example:
{
"notFirst": "!#valueOf($.first)",
"notSecond": "!#valueOf($.second)"
}
Conditional Logic
The library also provides a few ways to approach conditional logic in your transformers. The first is the #if method, which takes three parameters: a boolean condition, an expression to evaluate when that condition is true, and an expression to evaluate when false. For example:
{
"result": "#if(#valueOf($.someValue) > 5, 'Greater than 5', 'Less than or equal to 5')"
}
The result property will evaluate to "Greater than 5" if the value of $.someValue is greater than 5, and "Less than or equal to 5" otherwise.
Secondly, there is the #includeIf method, which takes a path or boolean condition and will evaluate and include the property's object if true, returning null otherwise. For example:
{
"#includeIf($.someValue > 5) into 'result'": {
"message": "Greater than 5"
}
}
This will include the result property with the nested message property if the value of $.someValue is greater than 5, and will return null for result otherwise.
Range Variables
There are times that you may want to perform a particular transform and then do something additional to it, whether that's simple math or looping over it. Most of the time, you'd also rather that the intermediary transform not stick around in the final JSON output, it's just there to do some preprocessing. This is an area where you may find that range variables work for your needs.
You can declare and set a variable in the property name one of two ways, either as the direct result of an expression or as the output of a loop. Let's take a look at both.
{
"@firstVar": "#valueOf($.someInteger)",
"@secondVar": "#valueOf($.someOtherInteger)",
"sum": "@firstVar + @secondVar"
}
This is one way to directly declare and use variables in your transformations. All variables are prefixed with the @ symbol and specifying it in the property name indicates that you would like to set it and also remove that JSON node from the transformed result. Let's see how you would be able to do some preprocessing on an array now.
{
"#foreach(@x in $.someArray) into @tempResult": [
{
"intermediateValue": "@x.someInteger * 2"
}
],
"#foreach(@x;@i in @tempResult) into 'actualResult'": [
{
"finalValue": "@x.intermediateValue + 5",
"currentIndex": "@i"
}
]
}
The first loop will set the variable @tempResult to the value of the transformed array, which will use the provided content template to transform each array element it finds, then removes the node from the output. The second loop takes the variable results as input and loops over that intermediary array, applying additional transformation to the results, naming the final array property as actualResult. Note that you can optionally get both the current value at the current iteration as well as the index by declaring two variables separated by a semicolon, the left is the value and the right is the index.
You can also declare variables within the content template of a loop, but they only exist within it and on a per-iteration basis (meaning that the variable is redeclared, set, and thrown away every time through the loop). Let's see an example of that here.
{
"#foreach(@x in $.someArray) into 'finalResult'": [
{
"@scopedVar": "@x.someInteger / 2",
"subtractedValue": "@scopedVar - 3"
}
],
"invalidValue": "@scopedVar"
}
In the resulting JSON output, the finalResult property will be populated by the array loop but the invalidValue property will be null due to accessing a variable which has already been destroyed. Variables of the same name as outer variables will shadow them, so you may want to avoid that.
Looping over an object's properties with a range variable is also possible, and in that case the variable will be set to the property value for each iteration. For example:
{
"#foreach(@x in $.someObject) into 'result'": [
{
"propertyValue": "@x"
}
]
}
If you hardcode the property name like that, however, you will overwrite the same property with the current value each iteration through. You may want to keep the original property name as well in that case, and you can do that by using the #nameOf method with the range variable as its parameter, which will return the name of the property currently being iterated on. For example:
{
"#foreach(@x in $.someObject) into 'result'": [
{
"#nameOf(@x)": "@x"
}
]
}
Range Variables: Lambda Expressions
There is also another use for range variables, namely in lambda expressions. Several methods in the library will take a lambda in order to better filter and/or refine the JSON data, used in the form of declaring one or more range variables (separated by a semicolon ;) and a body expression (separated by a colon :). A lambda variable's lifetime is scoped to the lambda body and will not be accessible outside of it. Let's take a quick look at a few methods which allow this usage.
{
"hasValues": "#valueOf($.some.integerArray)->#any(@x: @x > 5)",
"filteredData": "#valueOf($.some.complexArray)->#where(@x: @x.some.path < 10)",
"projectedData": "#valueOf($.some.complexArray)->#select(@x: @x.some.path)"
}
Assuming that we have the following data:
{
"some": {
"integerArray": [ 1, 2, 6, 10 ],
"complexArray": [
{
"some": {
"path": 5
}
},
{
"some": {
"path": 30
}
}
]
}
}
Using the previous transform, the output would look like:
{
"hasValues": true,
"filteredData": [
{
"some": {
"path": 5
}
}
],
"projectedData": [ 5, 30 ]
}
It's also worth noting that you can take a JSON-based lambda as a parameter in your custom external methods. These must always be of type Func<> in your custom method signature with generic type parameters being any type supported by Jolt external methods. Currently, this allows either one or two parameters and a return value. If you discover a use case for higher numbers of generic type parameters in your methods, feel free to reach out and we may support that further down the road.
For example, let's say you have the following external method that takes a lambda with one string parameter and a long return value.
[JoltExternalMethod("convertAllStringsToLongs")]
public static IEnumerable<long> ConvertAllStringsToLongs(IEnumerable<string> sequence, Func<string, long> convertStringToLong)
{
foreach (var value in sequence)
{
yield return convertStringToLong(value);
}
}
You could invoke that with a transformer that looks like this:
{
"converted": "#valueOf($.some.stringArray)->#convertAllStringsToLongs(@x: #toInteger(@x))"
}
You could also use a lambda with two parameters and a custom type that returns the object, so for example:
public sealed class ComplexObject
{
public int Id { get; set; }
public long Value { get; set; }
}
[JoltExternalMethod("accumulateWithCustomType")]
public static IEnumerable<ComplexObject> AccumulateWithCustomType(IEnumerable<long> sequence, Func<ComplexObject, long, long> accumulate)
{
var complexObject = new ComplexObject { Id = 0, Value = 0 };
foreach (var value in sequence)
{
complexObject.Id++;
complexObject.Value = accumulate(complexObject, value);
yield return complexObject;
}
}
So with a transformer like this:
{
"accumulated": "#valueOf($.some.integerArray)->#accumulateWithCustomType(@x;@y: @x.value + @y)"
}
You might get something like this as a result:
{
"accumulated": [
{
"id": 1,
"value": 1
},
{
"id": 2,
"value": 3
},
{
"id": 3,
"value": 6
},
{
"id": 4,
"value": 10
}
]
}
You may also notice that your custom data types are serialized to JSON with the camel-case naming convention. This is both to standardize on a style and make it easier to reason about styles, no context-specific changes needed.
Range Variables: Indexing and Slicing
You are also welcome to use a range expression to index into a string or array at a specific point or range as shorthand for calling the substring or slice library methods. Let's say we have the following source JSON:
{
"text": "this is an example",
"numbers": [ 1, 9, 3, 2, 6 ]
}
We can use the following transformer to obtain subsections of the string and array.
{
"@someText": "#valueOf($.text)",
"@someNumbers": "#valueOf($.numbers)",
"textStart": "@someText[..2]",
"textMiddle": "@someText[1..3]",
"textEnd": "@someText[^2..]",
"textCharacter": "@someText[^1]",
"arrayStart": "@someNumbers[..3]",
"arrayMiddle": "@someNumbers[2..4]",
"arrayEnd": "@someNumbers[^3..]",
"arrayElement": "@someNumbers[1]"
}
And that will create the resulting JSON:
{
"textStart": "th",
"textMiddle": "hi",
"textEnd": "le",
"textCharacter": "e",
"arrayStart": [ 1, 9, 3 ],
"arrayMiddle": [ 3, 2 ],
"arrayEnd": [ 3, 2, 6 ],
"arrayElement": 9
}
Range expressions immediately follow a variable and are enclosed in square brackets [ and ] which indicate indexing. They behave much the same as C# range expressions that you may already be used to, where a literal integer, range variable, or method return result indicates an offset index from the beginning of the string or array and the caret ^ indicates an index that is offset from the end of it. It's important to note that there are some standard library methods that take a range as a parameter, such as substring or slice, and in those cases you don't need to use square brackets and can specify the range expression as a first class citizen of the method call parameter, such as #substring($.some.text, 2..5) or #slice($.some.array, ^3..).
It's also worth noting that you can directly index into a string or array result of a method without going through variables at all. For example, #valueOf($.someString)[..2] would give the first two characters of the string retrieved by the valueOf method.
Range Variables: Using Blocks and Statements
A final use for range variables is in the case where you have a path or variable that needs direct modification to include, modify, or exclude specific JSON nodes. For example, you might not know the entire shape of the incoming JSON document but you know that it needs to stay the same except for a few changes, or perhaps you might want to replicate a few parts of the source document within the transformer output along with some tweaks to its data and/or structure. This would mean that you can't necessarily stand up an entire transformer that manually contains every node ahead of time. Let's take a look at a way you could approach this using the following source JSON.
{
"some": {
"integerArray": [ 1, 2, 6, 10 ],
"complexObject": {
"some": {
"path": 5
}
}
}
}
In this example, let's create a transformer that will take the properties from the some node, remove the integerArray property, add a property to the complexObject object, and change the value of the path property.
{
"#using($.some as @x) into 'result'": [
"#removeAt(@x.integerArray)",
"#setAt(@x.complexObject.new.value, 3)",
"#setAt(@x.complexObject.some.path, 20)"
]
}
This would output the following JSON:
{
"result": {
"complexObject": {
"new": {
"value": 3
},
"some": {
"path": 20
}
}
}
}
It's worth noting that statements are only allowed within a using block and must operate on the scoped range variable specified there. You may also notice that the #setAt() call was allowed to take a path that did not actually exist at the time of use. This is because that particular method will take care of creating the missing pieces of the JSON hierarchy for you if they don't currently exist.
You can also conditionally execute statements within a using block by using the #when method, which takes a boolean expression and at least one statement method.
{
"#using($.some as @x) into 'result'": [
"#when(#exists(@x.integerArray), #removeAt(@x.integerArray), #removeAt(@x.otherComplexObject))",
"#when(#valueOf(@x.complexObject.some.path) > 3, #setAt(@x.complexObject.some.other.path, 20))"
]
}
Following that up, you can also take advantage of pre-processing variables with a using block much the same way as a foreach loop does by assigning the output to a variable instead of a named property, which can be used either in a subsequent using block or other valid variable uses.
{
"#using($.some as @x) into @tempResult": [
"#removeAt(@x.integerArray)"
],
"#using(@tempResult as @x) into 'actualResult'": [
"#setAt(@x.some.path, 'text')"
]
}
And lastly, the #setAt method can take a value that's derived from an outer-scoped range variable or non-statement method call, much as you would with a when condition. Rewriting the previous example that used literal values to demonstrate this, we could do:
{
"@newValue": "#valueOf($.some.newValue)",
"#using($.some as @x) into 'result'": [
"#removeAt(@x.integerArray)",
"#setAt(@x.complexObject.new.value, @newValue)",
"#setAt(@x.complexObject.some.path, #valueOf($.someOtherValue))"
]
}
Error Handling
By default, if an error is encountered during the transformation process then the entire transformation will fail and an exception will be thrown. However, there are a few ways that you can handle errors within your transformers to allow for more graceful degradation of results in the case of unexpected input or other issues.
{
"default": "#try(#valueOf($.stringValue)->#toInteger(), @e: #valueOf($.defaultValue))",
"nullOnError": "#try(#valueOf($.stringValue)->#toInteger(), @e: null)"
}
The #try method takes two parameters: the first is the expression to evaluate and the second is a lambda that takes an exception parameter and returns the value to use in the case of an error. In the example above, the default property will attempt to convert a string value to an integer and if it fails it will return the value of $.defaultValue instead, while the nullOnError property will return null in the case of any error.
Creating Complex Literals
You can create arrays directly within your transformer by using square brackets [ and ] to enclose the array elements, which can be either literal values (including nested object and array literals), range variables, or the results of method calls. For example:
{
"@x": "#valueOf($.some.path)",
"array": "[ 1, 'two', null, @x, #valueOf($.some.path) ]"
}
Creating complex objects directly within the transformer is also possible by using curly braces { and } to enclose the object properties and their values. The property name must be a string literal, whereas the property value can be literal values (including nested object and array literals), range variables, or method call results. For example:
{
"@x": "#valueOf($.some.path)",
"object": "{ 'integerValue': 5, 'stringValue': 'test', 'booleanValue': true, 'rangeVariable': @x, 'methodCallResult': #valueOf($.some.path) }",
"nestedObject": "{ 'nested': { 'array': [ 1, 2, 3 ], 'object': { 'key': @x } } }"
}
These features allow you to construct complex JSON structures directly within your transformer without needing to rely on the shape of the input JSON document. These are available anywhere an expression in a property value is supported.
Pattern Matching
You can handle most of your typical data transformations with what we've talked about above and the standard library methods below, but you may want to make decisions based on the shape of your data or other factors in order to conditionally populate your JSON. Let's talk about the #match expression and see how we can expand upon your choices.
For example, you may have an expression like this:
{
"#match($.somePath as @x) into 'result'": [
{ "#is(object)": "'Found an object'" },
{ "#is(array)": "'Found an array'" },
{ "#is(string)": "'Found a string'" },
{ "#is(integer)": "'Found an integer'" },
{ "#is(decimal)": "'Found a decimal'" },
{ "#is(boolean)": "'Found a boolean'" },
{ "#is(null)": "'Found a null value'" }
]
}
These are various ways of type checking the same path and producing different results depending on what it happens to match to that evaluates to true. If you need more flexibility, you can use the #given method instead of (or in combination with) the #is method in order to evaluate arbitrary expressions.
{
"#match($.somePath as @x) into 'result'": [
{ "#given(#isObject(@x) && #valueOf($.someOther) > 5)": "'Found a specific object'" },
{ "#default()": "'Found something else'" }
]
}
You'll notice that this also includes a #default() method which takes no arguments and always evaluates to true, acting as a catch-all case for when your other cases have all failed.
In practice, these should help you out, but just in case you need more fine-grained control over objects and arrays, let's introduce pattern matching specifically for those two things.
{
"#match($.somePath as @x) into 'result'": [
{ "#is([])": "'Found an empty array'" },
{ "#is([_])": "'Found an array with one or more elements'" },
{ "#is(['test', _])": "'Found an array whose first element is `test` and contains at least one more element'" },
{ "#is([_, _])": "'Found an array with at least two elements'" }
]
}
The underscore _ character is a discard character, allowing it to match with any possible value. The explicit values, such as 'test', must match exactly. As a general rule, a discard at the tail end of an array pattern indicates one or more elements.
You can also match the same way with object literals and mix discards in where appropriate.
{
"#match($.somePath as @x) into 'result'": [
{ "#is({})": "'Found an object with zero or more properties'" },
{ "#is({ 'someName': 'someValue' })": "'Found an object with one specific property with a specific value and zero or more other properties'" },
{ "#is({ 'someName': { 'nested': _ } })": "'Found an object with one specific nested property that contains any value'" },
{ "#is({ 'someName': [_, 'someValue'] })": "'Found an object with one specific property that is an array where the first element is anything and the second is a specific value'" }
]
}
For object patterns, as a general rule the object may have any number of other properties but must have the ones you specify.
If you put an undeclared variable in either an array pattern or an object pattern, it will be set to the value that's currently in that location. These variables are populated as they are matched, so one earlier in a specific case would be usable later in that same case and are evaluated in the order they appear in the pattern. They will live, much like lambda variables, within the scope of their specific case and will be disposed of afterwards.
{
"#match($.somePath as @x) into 'result'": [
{ "#is({ 'name': @name, 'values': [@firstValue, _] })": "#append('Name is ', @name, ' and first value is ', @firstValue)" },
{ "#is({ 'function': @f }) && #given(#isObject(@f))": "'Found a function object'" }
]
}
Additional Operations
As a final note, there are a few additional operations that you can take advantage of in your transformers that don't necessarily fit into the previous categories. The first is the null-coalescing operator ?? which returns the left-hand operand if it is not null, otherwise it returns the right-hand operand. For example:
{
"coalescingResult": "#valueOf($.some.nullableValue) ?? 'default value'"
}
You can also use the null-safe path dereferencing operator ?. in your JSON paths to prevent null reference errors when traversing the source document, as below, which will return null when the property is null instead of throwing an error.
{
"@x": "#valueOf($.some)",
"safeDereference": "@x.nullableObject?.property"
}
Library Methods
This package comes with quite a few methods built into it to get you started, all of which are documented here and represent the most common things that you may want to do when transforming a JSON file. If you find that an opportunity for a new library method exists, please raise an issue and it will be considered. For more detail on the individual library methods, please see the Jolt wiki over here.
Additionally, keep in mind that you can pass range variables as parameters into nearly all standard library methods. You can also use the piped method syntax directly off of a range variable, so for example the following transformer represents a valid use of both of those concepts together.
{
"@someVariable": "#valueOf($.some.arrayPath)",
"result": "@someVariable->#select(@y: @y->#length())"
}
| Method | Description | Example | Valid On |
|---|---|---|---|
| valueOf | Gets the value of a property at the specified path | #valueOf($.some.path) |
Property Value |
| exists | Returns true if the provided value is not null, false otherwise | #exists($.some.path) |
Property Value |
| isNull | Returns true if the provided value is null, false otherwise | #isNull($.some.path) |
Property Value |
| isMissing | Returns true if the provided path does not exist in the source document, false otherwise | #isMissing($.some.path) |
Property Value |
| if | Takes three parameters: a boolean condition, an expression to evaluate when that condition is true, and an expression to evaluate when false | #if(#valueOf($.some.path), 'Yes', 'No') |
Property Value |
| includeIf | Takes a path or boolean condition and will evaluate and include the property's object if true, returning null otherwise | "#includeIf($.some.path) into 'someName'": { "nestedValue": "#valueOf($.other.path)" } |
Property Name |
| eval | Evaluates an arbitrary expression, either from a path or literal value, and returns the result | #eval('1 + 2 == 3') |
Property Name/Value, Unsafe* |
| foreach | Evaluates a path and loops over the array elements or object properties it finds there to create its property values, naming the property as the string literal referred to by the into keyword |
"#foreach(@x in $.some.path) into 'result'": [ { "templateValue": "#valueOf($.other.path)" } ] |
Property Name |
| nameOf | Returns the name of the property being evaluated by the provided loop variable | "#nameOf(@x)": "#valueOf($.some.path)" |
Property Name |
| indexOf | Returns the zero-based index value of the first occurrence of the provided value | #indexOf(#valueOf($.some.path), 'some string') |
Property Value |
| length | Returns the length of a string or array value | #length($.some.path) |
Property Value |
| toUpperCase | Returns the uppercase version of a string value | #toUpperCase($.some.path) |
Property Value |
| toLowerCase | Returns the lowercase version of a string value | #toLowerCase($.some.path) |
Property Value |
| trim | Returns the string value with all leading and trailing whitespace removed | #trim($.some.path) |
Property Value |
| trimStart | Returns the string value with all leading whitespace removed | #trimStart($.some.path) |
Property Value |
| trimEnd | Returns the string value with all trailing whitespace removed | #trimEnd($.some.path) |
Property Value |
| replace | Returns the string value with all occurrences of a specified substring replaced with another value | #replace($.some.path, 'old', 'new') |
Property Value |
| startsWith | Returns true if a string value starts with the provided substring, false otherwise | #startsWith($.some.path, 'start') |
Property Value |
| endsWith | Returns true if a string value ends with the provided substring, false otherwise | #endsWith($.some.path, 'end') |
Property Value |
| substring | Returns the string value that falls within the provided range in a given string | #substring($.some.path, 1..2) |
Property Value |
| isRegexMatch | Returns true if a string value matches the provided regular expression pattern, false otherwise | #isRegexMatch($.some.path, '^pattern$') |
Property Value |
| regexReplace | Returns the string value with all occurrences of a specified regular expression pattern replaced with another value | #regexReplace($.some.path, '^pattern$', 'new') |
Property Value |
| slice | Returns the array slice that falls within the provided range in a given array | #slice($.some.path, 1..2) |
Property Value |
| reverse | Returns the string or array value with its elements in reverse order | #reverse($.some.path) |
Property Value |
| flatten | Returns a single array that is the result of recursively flattening an array of nested arrays | #flatten($.some.path) |
Property Value |
| groupBy | Returns a JSON object that represents the grouping of an array's contents by its individual property values | #groupBy($.some.path, @x: @x.propertyName) |
Property Value |
| summarizeWith | Returns an object array that's the result of applying an aggregate method to a grouped array's key and/or results | #summarizeWith($.some.group, @group: #someAggregateMethod(@group.key, @group.results)) |
Property Value ** |
| orderBy | Returns an array in ascending order as determined by its individual property values | #orderBy($.some.path, @x: @x.propertyName) |
Property Value |
| orderByDesc | Returns an array in descending order as determined by its individual property values | #orderByDesc($.some.path, @x: @x.propertyName) |
Property Value |
| take | Returns an array containing the leading elements of an array up to the specified count | #take($.some.path, 5) |
Property Value |
| skip | Returns an array excluding the leading elements of an array up to the specified count | #skip($.some.path, 5) |
Property Value |
| takeWhile | Returns an array containing the leading elements of an array that satisfy a specified condition | #takeWhile($.some.path, @x: @x.propertyName > 5) |
Property Value |
| skipWhile | Returns an array excluding the leading elements of an array that satisfy a specified condition | #skipWhile($.some.path, @x: @x.propertyName > 5) |
Property Value |
| distinct | Returns an array of items associated with distinct values of a specified property or scalar values | #distinct($.some.path, @x: @x.propertyName) |
Property Value |
| reduce | Returns a single value that is the accumulated result of applying a two-parameter lambda (the accumulated value and the current value) to each element of an array, with an optional seed value. Returns the seed value (if any) when the array is empty. | #reduce($.some.path, @acc;@current: @acc + @current, @optionalSeedValue) |
Property Value |
| zip | Returns a single array that is the result of combining two arrays by pairs with a two-parameter lambda (the first element and the second element) | #zip($.some.path, $.some.other.path, @x;@y: { 'first': @x, 'second': @y }) |
Property Value |
| keysFrom | Returns an array of the property names of an object | #keysFrom($.some.path) |
Property Value |
| valuesFrom | Returns an array of the property values of an object | #valuesFrom($.some.path) |
Property Value |
| contains | Returns true when an array or string contains the provided value | #contains($.some.path, 'some string') |
Property Value |
| roundTo | Returns the value of a provided number rounded to the specified decimal places | #roundTo($.some.path, 2) |
Property Value |
| try | Evaluates an expression and returns the result, or if an error is encountered it evaluates the provided lambda with the exception as a parameter and returns that result instead | #try(#valueOf($.some.path)->#toInteger(), @e: 'default value') |
Property Value |
| max | Returns the maximum value found within an array of numbers (optionally with a projection lambda) | #max($.some.path, @x: @x.someValue) |
Property Value |
| min | Returns the minimum value found within an array of numbers (optionally with a projection lambda) | #min($.some.path, @x: @x.someValue) |
Property Value |
| sum | Returns the total value found within an array of numbers (optionally with a projection lambda) | #sum($.some.path, @x: @x.someValue) |
Property Value |
| average | Returns the average value found within an array of numbers (optionally with a projection lambda) | #average($.some.path, @x: @x.someValue) |
Property Value |
| joinWith | Returns a string that joins all elements of an array with the provided delimiter | #joinWith($.some.path, ',') |
Property Name/Value |
| splitOn | Returns an array of substrings from a string value splitting on the provided delimiter | #splitOn($.some.path, ',') |
Property Value |
| append | Returns a string or array made from appending one or more strings or arrays onto them | #append($.some.path, 'one', 'two') |
Property Value |
| currentDateTime | Returns the current date and time as a string in ISO 8601 format | #currentDateTime() |
Property Value |
| currentDateTimeUtc | Returns the current date and time in UTC as a string in ISO 8601 format | #currentDateTimeUtc() |
Property Value |
| parseDateTime | Returns a string in ISO 8601 format that represents the date and time value of the provided string, requires exact format with optional format string | #parseDateTime($.some.path, 'yyyy-MM-dd') |
Property Value |
| formatDateTime | Returns a string that represents the date and time value of the provided string formatted according to the provided format string | #formatDateTime($.some.path, 'yyyy-MM-dd') |
Property Value |
| addDays | Returns a string in ISO 8601 format that represents the date and time value of the provided string with the specified number of days added to it | #addDays($.some.path, 7) |
Property Value |
| addHours | Returns a string in ISO 8601 format that represents the date and time value of the provided string with the specified number of hours added to it | #addHours($.some.path, 5) |
Property Value |
| addMinutes | Returns a string in ISO 8601 format that represents the date and time value of the provided string with the specified number of minutes added to it | #addMinutes($.some.path, 30) |
Property Value |
| isInteger | Returns true if the value is represented as a whole number | #isInteger($.some.path) |
Property Value |
| isString | Returns true if the value is represented as a string | #isString($.some.path) |
Property Value |
| isDecimal | Returns true if the value is represented as a floating point number | #isDecimal($.some.path) |
Property Value |
| isBoolean | Returns true if the value is represented as a boolean | #isBoolean($.some.path) |
Property Value |
| isArray | Returns true if the value is represented as an array, false otherwise | #isArray($.some.path) |
Property Value |
| isObject | Returns true if the value is represented as an object, false otherwise | #isObject($.some.path) |
Property Value |
| isEmpty | Returns true if the value is an array or string with no contents, false otherwise | #isEmpty($.some.path) |
Property Value |
| toInteger | Returns a value converted to a whole number | #toInteger($.some.path) |
Property Value |
| toString | Returns the string representation of a value | #toString($.some.path) |
Property Value |
| toDecimal | Returns a value converted to a floating point number | #toDecimal($.some.path) |
Property Value |
| toBoolean | Returns a value converted to a boolean | #toBoolean($.some.path) |
Property Value |
| merge | Returns an object that is the result of merging two or more objects together with a deep copy, with later values taking precedence over earlier ones | #merge($.some.path, $.some.other.path) |
Property Value |
| any | Returns true if the value is an array or string with contents, false otherwise (lambda parameter is optional) | #any($.some.path, @x: @x.someValue == 5) |
Property Value |
| where | Returns an array of objects that match a predicate | #where($.some.path, @x: @x.other.path > 2) |
Property Value |
| select | Returns an array of objects that are the result of a projection | #select($.some.path, @x: @x.other.path) |
Property Value |
| transformWith | Returns an object that is the result of transforming a document sub-path using a standalone transformer | #transformWith($.some.path, 'SomeNamedTransformer') |
Property Value |
| using | Assigns a specific path to a range variable and allows statements to operate on it | "#using($.some.path as @x) into 'result'":[ "#setAt(@x.other.path, 5)" ] |
Property Name |
| removeAt | Removes a JSON node from the provided variable-based path | #removeAt(@x.some.path) |
Statement |
| setAt | Adds or modifies a JSON node specified with the provided variable-based path | #setAt(@x.some.path, 5) |
Statement |
| when | Conditionally executes a statement based on a boolean expression | #when(#exists(@x.integerArray), #removeAt(@x.integerArray)) |
Statement |
| match | Matches a value against a set of conditions, evaluating from top to bottom and returning the value of the first one matched | "#match($.path as @x) into 'result'": [ ... ] |
Property Name |
| is | Takes a type literal, array pattern, object pattern, or scalar literal value and tries to match it against the value present in the #match expression | { "#is(object)": "'some result'" } |
Property Name, Matching |
| given | Takes an expression and evaluates the result as a boolean, succeeding on true and failing on false | { "#given(@x > 5)": "'some result'" } |
Property Name, Matching |
| default | Acts as a catch-all in a match expression, always evaluating to true | { "#default()": "'some result'" } |
Property Name, Matching |
<h6>* The #eval method is considered unsafe because it can execute any expression, including ones that may have unwanted side effects or security implications. It should be used with caution and only with trusted input. In order to enable unsafe method usage, the JoltOptions instance that can be passed into your JoltJsonTransformer has a method called WithUnsafeAllowed that will enable this. Use with caution!</h6>
<h6>** I'd also like to call out the summarizeWith method, which on its surface can be a little confusing, so let's break it apart a little bit in the next section.</h6>
SummarizeWith
The summarizeWith method is a powerful tool that allows you to perform aggregation operations on a grouped array of objects. It takes a path or value to the group, and a lambda function that defines how to summarize the group. Let's take a look at some objects in an array that could be grouped.
{
"someArray": [
{
"groupingProperty": "A",
"valueProperty": 1
},
{
"groupingProperty": "A",
"valueProperty": 2
},
{
"groupingProperty": "B",
"valueProperty": 3
}
]
}
Let's do a simple grouping and see what that outputs.
{
"groupedArray": "#groupBy($.someArray, @x: @x.groupingProperty)"
}
The result of that groupBy operation would be:
{
"groupedArray": [
{
"key": "A",
"results": [
{
"groupingProperty": "A",
"valueProperty": 1
},
{
"groupingProperty": "A",
"valueProperty": 2
}
]
},
{
"key": "B",
"results": [
{
"groupingProperty": "B",
"valueProperty": 3
}
]
}
]
}
That grouped things all right, but it's a bit verbose and tries to keep all of the original data. Let's say that we just want to get an idea of the totals for the valueProperty for each group. We can use the summarizeWith method to do that.
{
"summarizedArray": "#valueOf($.someArray)->#groupBy(@x: @x.groupingProperty)->#summarizeWith(@group: { 'name': @group.key + ' total', 'totalValue': #sum(@group.results, @y: @y.valueProperty) })"
}
You'll notice that summarizeWith takes a lambda which will give us access to each group, both the key and the results, and store the summarized data with a property for the key and one for the summary value. In the example above, for each grouping we're creating an object that contains both a name that incorporates the group key as well as the sum of the results. This will give us a summarized array that looks like this:
{
"summarizedArray": [
{
"key": "A",
"value": {
"name": "A total",
"totalValue": 3
}
},
{
"key": "B",
"value": {
"name": "B total",
"totalValue": 3
}
}
]
}
It doesn't need to be an object, you could just directly apply some method to the @group.results. If, for example, we'd done #summarizeWith(@group: #sum(@group.results)) on the grouping call above, it would have just produced this:
{
"summarizedArray": [
{
"key": "A",
"value": 3
},
{
"key": "B",
"value": 3
}
]
}
It's also worth noting that the summarizeWith method does not explicitly need to be called off of a groupBy method, as long as what you are feeding it is in the shape of what the groupBy method outputs (namely a key and a results array). The groupBy method just takes some of that work out of the process for you.
Operator Precedence And Grammar
Also, here is a small table of how Jolt regards levels of operator precedence to help you understand how your expressions may be evaluated at runtime. The higher the precedence level, the tighter it binds (e.g. * binds tighter than + and so multiplication is performed first). The unary logical NOT ! binds tighter than any binary operator, so it only negates the operand immediately following it (e.g. !a && b is evaluated as (!a) && b); use parentheses to negate a larger expression, such as !(a && b).
| Operator | Description | Level |
|---|---|---|
| Null Coalescing | ?? |
0 |
| Logical Or | \|\| |
1 |
| Logical And | && |
2 |
| Equals | == |
3 |
| Not Equals | != |
3 |
| Less Than | < |
4 |
| Greater Than | > |
4 |
| Less Than Or Equals | <= |
4 |
| Greater Than Or Equals | >= |
4 |
| Addition | + |
5 |
| Subtraction | - |
5 |
| Multiplication | * |
6 |
| Division | / |
6 |
| Logical Not (Unary) | ! |
7 |
For reference, the full EBNF grammar lives over here.
Alternate External Method Registrations
There are additional ways available to register your external methods, let's take a look at another one.
Method Registration
The slight bit of added complexity that the previous way (using the [JoltExternalMethod] attribute) hid from you is the registration process. If you'd like to do this process manually, you will need a few things.
The first thing is the MethodRegistration class. This wraps up all of the relevant information about your external method in a way that the transformer can understand in order to call it. You can either choose static or instance methods. Let's register the ones we defined earlier.
var staticRegistration = MethodRegistration.FromStaticMethod(typeof(TransformerMethods), nameof(TransformerMethods.IsNotNull));
var instanceRegistration = MethodRegistration.FromInstanceMethod(nameof(TransformerMethods.ReverseString));
You'll notice that you don't have to provide an instance type for the instance method, just the name of the method itself. This is because the transformer will assume that you sent in an appropriate method context instance when it was created and will just use whatever you provide during method resolution. Include the registrations and an instance for your methods (if needed) when you create the JoltJsonTransformer instance.
var transformer = JoltJsonTransformer.DefaultWith(transformerJson, new[] { staticRegistration, instanceRegistration }, new TransformerMethods());
That's it! You can pass in your source JSON document to the Transform method call just like before and collect the transformed result.
Transformer Registration
As you go, you may run into the issue where the transformer is ultimately too complex to reason about in a single file. In that case, you can break it up into multiple transformers and then call them from each other using the #transformWith method. This method takes a path to the sub-document to transform and the name of the transformer to use, which must be registered with the JoltJsonTransformer instance. Let's see how we can register a transformer and then call it from another one.
Registration is fairly straightforward, you need a name and the transformer JSON that will be used.
var registration = new TransformerRegistration("PartialTransformer", partialTransformerJson);
context.RegisterTransformer(registration);
And then in your primary transformer, on the property value side you can call #transformWith($.some.path, 'PartialTransformer') to execute the registered transformer against the sub-document at $.some.path and return the result as the value of that evaluation.
Variables from the primary transformer will not be available in the referenced transformer, although it can receive a JSON object as parameters through the #transformWith method, which will store the object in an implicitly created variable called @params and allow access to the properties from there. This allows you to create more dynamic and reusable transformers that can be configured at runtime with different parameters. For example, you could have a transformer that formats a date according to a provided format string, and then call it with different format strings as needed. Here's an example using the object literal syntax.
{
"formattedDate": "#transformWith($.date, 'DateFormatter', { 'format': 'MM/dd/yyyy' })"
}
And the referenced transformer could access the format property from the @params variable like this:
{
"date": "#formatDateTime($.some.date, @params.format)"
}
This is definitely recommended for composability and readability as your transforms grow larger and become unwieldy. And don't worry about @params being null when no parameter object is provided, it will default to an empty JSON object. It's also local to the transformer execution and only exists for the lifetime of that execution.
Also, a recommended pattern that's possible using these referenced transformers is to use the #merge method to enrich the results of the referenced transformer with additional properties from the primary transformer. This allows you to keep your transformers focused and reusable while still being able to add context-specific information as needed. For example:
{
"enrichedResult": "#merge(#transformWith($.some.path, 'PartialTransformer'), { 'source': 'local', 'processedAt': #currentDateTimeUtc() })"
}
Here's a final pattern that may prove useful. Last example and we'll move on!
{
"#foreach(@x in $.order.items) into 'lineItems'": [
{
"item": "#transformWith(@x, 'LineItemTransformer', { 'currency': #valueOf($.order.currency) })"
}
]
}
Validating Your Transformer Syntax
Additionally, transformers may prove difficult to test for correctness without actually obtaining a source document and running through it, which can take time and be specific to a source document. There is a way, however, to get a quick sanity check on your syntax and expression shapes and that's through the Validate() method on the JoltTransformer. First, you create the JoltJsonTransformer instance, using the IJsonContext that contains the transformer string you'd like to validate, and then call Validate() and iterate through the issues that come back, if any. This will not execute any evaluations against a source document and so you won't get errors (such as an incorrect variable value) that would appear at runtime, rather it would identify issues (such as syntax misuse) in how you've constructed your transformer. It will also verify that any calls to #transformWith will resolve to registered transformers.
Issues will be returned as instances of Jolt.Structure.ValidationIssue and will contain information about the issue that was found, such as the type of issue, a message describing the issue, and the path to the part of the transformer where the issue was found. This can be a great way to catch mistakes early on in development before you even have a source document to test against. The issue contract is below:
public sealed class ValidationIssue
{
public ValidationIssueType Type { get; }
public ExceptionCode Code { get; }
public string Message { get; }
public string TransformerExpressionPath { get; }
public string ExpressionText { get; }
public bool IsInPropertyName { get; }
public ExpressionSpan? Span { get; }
}
The TransformerExpressionPath is the path to the property (or array element) containing the expression with the issue. A property's name and value share the same path, so IsInPropertyName indicates whether the issue is within the expression in the property's name rather than its value. When known, Span gives the location of the issue within that expression as a Start index and Length: the method name (e.g. #valueOf) for an issue with a method call, the variable (e.g. @x) for an undeclared variable, the quoted name for an unregistered transformer, and the token where parsing failed for a syntax error. A syntax error found at the end of an expression, such as a missing closing parenthesis, has an empty span at the end.
Streaming Large Amounts Of Documents
The JoltTransformer class that the JsonJoltTransformer types inherit from allows several different ways of interacting with sequences of source data beyond the default string input which assumes that it contains only a single object.
First, when you have a series of JSON objects, condensed so that each line has a single complete object such as in NDJSON / JSON Lines, you may find the following methods useful:
| Method | Description |
|---|---|
| TransformLines(TextReader reader, TextWriter writer, StreamingOptions? options) | Reads a single line at a time from the reader, transforms each, and writes the output to the provided writer. |
| TransformLines(Stream input, Stream output, StreamingOptions? options) | Reads a single line at a time from the given stream, transforms each, and writes the output to the provided stream. |
| TransformLinesAsync(TextReader reader, TextWriter writer, CancellationToken? cancellationToken, StreamingOptions? options) | Asynchronously reads a single line at a time from the reader, transforms each, and asynchronously writes the output to the provided writer. |
| TransformLinesAsync(Stream input, Stream output, CancellationToken? cancellationToken, StreamingOptions? options) | Asynchronously reads a single line at a time from the given stream, transforms each, and asynchronously writes the output to the provided stream. |
And that's great, but what about when you have a different format than just a single JSON object per line? That's where the following methods come in:
| Method | Description |
|---|---|
| TransformSequence(TextReader reader, TextWriter writer, StreamingOptions? options) | Reads a single JSON object at a time from the provided reader, transforms each, and writes the output to the provided writer. |
| TransformSequence(Stream input, Stream output, StreamingOptions? options) | Reads a single JSON object at a time from the provided reader, transforms each, and writes the output to the provided writer. |
| TransformSequenceAsync(TextReader reader, TextWriter writer, CancellationToken? cancellationToken, StreamingOptions? options) | Asynchronously reads a single JSON object at a time from the provided reader, transforms each, and asynchronously writes the output to the provided writer. |
| TransformSequenceAsync(Stream input, Stream output, CancellationToken? cancellationToken, StreamingOptions? options) | Asynchronously reads a single JSON object at a time from the provided reader, transforms each, and asynchronously writes the output to the provided writer. |
The TransformSequence methods will try to automatically determine the format you're using. They support, in order: RFC7464 JSON Sequences (JSON objects separated by a 0x1E Record Separator byte), JSON objects separated by newlines/whitespace (prettified or not), or a JSON array of JSON objects. The last one will degrade to reading the entire array into memory before processing, so using one of the previous methods that are better supported by streaming would generally be more ideal to preserve application memory.
By default, the output will use the same output format and delimiter as the incoming stream uses. For more control, though, see the StreamingOptions class which allows you to choose how the output format is written (e.g. single line per object, multi-line, and others) and which delimiter to use (e.g. RFC7464, newline, and so on).
Verifying Performance
As a part of the JoltOptions instance passed into the transformer when creating it, you can call the WithExecutionTracing method on it to enable performance measuring. Each transformer method call will be timed and checkpointed, giving you an opportunity to monitor how performant your transformation operations are.
Performance results will be available after a given transformer run as a part of the IMessageProvider instance that's accessible through the JoltContext instance, specifically through the ExecutionTraces property. Every trace entry will be in this collection, no matter where or when it occurred to give a complete overview of all trace operations, but you can also drill into a given trace entry using the ChildEntries property and obtain a tree-style view instead. The overall results will be ordered by the time that an entry's scope (or root-level enclosing scope) started and go from there.
Speaking of scopes, you should know that each entry has its own scope, which marks when that particular trace entry started, any checkpoints that occurred within it, and when it completed.
public sealed class ExecutionTraceEntry
{
public string TransformerName { get; }
public DateTimeOffset Timestamp { get; }
public long ExecutionStartedAtTicks { get; }
public TimeSpan ExecutionTimeElapsed { get; }
public ExecutionTraceCheckpoint[] Checkpoints { get; }
public ExecutionTraceEntry[] ChildEntries { get; }
public string Message { get; }
}
public sealed class ExecutionTraceCheckpoint
{
public string Name { get; }
public TimeSpan TimeSinceLastCheckpoint { get; }
public long TicksSinceScopeOpened { get; }
public string[] InputValues { get; }
public string? OutputValue { get; }
}
Calling ToString on a given ExecutionTraceEntry instance will provide a more friendly and formatted overview of the data inside it and all of its child entries, if any. For example, using the timing output of one of the tests, you'll notice that the outer scope calling into a partial transformer had a child scope opened within it (which is indented in the output string):
6/21/2026 5:47:04 PM +00:00 [Transformer: PartialTransformer] Scope completed
[Checkpoint: Invoking method 'Transform'] at 00:00:00.0004170
[Checkpoint: Method input '$.subPath,PartialTransformer,null,Jolt.Evaluation.EvaluationContext'] at 00:00:00.0001476
[Checkpoint: Method invocation completed with '{"sourceVariableX":null,"sourcePathValue":"test.source.path","integerValue":null,"textValue":null}'] at 00:00:04.1448826
6/21/2026 5:46:59 PM +00:00 [Transformer: PartialTransformer] Scope completed
[Checkpoint: Invoking method 'ValueOf'] at 00:00:00.0115538
[Checkpoint: Method input '$.sourcePath,Jolt.Evaluation.EvaluationContext'] at 00:00:00.0000006
[Checkpoint: Method invocation completed with 'test.source.path'] at 00:00:00.0006045
How To Contribute To This Project
If you happen to have the desire to help out with this (and I would love it if you do), there are several ways to do so that I'd recommend.
Raising Issues
You are more than welcome to raise an issue when you think you've found a bug, a mismatch between documentation and behavior, an idea for a new feature or library method that you'd like to see, or even just a question on intended behavior if it doesn't meet your expectations (there's always room to improve).
Submitting Pull Requests
Please raise an issue prior to submitting a PR so we can discuss and arrive at an ideal solution that you're then encouraged to help out with, namely so that you don't end up spending your time on fixing or adding something that may not turn out to be needed or intended in the end. I look forward to hearing from you!
Writing Tests
Testing is crucial and also the thing that tends to be lacking in this project. Please refer to the documentation in this README, the ones in the Tests folder, and also the Jolt Wiki to identify test cases that are missing or incorrect, and then raise an issue (as per above) with your concerns and findings so that we can discuss and decide how to move forward.
| Product | Versions Compatible and additional computed target framework versions. |
|---|---|
| .NET | net5.0 was computed. net5.0-windows was computed. net6.0 was computed. net6.0-android was computed. net6.0-ios was computed. net6.0-maccatalyst was computed. net6.0-macos was computed. net6.0-tvos was computed. net6.0-windows was computed. net7.0 was computed. net7.0-android was computed. net7.0-ios was computed. net7.0-maccatalyst was computed. net7.0-macos was computed. net7.0-tvos was computed. net7.0-windows was computed. net8.0 is compatible. net8.0-android was computed. net8.0-browser was computed. net8.0-ios was computed. net8.0-maccatalyst was computed. net8.0-macos was computed. net8.0-tvos was computed. net8.0-windows was computed. net9.0 was computed. net9.0-android was computed. net9.0-browser was computed. net9.0-ios was computed. net9.0-maccatalyst was computed. net9.0-macos was computed. net9.0-tvos was computed. net9.0-windows was computed. net10.0 is compatible. net10.0-android was computed. net10.0-browser was computed. net10.0-ios was computed. net10.0-maccatalyst was computed. net10.0-macos was computed. net10.0-tvos was computed. net10.0-windows was computed. |
| .NET Core | netcoreapp2.0 was computed. netcoreapp2.1 was computed. netcoreapp2.2 was computed. netcoreapp3.0 was computed. netcoreapp3.1 was computed. |
| .NET Standard | netstandard2.0 is compatible. netstandard2.1 is compatible. |
| .NET Framework | net461 was computed. net462 was computed. net463 was computed. net47 was computed. net471 was computed. net472 was computed. net48 was computed. net481 was computed. |
| MonoAndroid | monoandroid was computed. |
| MonoMac | monomac was computed. |
| MonoTouch | monotouch was computed. |
| Tizen | tizen40 was computed. tizen60 was computed. |
| Xamarin.iOS | xamarinios was computed. |
| Xamarin.Mac | xamarinmac was computed. |
| Xamarin.TVOS | xamarintvos was computed. |
| Xamarin.WatchOS | xamarinwatchos was computed. |
-
.NETStandard 2.0
- Microsoft.Extensions.Logging (>= 10.0.7)
- System.Collections.Immutable (>= 10.0.8)
-
.NETStandard 2.1
- Microsoft.Extensions.Logging (>= 10.0.7)
- System.Collections.Immutable (>= 10.0.8)
-
net10.0
- Microsoft.Extensions.Logging (>= 10.0.7)
-
net8.0
- Microsoft.Extensions.Logging (>= 10.0.7)
- System.Collections.Immutable (>= 10.0.8)
NuGet packages (2)
Showing the top 2 NuGet packages that depend on Jolt:
| Package | Downloads |
|---|---|
|
Jolt.Json.DotNet
A friendly way of transforming JSON into different JSON. |
|
|
Jolt.Json.Newtonsoft
A friendly way of transforming JSON into different JSON. |
GitHub repositories
This package is not used by any popular GitHub repositories.
| Version | Downloads | Last Updated |
|---|---|---|
| 2.5.0 | 109 | 10/20/2026 |
| 2.4.3 | 164 | 10/5/2026 |
| 2.4.2 | 178 | 9/9/2026 |
| 2.4.1 | 162 | 9/8/2026 |
| 2.3.1 | 179 | 8/8/2026 |
| 2.3.0 | 190 | 7/21/2026 |
| 2.2.0 | 188 | 7/12/2026 |
| 2.1.0 | 411 | 6/14/2026 |
| 2.0.0 | 193 | 5/23/2026 |
| 1.6.1 | 203 | 5/10/2026 |
| 1.6.0 | 195 | 3/3/2026 |
| 1.5.1 | 178 | 3/1/2026 |
| 1.5.0 | 214 | 2/21/2026 |
| 1.4.9 | 192 | 2/16/2026 |
| 1.4.8 | 193 | 2/15/2026 |
| 1.4.7 | 192 | 2/10/2026 |
| 1.4.6 | 187 | 2/9/2026 |
| 1.4.4 | 355 | 12/21/2025 |
| 1.4.3 | 255 | 12/12/2025 |