JSLT

repository·master·Indexed 18 days ago

https://github.com/schibsted/jslt

A complete query and transformation language for JSON inspired by jq, XPath, and XQuery. JSLT allows developers to extract values, filter JSON objects, and transform between different JSON formats. It provides a Java API, a command-line interface, and a web-based Playground for testing transformations. Key features include support for custom functions, array sorting via the sort() macro, and an experimental module containing the exp:group-by macro.

Tokens
14.6K
Snippets
57
Records
63
Agent score
66%

What's inside jslt

  1. Compare `filter` macro vs `FOR` expression filtering

    master

    JSLT allows filtering via a filter macro or directly within a FOR expression. While the filter macro is functional, the FOR expression syntax is generally preferred for readability when performing transformations and filtering in a single pass.

    Using the filter macro: [for (filter(.path.to.array, .condition)) .output.path]

    Using FOR expression filtering: [for (.path.to.array) .output.path if (.condition)]

    // Using filter macro
    [for (filter(.Reservations.Instances, .EbsOptimized)) .InstanceId]
    
    // Using FOR expression (preferred)
    [for (.Reservations.Instances) .InstanceId if (.EbsOptimized)]
  2. Define and use functions in JSLT

    master

    You can define custom functions in JSLT using the def keyword. A function consists of a name, a list of parameters, and an expression. When the function is called, the expression is evaluated with the provided parameter values bound to the parameter names.

    Syntax:

    def name(param1, param2)
      expression

    Functions enable recursive traversal of JSON structures and allow for complex computations that would otherwise be difficult to express in pure JSLT.

    def name(param1, param2)
      expression
  3. Proposed Java Query API for JSLT

    master

    The current JSLT API primarily provides apply(JsonNode) -> JsonNode, which requires manual type conversion from the resulting JsonNode. To improve Java integration, the project is considering extending the Expression interface with type-specific methods. These methods allow you to directly extract values as standard Java types from a JsonNode input.

    // Proposed methods on the Expression interface
    public String queryString(JsonNode input);
    public Collection<String> queryStrings(JsonNode input);
    public boolean queryBoolean(JsonNode input);
    public int queryInt(JsonNode input);
    public Collection<Integer> queryInts(JsonNode input);
    public long queryLong(JsonNode input);
    public Collection<Long> queryLongs(JsonNode input);
  4. Accessing object properties on arrays using dot notation

    master

    In JSLT, applying a dot selector (e.g., .foo) to an array of objects allows you to index into every object within that array. This operation performs a map-like transformation: it traverses each element in the array, retrieves the specified property, and returns a flattened array of the resulting values. This is particularly useful for extracting specific fields from nested structures where arrays are present at multiple levels.

    // Example: Extracting PrivateIpAddress from a nested AWS EC2 structure
    .Reservations.Instances.PrivateIpAddress
  5. Concept: The `..` operator for recursive descent

    master

    The .. operator is a proposed feature for JSLT that allows for recursive descent through a JSON structure. It functions similarly to the . operator, but instead of accessing a specific key at the current level, it traverses all sub-objects and arrays to find all occurrences of a specific key.

    Use Case

    It is designed to simplify extracting properties from deeply nested lists without using nested for loops. For example, when extracting IP addresses from a nested AWS EC2 response, instead of writing nested loops that result in nested arrays and null values, you can use the recursive operator to get a flat list of all matches.

    Proposed Rules

    • Can appear anywhere the . operator is currently valid.
    • Returns a list of all matches found during recursion.
    • For objects: If the specified key exists, its value is added to the matches. The operator then traverses all values that are objects or arrays.
    • For arrays: The operator traverses all elements within the array.
    // Current way to extract nested properties (results in nested arrays):
    for (.Reservations)
      for (.Instances)
        .PrivateIpAddress
    
    // Proposed way using the .. operator (results in a flat list):
    ..PrivateIpAddress
  6. Import other JSLT files into your transform

    master

    The import statement allows you to modularize JSLT transforms by making variables and functions defined in another file available in your current scope. This is useful for sharing logic across multiple transform files.

    Simple Import

    When using a simple import, all variables and functions from the imported file are brought directly into the local scope. Note that the top-level transform expression of the imported file is not accessible via this method.

    import "reference/to/other/file.jstl"

    Import with Prefix (Namespacing)

    You can use the as keyword to provide a prefix for the imported content. This prevents name collisions and provides a structured way to access the imported logic:

    • Functions: Accessed as prefix:functionName.
    • Variables: Accessed as $prefix:variableName.
    • The Transform itself: The top-level expression of the imported file becomes a function available as prefix(input).

    When using a prefix, any items imported into the target file by the original file remain invisible to the current scope.

    import "reference/to/other/file.jstl" as foo
    import "transforms/pulse-identified.jstl2" as identified
    import "transforms/pulse-anonymized.jstl2" as anonymized
    
    if (.actor."spt:userId" and not(ends-with(":null", .actor."spt:userId")))
      identified(.)
    else
      anonymized(.)
  7. Perform object matching with the * operator

    master

    Object matching allows you to specify certain keys to override or insert, while copying all other existing keys from the input object using the * operator.

    • * : . matches all keys not explicitly defined and copies them as-is.
    • You can exclude specific keys from the match using the - syntax: * - key1, key2 : ..
    • Matching can be used inside nested objects.

    Example: To multiply foo by 10 but keep all other keys:

    {
      "foo" : .foo * 10,
      * : .
    }
    // Match all keys and multiply all values by 10
    { * : . * 10 }
    
    // Match all keys except 'bar' and 'baz'
    { "foo" : .foo * 10, * - bar, baz : . }
  8. Use the pipe operator |

    master

    The pipe operator | changes the context node (.) for the expression on its right side. The expression on the left becomes the new context.

    This is useful for shortening long paths or chaining transformations.

    Example: Given input {"a": {"b":1,"c":2,"d":3}}:

    • .a | [.b, .c, .d] evaluates to [1, 2, 3]
    • This is equivalent to [.a.b, .a.c, .a.d]
    // Chaining pipes
    1 | [.,.] | {"a": ., "b": .}
    // Result: {"a": [1,1], "b": [1,1]}
  9. Implement efficient aggregates using the reduce macro

    master

    Instead of implementing aggregate functions (like sum or average) using recursion—which is inefficient because it creates $n$ sub-arrays for an array of $n$ elements—you can use the reduce macro.

    reduce(sequence, reduce expression) reduces a sequence to a single value by applying the reduce expression to pairs of values. Within the reduce expression, the two values are accessible via the variables $left and $right. If the sequence is empty, the result is null.

    // Example implementation of sum using reduce
    def sum(numbers)
      reduce($numbers, $left + $right)
    
    // Example implementation of average
    def average(numbers)
      sum($numbers) / size($numbers)
    
    // Example implementation of any (logical OR)
    def any(booleans)
      reduce($booleans, $left or $right)
    
    // Example implementation of all (logical AND)
    def all(booleans)
      reduce($booleans, $left and $right)
  10. Understand value ordering and type precedence in JSLT

    master

    When sorting, JSLT follows specific rules for comparing different types.

    Type Precedence

    If comparing different types, the order from smallest to largest is:

    1. null
    2. Booleans
    3. Numbers
    4. Strings
    5. Arrays
    6. Objects

    Type-Specific Ordering

    • Numbers: Natural numeric order.
    • Strings: Unicode code point order (no custom language collations supported; use custom functions to generate sort keys for specific collation needs).
    • Booleans: false sorts before true.
    • Arrays: Sorted element-by-element starting from the first index. If an array lacks an element at the current index, it is considered smaller. If elements are equal, comparison moves to the next index.
    • Objects: Compared by size first, then by the smallest key, then by the value of that smallest key. If keys are equal, comparison moves to the next key.
  11. Proposed Jslt convenience class

    master

    To simplify the workflow of compiling a query and then applying it, a proposed Jslt utility class would allow executing queries in a single step. This approach avoids the manual two-step process of compile(query) followed by apply(input). To mitigate the performance overhead of repeated compilations, this class would ideally implement an internal cache for compiled expressions.

    // Proposed Jslt class methods
    public JsonNode apply(String query, JsonNode input);
    public String queryString(String query, JsonNode input);
    // ...