Data Sources & Query Executers

This content was generated with the assistance of AI and should be verified against the official documentation before being relied on in production.

The reference material consulted while preparing these pages is listed in the section bibliography.

Baseline date: 2026-10-09. A JasperReports template describes how rows look; it does not know where they come from. At fill time the engine needs exactly one thing for each dataset: a JRDataSource, an object that hands over one row at a time. Everything on this page is a way to obtain that object, either by building it in Java and passing it to fillReport, or by writing a query in the template and letting a query executer build it. The page walks through the data sources that ship with JasperReports 7.0.8 and the two extension points for writing your own, using the monthly sales report of the Bookshelf Billing service as the running example.

All examples were compiled and run with Java 21 source level, JasperReports 7.0.8, H2 2.4.240 as the database, and (for the JPA example) Hibernate ORM 7.4.12 with Jakarta Persistence 3.2.0. For every example the generated PDF was opened and its text compared with the input rows. What was not run is stated where it comes up. This page covers where the data comes from; the template language itself is in JRXML Report Design, the surrounding lifecycle in JasperReports Architecture & Lifecycle, and how a report is wired into a Spring Boot service in Generating Reports from Databases & Services.

Two ways to feed a report

The figure shows the two paths. In the first, application code creates a JRDataSource and passes it to JasperFillManager.fillReport(report, parameters, dataSource); the template has no <query> element. In the second, the template declares <query language="...">, the engine looks up the query executer factory registered for that language, and the factory’s executer runs the query text and returns a JRDataSource. JDBC is the best known case: the sql language is a query executer whose factory expects a java.sql.Connection in the REPORT_CONNECTION parameter.

Class diagram of JRDataSource, JRRewindableDataSource and their implementations, and of QueryExecuterFactory, JRQueryExecuter and a custom executer

JRDataSource has two methods. next() moves to the next row and returns false at the end, and getFieldValue(JRField) returns the value of one field of the current row, identified by the field’s name (and its description, which several data sources use as a mapping hint). JRRewindableDataSource adds moveFirst(), which restarts the iteration. Whether a data source is rewindable matters when the same instance is read more than once, for example by two successive fills. In 7.0.8 the bean, map, XML, JSON, JSONQL, Excel, JPA and empty data sources are rewindable; JRResultSetDataSource (the one created for JDBC queries) and JRCsvDataSource are not, which was checked by reflection on the 7.0.8 jars.

Maven dependencies

The first example needs the engine, the PDF exporter, the fonts extension and a compiler for the report’s Java expressions. The other examples add one artifact each, listed where they are introduced. Spring Boot setup in a real service is in Getting Started with Spring Boot.

<dependency>
  <groupId>net.sf.jasperreports</groupId>
  <artifactId>jasperreports</artifactId>
  <version>7.0.8</version>
</dependency>
<dependency>
  <groupId>net.sf.jasperreports</groupId>
  <artifactId>jasperreports-pdf</artifactId>
  <version>7.0.8</version>
</dependency>
<dependency>
  <groupId>net.sf.jasperreports</groupId>
  <artifactId>jasperreports-fonts</artifactId>
  <version>7.0.8</version>
</dependency>
<dependency>
  <groupId>net.sf.jasperreports</groupId>
  <artifactId>jasperreports-jdt</artifactId>
  <version>7.0.8</version>
</dependency>

A shared template and helper

Most examples fill the same flat template: four fields (category, title, quantity, amount) and a detail band. It is written in the JasperReports 7 JRXML format (namespace-free elements with a kind attribute); templates written for 6.x do not load in 7.0, and Jaspersoft Studio 7 converts them. Save it as src/main/resources/reports/sales.jrxml. Field names are the contract between template and data source: every data source below resolves a field by name, although each one uses the name differently (a getter, a map key, a column header, a JSON property).

sales.jrxml
<jasperReport name="sales" language="java" pageWidth="595" pageHeight="842"
              columnWidth="515" leftMargin="40" rightMargin="40" topMargin="40" bottomMargin="40">
  <field name="category" class="java.lang.String"/>
  <field name="title" class="java.lang.String"/>
  <field name="quantity" class="java.lang.Integer"/>
  <field name="amount" class="java.math.BigDecimal"/>
  <title height="30">
    <element kind="staticText" x="0" y="0" width="515" height="26" fontSize="16" bold="true">
      <text><![CDATA[Bookshelf sales]]></text>
    </element>
  </title>
  <detail>
    <band height="20">
      <element kind="textField" x="0" y="0" width="120" height="18">
        <expression><![CDATA[$F{category}]]></expression>
      </element>
      <element kind="textField" x="120" y="0" width="230" height="18">
        <expression><![CDATA[$F{title}]]></expression>
      </element>
      <element kind="textField" x="350" y="0" width="50" height="18" hTextAlign="Right">
        <expression><![CDATA[$F{quantity}]]></expression>
      </element>
      <element kind="textField" x="400" y="0" width="115" height="18" hTextAlign="Right" pattern="#,##0.00">
        <expression><![CDATA[$F{amount}]]></expression>
      </element>
    </band>
  </detail>
</jasperReport>

The helper below compiles a template from the classpath and writes the filled report to a PDF. Compiling at run time is fine for examples; production code compiles once at build time or at start-up and reuses the JasperReport.

Support.java
package bookshelf;

import java.io.InputStream;
import java.nio.file.Path;

import net.sf.jasperreports.engine.JRException;
import net.sf.jasperreports.engine.JasperCompileManager;
import net.sf.jasperreports.engine.JasperExportManager;
import net.sf.jasperreports.engine.JasperPrint;
import net.sf.jasperreports.engine.JasperReport;

final class Support {

    private Support() {
    }

    static JasperReport compile(String classpathResource) throws JRException {
        try (InputStream in = Support.class.getResourceAsStream(classpathResource)) {
            return JasperCompileManager.compileReport(in);
        } catch (java.io.IOException e) {
            throw new JRException(e);
        }
    }

    static void toPdf(JasperPrint print, String fileName) throws JRException {
        Path out = Path.of("target", fileName);
        JasperExportManager.exportReportToPdfFile(print, out.toString());
        System.out.println(fileName + ": " + print.getPages().size() + " page(s)");
    }
}

Both pieces derive from the JasperReports data source sample.

Beans and maps

JRBeanCollectionDataSource wraps a Collection of objects and reads each field through a bean getter: the field category calls getCategory(). The object must therefore be a classic JavaBean. A Java record does not work: its accessor is category(), not getCategory(), and the fill fails with an exception that reports an unknown property category on the record class (checked on 7.0.8). Use a small class with getters, as below, or a map.

JRMapCollectionDataSource reads rows from Map instances instead, with the field name as the key. It needs no bean class, which suits rows assembled from several sources or from a JdbcTemplate result. The rows may be immutable maps. The parameters map passed to fillReport must be mutable, however: the engine adds its built-in parameters to it, and a Map.of() or other immutable map is rejected with an UnsupportedOperationException (checked on 7.0.8), so pass a new HashMap<>() even when there are no parameters.

SaleRow.java
package bookshelf;

import java.math.BigDecimal;

/** A bean data source reads properties through getters, so this must be a classic bean. */
public class SaleRow {

    private final String category;
    private final String title;
    private final int quantity;
    private final BigDecimal amount;

    public SaleRow(String category, String title, int quantity, BigDecimal amount) {
        this.category = category;
        this.title = title;
        this.quantity = quantity;
        this.amount = amount;
    }

    public String getCategory() {
        return category;
    }

    public String getTitle() {
        return title;
    }

    public int getQuantity() {
        return quantity;
    }

    public BigDecimal getAmount() {
        return amount;
    }
}
BeanAndMapReport.java
package bookshelf;

import java.math.BigDecimal;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import net.sf.jasperreports.engine.JRDataSource;
import net.sf.jasperreports.engine.JasperFillManager;
import net.sf.jasperreports.engine.JasperPrint;
import net.sf.jasperreports.engine.JasperReport;
import net.sf.jasperreports.engine.data.JRBeanCollectionDataSource;
import net.sf.jasperreports.engine.data.JRMapCollectionDataSource;

public class BeanAndMapReport {

    public static void main(String[] args) throws Exception {
        JasperReport report = Support.compile("/reports/sales.jrxml");

        // 1. Beans: one field name maps to one getter (getCategory -> category).
        List<SaleRow> rows = List.of(
                new SaleRow("Fiction", "The Pragmatic Novel", 3, new BigDecimal("44.97")),
                new SaleRow("Fiction", "Garden of Forking Paths", 1, new BigDecimal("12.50")),
                new SaleRow("Science", "Entropy for Librarians", 2, new BigDecimal("59.00")));
        JRDataSource beans = new JRBeanCollectionDataSource(rows);
        // The parameters map must be mutable: the engine adds built-in parameters to it.
        Map<String, Object> params = new HashMap<>();
        JasperPrint fromBeans = JasperFillManager.fillReport(report, params, beans);
        Support.toPdf(fromBeans, "ds-beans.pdf");

        // 2. Maps: the field name is the map key, no bean class needed.
        List<Map<String, ?>> maps = List.of(
                Map.of("category", "Fiction", "title", "The Pragmatic Novel",
                        "quantity", 3, "amount", new BigDecimal("44.97")),
                Map.of("category", "Science", "title", "Entropy for Librarians",
                        "quantity", 2, "amount", new BigDecimal("59.00")));
        JasperPrint fromMaps = JasperFillManager.fillReport(
                report, new HashMap<>(), new JRMapCollectionDataSource(maps));
        Support.toPdf(fromMaps, "ds-maps.pdf");
    }
}

JRBeanArrayDataSource is the same as the collection variant for an array, and the collection variant has a second constructor argument that controls whether the field description is used as the property path in place of the field name (see the Javadoc). See the JRBeanCollectionDataSource and JRMapCollectionDataSource Javadoc and the data source sample these examples derive from.

For rows that come from Spring Data repositories, the usual pattern is to map projections to beans or maps and wrap them here; the streaming variant, which keeps memory flat for large result sets, is the custom data source later on this page, and the surrounding Spring code is on Generating Reports from Databases & Services.

JDBC and the sql query language

When the data lives in a relational database, the shortest route is to put the query in the template and give the engine a Connection. The template declares a parameter, and the query uses it with the usual $P{MIN_AMOUNT} syntax. Because it is used in a position where a value is expected, the engine turns it into a JDBC bind parameter rather than pasting text into the SQL; to paste text (a table name or a whole WHERE fragment) the syntax is $P!{NAME}, which is the one place where SQL injection is possible and must never take user input.

The sales-jdbc.jrxml template is sales.jrxml with these lines added before the fields:

sales-jdbc.jrxml (beginning)
<jasperReport name="sales" language="java" pageWidth="595" pageHeight="842"
              columnWidth="515" leftMargin="40" rightMargin="40" topMargin="40" bottomMargin="40">
  <parameter name="MIN_AMOUNT" class="java.math.BigDecimal"/>
  <query language="sql"><![CDATA[
    SELECT category, title, quantity, amount
    FROM sale
    WHERE amount >= $P{MIN_AMOUNT}
    ORDER BY category, title
  ]]></query>
  <field name="category" class="java.lang.String"/>

And the Java side opens an in-memory H2 database, creates the table and fills the report. Add the H2 driver (com.h2database:h2:2.4.240) to follow the example; a PostgreSQL service uses the PostgreSQL driver and a pooled DataSource, with the URL, user and password read from the environment or the Spring configuration, never hard-coded.

JdbcReport.java
package bookshelf;

import java.math.BigDecimal;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.Statement;
import java.util.HashMap;
import java.util.Map;

import net.sf.jasperreports.engine.JasperFillManager;
import net.sf.jasperreports.engine.JasperPrint;
import net.sf.jasperreports.engine.JasperReport;

public class JdbcReport {

    public static void main(String[] args) throws Exception {
        JasperReport report = Support.compile("/reports/sales-jdbc.jrxml");
        // Production code reads the URL, user and password from the environment or a DataSource bean.
        try (Connection connection = DriverManager.getConnection("jdbc:h2:mem:bookshelf", "sa", "");
                Statement st = connection.createStatement()) {
            st.execute("CREATE TABLE sale (category VARCHAR(40), title VARCHAR(80), quantity INT, amount DECIMAL(10,2))");
            st.execute("INSERT INTO sale VALUES ('Fiction','The Pragmatic Novel',3,44.97),"
                    + "('Fiction','Garden of Forking Paths',1,12.50),('Science','Entropy for Librarians',2,59.00)");

            Map<String, Object> params = new HashMap<>();
            params.put("MIN_AMOUNT", new BigDecimal("20.00"));
            // The Connection goes in as a built-in parameter; the engine runs the query in the template.
            JasperPrint print = JasperFillManager.fillReport(report, params, connection);
            Support.toPdf(print, "ds-jdbc.pdf");
        }
    }
}

The connection is not closed by JasperReports, so the caller keeps its normal try-with-resources or pool handling. Query parameters and clauses are described in the query sample and the configuration reference, which this example derives from. The engine also accepts a JRResultSetDataSource built from a ResultSet you opened yourself, for cases where the SQL is built in Java; that class is the non-rewindable one in the figure.

JSON: JsonDataSource and JSONQL

JSON is handled by the jasperreports-json module. There are two data sources, and both read the JSON from an InputStream, typically the body of an HTTP response:

  • JsonDataSource(inputStream, selectExpression) takes a selection expression that picks the array of rows from the document; here the property sales. Fields map to the property of the same name, or to the path in the field’s description.

  • The jsonql query language does the same from the template. The input arrives in the built-in parameter named by the constant JsonQLQueryExecuterFactory.JSON_INPUT_STREAM, the query text is the selection expression, and each field can carry the property net.sf.jasperreports.jsonql.field.expression to say which part of the row it reads.

The jsonql template differs from sales.jrxml by a query and the field properties:

sales-jsonql.jrxml (beginning)
<jasperReport name="sales" language="java" pageWidth="595" pageHeight="842"
              columnWidth="515" leftMargin="40" rightMargin="40" topMargin="40" bottomMargin="40">
  <query language="jsonql"><![CDATA[sales]]></query>
  <field name="category" class="java.lang.String"><property name="net.sf.jasperreports.jsonql.field.expression" value="category"/></field>
  <field name="title" class="java.lang.String"><property name="net.sf.jasperreports.jsonql.field.expression" value="title"/></field>
  <field name="quantity" class="java.lang.Integer"><property name="net.sf.jasperreports.jsonql.field.expression" value="quantity"/></field>
  <field name="amount" class="java.math.BigDecimal"><property name="net.sf.jasperreports.jsonql.field.expression" value="amount"/></field>

Add net.sf.jasperreports:jasperreports-json:7.0.8. The example below fills the plain template through JsonDataSource and the jsonql template through the query executer; it also runs the XML examples of the next section, so its imports cover both.

JsonAndXmlReport.java
package bookshelf;

import java.io.ByteArrayInputStream;
import java.io.InputStream;
import java.nio.charset.StandardCharsets;
import java.util.HashMap;
import java.util.Map;

import javax.xml.parsers.DocumentBuilderFactory;

import net.sf.jasperreports.engine.JasperFillManager;
import net.sf.jasperreports.engine.JasperPrint;
import net.sf.jasperreports.engine.JasperReport;
import net.sf.jasperreports.engine.data.JRXmlDataSource;
import net.sf.jasperreports.engine.query.JRXPathQueryExecuterFactory;
import net.sf.jasperreports.json.data.JsonDataSource;
import net.sf.jasperreports.json.query.JsonQLQueryExecuterFactory;

public class JsonAndXmlReport {

    static final String JSON = """
            {"sales": [
              {"category": "Fiction", "title": "The Pragmatic Novel", "quantity": 3, "amount": 44.97},
              {"category": "Science", "title": "Entropy for Librarians", "quantity": 2, "amount": 59.00}
            ]}
            """;

    static final String XML = """
            <sales>
              <sale><category>Fiction</category><title>The Pragmatic Novel</title><quantity>3</quantity><amount>44.97</amount></sale>
              <sale><category>Science</category><title>Entropy for Librarians</title><quantity>2</quantity><amount>59.00</amount></sale>
            </sales>
            """;

    private static InputStream stream(String text) {
        return new ByteArrayInputStream(text.getBytes(StandardCharsets.UTF_8));
    }

    public static void main(String[] args) throws Exception {
        // A. JsonDataSource: the second argument is a JSON selection expression that picks the array.
        JasperReport plain = Support.compile("/reports/sales.jrxml");
        JsonDataSource json = new JsonDataSource(stream(JSON), "sales");
        Support.toPdf(JasperFillManager.fillReport(plain, new HashMap<>(), json), "ds-json.pdf");

        // B. The same data through a JSONQL query declared in the template.
        JasperReport jsonql = Support.compile("/reports/sales-jsonql.jrxml");
        Map<String, Object> p = new HashMap<>();
        p.put(JsonQLQueryExecuterFactory.JSON_INPUT_STREAM, stream(JSON));
        Support.toPdf(JasperFillManager.fillReport(jsonql, p), "ds-jsonql.pdf");

        // C. XML: JRXmlDataSource with an XPath selecting the repeating element.
        // Field names resolve through the <description> of each field in the template.
        JRXmlDataSource xml = new JRXmlDataSource(stream(XML), "/sales/sale");
        xml.setDatePattern("yyyy-MM-dd");
        xml.setNumberPattern("#0.00");
        xml.setLocale(java.util.Locale.ROOT);
        JasperReport xpathReport = Support.compile("/reports/sales-xpath.jrxml");
        Support.toPdf(JasperFillManager.fillReport(xpathReport, new HashMap<>(), xml), "ds-xml.pdf");

        // D. The same through an xPath query: the parsed document is a built-in parameter.
        var factory = DocumentBuilderFactory.newInstance();
        factory.setFeature("http://apache.org/xml/features/disallow-doctype-decl", true);
        Map<String, Object> q = new HashMap<>();
        q.put(JRXPathQueryExecuterFactory.PARAMETER_XML_DATA_DOCUMENT,
                factory.newDocumentBuilder().parse(stream(XML)));
        q.put(JRXPathQueryExecuterFactory.XML_NUMBER_PATTERN, "#0.00");
        q.put(JRXPathQueryExecuterFactory.XML_LOCALE, java.util.Locale.ROOT);
        Support.toPdf(JasperFillManager.fillReport(xpathReport, q), "ds-xpath.pdf");
    }
}

The numbers in the JSON (44.97) were converted to the BigDecimal fields without extra configuration. Fetching the JSON from another service is ordinary Spring client code (RestClient, covered on the existing REST APIs page) that hands the response stream to one of these two. The JSON data source sample and the JSONQL data source sample are the official versions of these two paths, and JsonDataSource is the Javadoc.

XML and XPath

XML mirrors the JSON case. JRXmlDataSource(inputStream, xPath) iterates over the nodes selected by an XPath expression, and the xPath query language does the same from the template, with the parsed org.w3c.dom.Document in the built-in parameter XML_DATA_DOCUMENT. Each field says which child to read in its <description>, which is an XPath relative to the row node. The template differs from sales.jrxml as follows:

sales-xpath.jrxml (beginning)
<jasperReport name="sales" language="java" pageWidth="595" pageHeight="842"
              columnWidth="515" leftMargin="40" rightMargin="40" topMargin="40" bottomMargin="40">
  <query language="xPath"><![CDATA[/sales/sale]]></query>
  <field name="category" class="java.lang.String"><description><![CDATA[category]]></description></field>
  <field name="title" class="java.lang.String"><description><![CDATA[title]]></description></field>
  <field name="quantity" class="java.lang.Integer"><description><![CDATA[quantity]]></description></field>
  <field name="amount" class="java.math.BigDecimal"><description><![CDATA[amount]]></description></field>

Both paths need the jasperreports-jaxen module (net.sf.jasperreports:jasperreports-jaxen:7.0.8), which provides the XPath engine: with it removed from the classpath the fill fails with a "JaxenXPathExecuterFactory not found" error (checked on 7.0.8). The JRXmlDataSource and the xPath query in the example above print the same rows. When parsing XML that comes from outside, disable DTDs on the DocumentBuilderFactory, as the example does for the query variant. The number and date patterns (#0.00, yyyy-MM-dd) and the locale tell the data source how to parse the text of the elements into the field classes. See the XML data source sample and the JRXmlDataSource Javadoc.

CSV and Excel

JRCsvDataSource (in the core jar) reads delimited text. With setUseFirstRowAsHeader(true) the header names are the field names; without it, setColumnNames supplies them. The record and field delimiters, the charset, and the locale used to parse numbers and dates are all configurable, and getting the locale wrong is the usual cause of wrong amounts: 44.97 parsed in a locale with a decimal comma is not 44.97.

Excel is read by ExcelDataSource in jasperreports-excel-poi (Apache POI; .xls and .xlsx), or by FastExcelDataSource in jasperreports-fastexcel, a lighter reader for .xlsx only. Both extend AbstractXlsDataSource, so they share setUseFirstRowAsHeader, setColumnNames and setSheetSelection. The example builds a small workbook in memory so that it needs no external file; it needs net.sf.jasperreports:jasperreports-excel-poi:7.0.8. The FastExcel variant was also run with the same workbook and gave the same rows: replace the data source with new FastExcelDataSource(inputStream), which has the same setters, and the artifact with jasperreports-fastexcel.

CsvAndExcelReport.java
package bookshelf;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.InputStream;
import java.math.BigDecimal;
import java.nio.charset.StandardCharsets;
import java.util.HashMap;
import java.util.Locale;

import org.apache.poi.ss.usermodel.Row;
import org.apache.poi.ss.usermodel.Sheet;
import org.apache.poi.xssf.usermodel.XSSFWorkbook;

import net.sf.jasperreports.engine.JasperFillManager;
import net.sf.jasperreports.engine.JasperReport;
import net.sf.jasperreports.engine.data.JRCsvDataSource;
import net.sf.jasperreports.poi.data.ExcelDataSource;

public class CsvAndExcelReport {

    static final String CSV = """
            category,title,quantity,amount
            Fiction,The Pragmatic Novel,3,44.97
            Science,Entropy for Librarians,2,59.00
            """;

    public static void main(String[] args) throws Exception {
        JasperReport report = Support.compile("/reports/sales.jrxml");

        // CSV: the first row names the fields; the locale decides how "44.97" is parsed.
        JRCsvDataSource csv = new JRCsvDataSource(
                new ByteArrayInputStream(CSV.getBytes(StandardCharsets.UTF_8)), "UTF-8");
        csv.setUseFirstRowAsHeader(true);
        csv.setRecordDelimiter("\n");
        csv.setFieldDelimiter(',');
        csv.setLocale(Locale.ROOT);
        Support.toPdf(JasperFillManager.fillReport(report, new HashMap<>(), csv), "ds-csv.pdf");

        // Excel: the Apache POI based data source reads .xls and .xlsx (jasperreports-excel-poi).
        // The FastExcel alternative (jasperreports-fastexcel) is the lighter .xlsx-only reader.
        try (InputStream xlsx = new ByteArrayInputStream(buildWorkbook())) {
            ExcelDataSource excel = new ExcelDataSource(xlsx);
            excel.setUseFirstRowAsHeader(true);
            excel.setSheetSelection("Sales");
            Support.toPdf(JasperFillManager.fillReport(report, new HashMap<>(), excel), "ds-excel.pdf");
        }
    }

    /** Builds a small workbook so that the example needs no external file. */
    private static byte[] buildWorkbook() throws Exception {
        try (XSSFWorkbook wb = new XSSFWorkbook(); ByteArrayOutputStream out = new ByteArrayOutputStream()) {
            Sheet sheet = wb.createSheet("Sales");
            String[][] rows = {
                {"category", "title", "quantity", "amount"},
                {"Fiction", "The Pragmatic Novel", "3", "44.97"},
                {"Science", "Entropy for Librarians", "2", "59.00"}};
            for (int r = 0; r < rows.length; r++) {
                Row row = sheet.createRow(r);
                for (int c = 0; c < rows[r].length; c++) {
                    if (r > 0 && c >= 2) {
                        row.createCell(c).setCellValue(new BigDecimal(rows[r][c]).doubleValue());
                    } else {
                        row.createCell(c).setCellValue(rows[r][c]);
                    }
                }
            }
            wb.write(out);
            return out.toByteArray();
        }
    }
}

The JasperReports samples that these two derive from are the CSV data source sample and the Excel data source sample. The Javadoc is at JRCsvDataSource and ExcelDataSource.

Data adapters

A data adapter packages a data source’s configuration (a CSV file and its delimiters, a JDBC connection, an Excel workbook) as a small file, so that a designer can choose it in Jaspersoft Studio and the template can name it instead of being filled by Java code. Adapters ship in jasperreports-data-adapters and the format modules (jasperreports-json, jasperreports-excel-poi, jasperreports-data-adapters-http for HTTP sources). In 7.0.8 an adapter file is XML: the root element names the adapter type and a class attribute names the implementation.

The template points at the adapter with the net.sf.jasperreports.data.adapter property, and the file is resolved through the repository, so a classpath-relative path works. This example reads the CSV file from the classpath; the .jrdax file below was written by serialising a CsvDataAdapterImpl with Jackson’s XmlMapper and trimming the empty elements, because in 7.0 the adapter classes are Jackson-annotated and that is the simplest way to get the exact element names.

src/main/resources/data/sales-csv.jrdax
<?xml version="1.0" encoding="UTF-8"?>
<csvDataAdapter class="net.sf.jasperreports.data.csv.CsvDataAdapterImpl">
  <name>sales-csv</name>
  <dataFile type="repositoryDataLocation">
    <location>data/sales.csv</location>
  </dataFile>
  <encoding>UTF-8</encoding>
  <recordDelimiter>&#10;</recordDelimiter>
  <fieldDelimiter>,</fieldDelimiter>
  <useFirstRowAsHeader>true</useFirstRowAsHeader>
  <locale>en</locale>
</csvDataAdapter>
sales-adapter.jrxml (beginning)
<jasperReport name="sales" language="java" pageWidth="595" pageHeight="842"
              columnWidth="515" leftMargin="40" rightMargin="40" topMargin="40" bottomMargin="40">
  <property name="net.sf.jasperreports.data.adapter" value="data/sales-csv.jrdax"/>
  <field name="category" class="java.lang.String"/>
DataAdapterReport.java
package bookshelf;

import java.util.HashMap;

import net.sf.jasperreports.engine.JasperFillManager;
import net.sf.jasperreports.engine.JasperReport;

public class DataAdapterReport {

    public static void main(String[] args) throws Exception {
        // The template names the adapter in the net.sf.jasperreports.data.adapter property;
        // no data source or connection is passed to fillReport.
        JasperReport report = Support.compile("/reports/sales-adapter.jrxml");
        Support.toPdf(JasperFillManager.fillReport(report, new HashMap<>()), "ds-adapter.pdf");
    }
}

No data source or connection is passed to fillReport: the adapter supplies the data source itself. The example above needs net.sf.jasperreports:jasperreports-data-adapters:7.0.8, and it read the file from the classpath. Credentials for JDBC and HTTP adapters should not be stored in the .jrdax file; the adapter files are plain XML and usually end up in version control. The Excel data adapter sample is the official example of the mechanism, and net.sf.jasperreports.data.adapter is the property in the configuration reference.

Hibernate HQL and Jakarta JPA

Two query languages run object queries instead of SQL: ejbql (JPA) in jasperreports-ejbql, and hql (Hibernate native) in jasperreports-hibernate. The older JasperReports books show these from the Hibernate 3 and javax.persistence era, with an OLAP chapter around Mondrian and XMLA. That is outdated on three points. The modules now target Jakarta Persistence: jasperreports-ejbql and jasperreports-hibernate use the jakarta.persistence namespace (the executer class lives in net.sf.jasperreports.jakarta.ejbql), and the jasperreports-ejbql-j2ee and jasperreports-hibernate-j2ee artifacts exist for applications still on javax. Hibernate 3 is long gone: the Hibernate module is built against Hibernate ORM 6.3.1, and it was not run against Spring Boot 4’s Hibernate 7. And the OLAP and Mondrian support is largely retired and is not covered here.

The JPA path was run on Hibernate ORM 7.4.12 (a Hibernate 7 release, the generation Spring Boot 4 uses) with Jakarta Persistence 3.2.0, although jasperreports-ejbql 7.0.8 declares 3.1.0. The template declares the language, the query refers to the entity name and a report parameter, and the EntityManager is passed in the built-in JPA_ENTITY_MANAGER parameter.

sales-ejbql.jrxml (beginning)
<jasperReport name="sales" language="java" pageWidth="595" pageHeight="842"
              columnWidth="515" leftMargin="40" rightMargin="40" topMargin="40" bottomMargin="40">
  <parameter name="MIN_AMOUNT" class="java.math.BigDecimal"/>
  <query language="ejbql"><![CDATA[
    SELECT s FROM SaleEntity s WHERE s.amount >= $P{MIN_AMOUNT} ORDER BY s.category, s.title
  ]]></query>
  <field name="category" class="java.lang.String"/>
  <field name="title" class="java.lang.String"/>
  <field name="quantity" class="java.lang.Integer"/>

The fields are read from the selected entity by bean getters, so the entity needs getters for the attributes that the report uses; without them the fill fails with the same "unknown property" error as for a record. Dependencies: net.sf.jasperreports:jasperreports-ejbql:7.0.8 plus Hibernate ORM and a driver; the persistence unit and entity follow.

SaleEntity.java
package bookshelf;

import java.math.BigDecimal;

import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.Id;

@Entity
public class SaleEntity {

    @Id
    @GeneratedValue
    private Long id;
    private String category;
    private String title;
    private int quantity;
    private BigDecimal amount;

    protected SaleEntity() {
    }

    public SaleEntity(String category, String title, int quantity, BigDecimal amount) {
        this.category = category;
        this.title = title;
        this.quantity = quantity;
        this.amount = amount;
    }

    public String getCategory() {
        return category;
    }

    public String getTitle() {
        return title;
    }

    public int getQuantity() {
        return quantity;
    }

    public BigDecimal getAmount() {
        return amount;
    }
}
src/main/resources/META-INF/persistence.xml
<?xml version="1.0" encoding="UTF-8"?>
<persistence xmlns="https://jakarta.ee/xml/ns/persistence" version="3.0">
  <persistence-unit name="bookshelf" transaction-type="RESOURCE_LOCAL">
    <class>bookshelf.SaleEntity</class>
    <properties>
      <property name="jakarta.persistence.jdbc.url" value="jdbc:h2:mem:jpa"/>
      <property name="jakarta.persistence.jdbc.user" value="sa"/>
      <property name="jakarta.persistence.jdbc.password" value=""/>
      <property name="hibernate.hbm2ddl.auto" value="create-drop"/>
    </properties>
  </persistence-unit>
</persistence>
JpaReport.java
package bookshelf;

import java.math.BigDecimal;
import java.util.HashMap;
import java.util.Map;

import jakarta.persistence.EntityManager;
import jakarta.persistence.EntityManagerFactory;
import jakarta.persistence.Persistence;
import net.sf.jasperreports.engine.JasperFillManager;
import net.sf.jasperreports.engine.JasperPrint;
import net.sf.jasperreports.engine.JasperReport;

public class JpaReport {

    public static void main(String[] args) throws Exception {
        JasperReport report = Support.compile("/reports/sales-ejbql.jrxml");
        try (EntityManagerFactory emf = Persistence.createEntityManagerFactory("bookshelf");
                EntityManager em = emf.createEntityManager()) {
            em.getTransaction().begin();
            em.persist(new SaleEntity("Fiction", "The Pragmatic Novel", 3, new BigDecimal("44.97")));
            em.persist(new SaleEntity("Fiction", "Garden of Forking Paths", 1, new BigDecimal("12.50")));
            em.persist(new SaleEntity("Science", "Entropy for Librarians", 2, new BigDecimal("59.00")));
            em.getTransaction().commit();

            Map<String, Object> params = new HashMap<>();
            params.put("MIN_AMOUNT", new BigDecimal("20.00"));
            // The EntityManager is passed in the built-in JPA_ENTITY_MANAGER parameter.
            params.put("JPA_ENTITY_MANAGER", em);
            JasperPrint print = JasperFillManager.fillReport(report, params);
            Support.toPdf(print, "ds-jpa.pdf");
        }
    }
}

Inside Spring Boot the EntityManager is injected and the persistence unit is the application’s own, so only the fill call is needed. Remember that the executer reads the result while filling: the transaction or the open EntityManager has to stay open until fillReport returns, and a lazily loaded association read after the manager is closed fails. For very large result sets the usual advice applies, namely to select only the needed columns and to page.

The Hibernate native variant is analogous: <query language="hql"> with the Hibernate Session passed in the HIBERNATE_SESSION built-in parameter (type org.hibernate.Session). This one was not run for this page, since the module is built against Hibernate 6.3. Both are described in the official EJBQL sample and Hibernate sample.

Custom data sources and rewindable data sources

Anything that can produce rows can be a data source: implement JRDataSource, or JRRewindableDataSource when it can restart. The class below wraps a Supplier<Iterator<SaleRow>>, which is what a Spring Data repository returns as a stream or a cursor. Because the supplier creates a fresh iterator, moveFirst() is trivial, and rows are never held in memory more than one at a time. It resolves each field through the field name; a data source that wants the description as a mapping hint would call field.getDescription().

SaleStreamDataSource.java
package bookshelf;

import java.util.Iterator;
import java.util.function.Supplier;

import net.sf.jasperreports.engine.JRException;
import net.sf.jasperreports.engine.JRField;
import net.sf.jasperreports.engine.JRRewindableDataSource;

/**
 * A custom data source over any Iterator of SaleRow. The supplier creates a fresh iterator,
 * which is what makes the data source rewindable (subreports and repeated fills call moveFirst).
 */
public class SaleStreamDataSource implements JRRewindableDataSource {

    private final Supplier<Iterator<SaleRow>> source;
    private Iterator<SaleRow> iterator;
    private SaleRow current;

    public SaleStreamDataSource(Supplier<Iterator<SaleRow>> source) {
        this.source = source;
        this.iterator = source.get();
    }

    @Override
    public boolean next() throws JRException {
        if (!iterator.hasNext()) {
            return false;
        }
        current = iterator.next();
        return true;
    }

    @Override
    public Object getFieldValue(JRField field) throws JRException {
        return switch (field.getName()) {
            case "category" -> current.getCategory();
            case "title" -> current.getTitle();
            case "quantity" -> current.getQuantity();
            case "amount" -> current.getAmount();
            default -> throw new JRException("Unknown field: " + field.getName());
        };
    }

    @Override
    public void moveFirst() throws JRException {
        iterator = source.get();
        current = null;
    }
}

A rewindable data source can be reused: the second fillReport in CustomSourcesReport below calls moveFirst() on the same instance and reads the rows again. A data source that cannot rewind (a JDBC result set, the CSV source) must be created again for every fill and every subreport that uses it. When a stream backs the data source, the stream (and so the database cursor) has to stay open for the whole fill, which is the reason to keep fills short and to fill in a transaction scope.

Custom query executers

A query executer is for the case where data should be requested from the template, with a language of your own, and parameters substituted into the query text. Three pieces are needed: a factory implementing QueryExecuterFactory (extend AbstractQueryExecuterFactory), an executer implementing JRQueryExecuter (extend JRAbstractQueryExecuter, which parses the query text and resolves $P{...} references), and the registration of the language. The executer’s createDatasource() returns a JRDataSource, here the custom one above, and close() releases resources. The abstract base class requires one more method, getParameterReplacement(String), which returns the text to insert for a parameter reference in the query.

The factory declares built-in parameters as name and type pairs. They are added to every report that uses the language, so the caller can pass the repository in the parameters map. The query text here is just the category name, which the template fills from the CATEGORY parameter.

SalesRepository.java
package bookshelf;

import java.util.List;

public interface SalesRepository {

    List<SaleRow> findByCategory(String category);
}
SalesQueryExecuterFactory.java
package bookshelf;

import java.util.Map;

import net.sf.jasperreports.engine.JRDataset;
import net.sf.jasperreports.engine.JRException;
import net.sf.jasperreports.engine.JRValueParameter;
import net.sf.jasperreports.engine.JasperReportsContext;
import net.sf.jasperreports.engine.query.AbstractQueryExecuterFactory;
import net.sf.jasperreports.engine.query.JRQueryExecuter;

/** Factory for the custom "bookshelf" query language. */
public class SalesQueryExecuterFactory extends AbstractQueryExecuterFactory {

    public static final String PARAMETER_SALES_REPOSITORY = "SALES_REPOSITORY";

    // Built-in parameters are declared as name/type pairs; they are added to every report that uses this language.
    private static final Object[] BUILTIN_PARAMETERS = {
        PARAMETER_SALES_REPOSITORY, SalesRepository.class
    };

    @Override
    public Object[] getBuiltinParameters() {
        return BUILTIN_PARAMETERS;
    }

    @Override
    public JRQueryExecuter createQueryExecuter(JasperReportsContext context, JRDataset dataset,
            Map<String, ? extends JRValueParameter> parameters) throws JRException {
        return new SalesQueryExecuter(context, dataset, parameters);
    }

    @Override
    public boolean supportsQueryParameterType(String className) {
        return true;
    }
}
SalesQueryExecuter.java
package bookshelf;

import java.util.Map;

import net.sf.jasperreports.engine.JRDataSource;
import net.sf.jasperreports.engine.JRDataset;
import net.sf.jasperreports.engine.JRException;
import net.sf.jasperreports.engine.JRValueParameter;
import net.sf.jasperreports.engine.JasperReportsContext;
import net.sf.jasperreports.engine.query.JRAbstractQueryExecuter;

/** Runs the query text of a "bookshelf" query: the category name, which may contain $P{} parameters. */
public class SalesQueryExecuter extends JRAbstractQueryExecuter {

    private final SalesRepository repository;

    protected SalesQueryExecuter(JasperReportsContext context, JRDataset dataset,
            Map<String, ? extends JRValueParameter> parameters) {
        super(context, dataset, parameters);
        this.repository = (SalesRepository) getParameterValue(SalesQueryExecuterFactory.PARAMETER_SALES_REPOSITORY);
        parseQuery();
    }

    @Override
    protected String getParameterReplacement(String parameterName) {
        // The parameter value is substituted into the query text as is.
        return String.valueOf(getParameterValue(parameterName));
    }

    @Override
    public JRDataSource createDatasource() throws JRException {
        String category = getQueryString().trim();
        return new SaleStreamDataSource(() -> repository.findByCategory(category).iterator());
    }

    @Override
    public void close() {
        // Nothing to release: the repository belongs to the caller.
    }

    @Override
    public boolean cancelQuery() throws JRException {
        return false;
    }
}

The template names the language and uses the parameter in the query:

sales-bookshelf.jrxml (beginning)
<jasperReport name="sales" language="java" pageWidth="595" pageHeight="842"
              columnWidth="515" leftMargin="40" rightMargin="40" topMargin="40" bottomMargin="40">
  <parameter name="CATEGORY" class="java.lang.String"/>
  <query language="bookshelf"><![CDATA[$P{CATEGORY}]]></query>

Registration is a property of the form net.sf.jasperreports.query.executer.factory.<language>, set in jasperreports.properties on the classpath root:

src/main/resources/jasperreports.properties
net.sf.jasperreports.query.executer.factory.bookshelf=bookshelf.SalesQueryExecuterFactory

The same property in jasperreports_extension.properties is not read for this purpose: with it there, compiling the template failed with "No query executer factory registered for the bookshelf language" (checked on 7.0.8). The extension file is for extension registry factories, and that is how the jasperreports-ejbql and jasperreports-hibernate modules register their languages, through a query executer factory bundle; a library that ships its own language would do the same, while an application only needs the property.

The driver below runs both custom pieces: the data source with a second fill after moveFirst(), and the query executer with the repository passed as a parameter. The second report printed only the two Fiction rows, which shows that the parameter substitution reached the repository.

CustomSourcesReport.java
package bookshelf;

import java.math.BigDecimal;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import net.sf.jasperreports.engine.JasperFillManager;
import net.sf.jasperreports.engine.JasperPrint;
import net.sf.jasperreports.engine.JasperReport;

public class CustomSourcesReport {

    public static void main(String[] args) throws Exception {
        List<SaleRow> all = List.of(
                new SaleRow("Fiction", "The Pragmatic Novel", 3, new BigDecimal("44.97")),
                new SaleRow("Fiction", "Garden of Forking Paths", 1, new BigDecimal("12.50")),
                new SaleRow("Science", "Entropy for Librarians", 2, new BigDecimal("59.00")));

        // 1. A custom, rewindable JRDataSource passed to fillReport.
        JasperReport plain = Support.compile("/reports/sales.jrxml");
        SaleStreamDataSource stream = new SaleStreamDataSource(all::iterator);
        JasperPrint first = JasperFillManager.fillReport(plain, new HashMap<>(), stream);
        Support.toPdf(first, "ds-custom.pdf");
        stream.moveFirst(); // reuse the same data source for a second fill
        Support.toPdf(JasperFillManager.fillReport(plain, new HashMap<>(), stream), "ds-custom-again.pdf");

        // 2. A custom query executer: the template says language="bookshelf" and the engine finds the factory
        // registered in jasperreports.properties.
        SalesRepository repository = category -> all.stream()
                .filter(s -> s.getCategory().equals(category)).toList();
        JasperReport report = Support.compile("/reports/sales-bookshelf.jrxml");
        Map<String, Object> params = new HashMap<>();
        params.put("CATEGORY", "Fiction");
        params.put(SalesQueryExecuterFactory.PARAMETER_SALES_REPOSITORY, repository);
        Support.toPdf(JasperFillManager.fillReport(report, params), "ds-executer.pdf");
    }
}

The extension points are documented in the Javadoc for JRDataSource, JRRewindableDataSource, QueryExecuterFactory and JRAbstractQueryExecuter, and the configuration reference lists the query executer factory properties. Expressions and parameters, including the built-in REPORT_DATA_SOURCE and REPORT_CONNECTION, are the topic of Expressions, Parameters, Fields & Variables.

Choosing a source

Source Choose it when Watch out for

Bean or map collection

Rows are already in memory after the service’s own mapping and authorisation.

Records are not beans; the whole collection is in memory.

JDBC query in the template

The designer owns a relational query and the data does not need Java rules.

$P!{} pastes text into the SQL; the connection is yours to close.

JSON, JSONQL, XML, XPath

Another service returns a document.

XPath needs jasperreports-jaxen; harden the XML parser; parse locale and patterns.

CSV, Excel

Users or finance supply files.

Locale-dependent numbers; the CSV source is not rewindable.

Data adapter

The designer wants to pick the source in Jaspersoft Studio.

Credentials in the adapter file.

JPA or Hibernate query

The report should use the entity model.

Getters are required; keep the session open for the fill; the Hibernate module is not yet verified against Hibernate 7.

Custom data source or executer

Streaming from a cursor, or a data store with no ready-made source.

You own field mapping, resource release and thread safety.