Query XML Using JSONata
Updated: May 13, 2026
This example shows how to read an XML document (or any other supported format) and query or transform it using JSONata expressions with DataPipeline.
Input XML File
<?xml version='1.0' encoding='UTF-8'?>
<document>
<record>
<id>0001</id>
<type>donut</type>
<name>Cake</name>
<ppu>0.55</ppu>
<batters>
<batter>
<id>1001</id>
<type>Regular</type>
</batter>
<batter>
<id>1002</id>
<type>Chocolate</type>
</batter>
<batter>
<id>1003</id>
<type>Blueberry</type>
</batter>
<batter>
<id>1004</id>
<type>Devil's Food</type>
</batter>
</batters>
<topping>
<id>5001</id>
<type>None</type>
</topping>
<topping>
<id>5002</id>
<type>Glazed</type>
</topping>
<topping>
<id>5005</id>
<type>Sugar</type>
</topping>
<topping>
<id>5007</id>
<type>Powdered Sugar</type>
</topping>
<topping>
<id>5006</id>
<type>Chocolate with Sprinkles</type>
</topping>
<topping>
<id>5003</id>
<type>Chocolate</type>
</topping>
<topping>
<id>5004</id>
<type>Maple</type>
</topping>
</record>
<record>
<id>0002</id>
<type>donut</type>
<name>Raised</name>
<ppu>0.55</ppu>
<batters>
<batter>
<id>1001</id>
<type>Regular</type>
</batter>
</batters>
<topping>
<id>5001</id>
<type>None</type>
</topping>
<topping>
<id>5002</id>
<type>Glazed</type>
</topping>
<topping>
<id>5005</id>
<type>Sugar</type>
</topping>
<topping>
<id>5003</id>
<type>Chocolate</type>
</topping>
<topping>
<id>5004</id>
<type>Maple</type>
</topping>
</record>
</document>
Java code listing
package com.northconcepts.datapipeline.examples.jsonata;
import com.northconcepts.datapipeline.core.Record;
import com.northconcepts.datapipeline.core.ValueNode;
import java.io.FileReader;
import static com.northconcepts.datapipeline.jsonata.Jsonata.jsonata;
public class QueryXmlUsingJSONata {
public static void main(String[] args) throws Throwable {
try (FileReader fileReader = new FileReader("data/input/nested-data.xml")) {
ValueNode> xmlRoot = Record.fromXml(fileReader);
printSection("Basic Selection");
runQuery("Query 1 - Donut Names", "document.record.name", xmlRoot);
runQuery("Query 2 - Batter Types", "document.record.batters.batter.type", xmlRoot);
printSection("Filtering");
runQuery("Query 3 - Donut Named Raised", "document.record[name='Raised']", xmlRoot);
runQuery("Query 4 - Donut with Chocolate with Sprinkles Topping", "document.record[topping[type='Chocolate with Sprinkles']].name", xmlRoot);
printSection("Reshaping");
runQuery("Query 5 - Name With Batter And Topping Counts",
"document.record.{'name': name, 'batterCount': $count(batters.batter), 'toppingCount': $count(topping)}",
xmlRoot);
runQuery("Query 6 - Donut Summary",
"document.record.{'id': id, 'name': name, 'pricePerUnit': ppu}",
xmlRoot);
printSection("Metrics");
runQuery("Query 7 - Total Donuts", "$count(document.record)", xmlRoot);
runQuery("Query 8 - Average Price Per Unit", "$average(document.record.ppu.$number())", xmlRoot);
}
}
private static void runQuery(String title, String expression, ValueNode> data) {
System.out.println(title);
System.out.println("Expression: " + expression);
System.out.println("Result: ");
System.out.println(jsonata(expression).evaluate(data));
}
private static void printSection(String title) {
System.out.println("==============================================================");
System.out.println(title);
}
}
Code walkthrough
- Read XML into memory
Record.fromXml(fileReader)parses the file and returns a ValueNode tree. All queries run against that tree.
- Execute JSONata expressions
runQueryprints the query name, the expression, and the evaluated result fromjsonata(expression).evaluate(data)
- Query patterns used
- Basic Selection: These expressions navigate fields and nested arrays.
- document.record.name
- document.record.batters.batter.type
- Basic Selection: These expressions navigate fields and nested arrays.
-
- Filtering: These expressions keep only records that match conditions.
- document.record[name='Raised']
- document.record[topping[type='Chocolate with Sprinkles']].name
- Filtering: These expressions keep only records that match conditions.
-
- Reshaping: These expressions build new objects with only the fields you want.
- document.record.{'name': name, 'batterCount': $count(batters.batter), 'toppingCount': $count(topping)}
- document.record.{'id': id, 'name': name, 'pricePerUnit': ppu}
- Reshaping: These expressions build new objects with only the fields you want.
-
- Metrics: These expressions compute aggregate values.
- $count(document.record)
- $average(document.record.ppu.$number())
- Metrics: These expressions compute aggregate values.
Console Output
==============================================================
Basic Selection
Query 1 - Donut Names
Expression: document.record.name
Result:
[Cake, Raised]
Query 2 - Batter Types
Expression: document.record.batters.batter.type
Result:
[Regular, Chocolate, Blueberry, Devil's Food, Regular]
==============================================================
Filtering
Query 3 - Donut Named Raised
Expression: document.record[name='Raised']
Result:
Record (MODIFIED) (is child record) (has child records) {
0:[id]:STRING=[0002]:String
1:[type]:STRING=[donut]:String
2:[name]:STRING=[Raised]:String
3:[ppu]:STRING=[0.55]:String
4:[batters]:RECORD=[
Record (MODIFIED) (is child record) (has child records) {
0:[batter]:RECORD=[
Record (MODIFIED) (is child record) {
0:[id]:STRING=[1001]:String
1:[type]:STRING=[Regular]:String
}]:Record
}]:Record
5:[topping]:ARRAY of RECORD=[[
Record (MODIFIED) (is child record) {
0:[id]:STRING=[5001]:String
1:[type]:STRING=[None]:String
},
Record (MODIFIED) (is child record) {
0:[id]:STRING=[5002]:String
1:[type]:STRING=[Glazed]:String
},
Record (MODIFIED) (is child record) {
0:[id]:STRING=[5005]:String
1:[type]:STRING=[Sugar]:String
},
Record (MODIFIED) (is child record) {
0:[id]:STRING=[5003]:String
1:[type]:STRING=[Chocolate]:String
},
Record (MODIFIED) (is child record) {
0:[id]:STRING=[5004]:String
1:[type]:STRING=[Maple]:String
}]]:ArrayValue
}
Query 4 - Donut with Chocolate with Sprinkles Topping
Expression: document.record[topping[type='Chocolate with Sprinkles']].name
Result:
Cake
==============================================================
Reshaping
Query 5 - Name With Batter And Topping Counts
Expression: document.record.{'name': name, 'batterCount': $count(batters.batter), 'toppingCount': $count(topping)}
Result:
[Record (MODIFIED) (is child record) {
0:[name]:STRING=[Cake]:String
1:[batterCount]:INT=[4]:Integer
2:[toppingCount]:INT=[7]:Integer
}
, Record (MODIFIED) (is child record) {
0:[name]:STRING=[Raised]:String
1:[batterCount]:INT=[1]:Integer
2:[toppingCount]:INT=[5]:Integer
}
]
Query 6 - Donut Summary
Expression: document.record.{'id': id, 'name': name, 'pricePerUnit': ppu}
Result:
[Record (MODIFIED) (is child record) {
0:[id]:STRING=[0001]:String
1:[name]:STRING=[Cake]:String
2:[pricePerUnit]:STRING=[0.55]:String
}
, Record (MODIFIED) (is child record) {
0:[id]:STRING=[0002]:String
1:[name]:STRING=[Raised]:String
2:[pricePerUnit]:STRING=[0.55]:String
}
]
==============================================================
Metrics
Query 7 - Total Donuts
Expression: $count(document.record)
Result:
2
Query 8 - Average Price Per Unit
Expression: $average(document.record.ppu.$number())
Result:
0.55
