`ItemReader`s: files & XML/JSON

This section documents the current Spring Batch line — 6.0.x, on Spring Framework 7 and Spring Boot 4.1.x, with a Java 17+ baseline — as published at the Spring Batch reference documentation. No specific patch version is pinned. Some surfaces (Spring Cloud Task and Spring Cloud Data Flow orchestration, the deployer-based partition handler, and JSR-352) are linked, not documented in depth.

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

Most batch input arrives as a file. Spring Batch supplies readers for delimited and fixed-length flat files, XML and JSON, and a contract for writing your own.

The ItemReader contract

public interface ItemReader<T> {
    T read() throws Exception;   // returns the next item, or null when exhausted
}

read() returns one item per call and null exactly once, to signal the end of input — that null ends the step. Returning null early therefore truncates the job, which is a common bug in hand-written readers.

ItemStreamReader<T> adds the ItemStream callbacks (open, update, close) that make a reader restartable: update writes the current position into the step ExecutionContext at every commit point, and open reads it back on restart. Every framework reader implements it. See Steps, executions & the ExecutionContext for the contract and for registering delegate streams.

FlatFileItemReader

Reading a flat file is a chain of four responsibilities:

flowchart LR File["Resource
one line of text"] --> LM["LineMapper"] LM --> LT["LineTokenizer
split the line"] LT --> FS["FieldSet
typed positional/named access"] FS --> FSM["FieldSetMapper
build the domain object"] FSM --> Item["item"]

FlatFileItemReaderBuilder assembles all four for the common cases:

@Bean
@StepScope
public FlatFileItemReader<TradeCsv> tradeReader(
        @Value("#{jobParameters['input.file']}") Resource inputFile) {
    return new FlatFileItemReaderBuilder<TradeCsv>()
            .name("tradeReader")            // required: the ExecutionContext key prefix
            .resource(inputFile)
            .linesToSkip(1)                 // a header row
            .delimited()
            .delimiter(",")
            .quoteCharacter('"')
            .names("externalId", "isin", "quantity", "price", "tradeDate")
            .targetType(TradeCsv.class)     // maps by property name
            .strict(true)                   // fail if the resource is missing
            .build();
}

.name(…​) is mandatory for a restartable reader — it is the key under which the line count is saved.

A fixed-length file uses column ranges instead of a delimiter:

@Bean
public FlatFileItemReader<Customer> fixedWidthCustomerReader() {
    return new FlatFileItemReaderBuilder<Customer>()
            .name("fixedWidthCustomerReader")
            .resource(new ClassPathResource("customers.txt"))
            .fixedLength()
            .columns(new Range(1, 10), new Range(11, 40), new Range(41, 42), new Range(43, 52))
            .names("customerId", "name", "countryCode", "balance")
            .targetType(Customer.class)
            .build();
}

A custom FieldSetMapper

targetType(…​) uses a BeanWrapperFieldSetMapper under the covers, which needs a no-argument constructor and setters. For conversion, validation or an immutable type, write the mapper:

public class TradeFieldSetMapper implements FieldSetMapper<Trade> {

    @Override
    public Trade mapFieldSet(FieldSet fieldSet) {
        return new Trade(
                fieldSet.readString("isin"),
                fieldSet.readLong("quantity"),
                fieldSet.readBigDecimal("price"),
                LocalDate.parse(fieldSet.readString("tradeDate")));
    }
}
.delimited()
.names("isin", "quantity", "price", "tradeDate")
.fieldSetMapper(new TradeFieldSetMapper())

FieldSet is the typed accessor over the tokenized line: readString, readInt, readLong, readBigDecimal, readBoolean, readDate, each by name or index.

Files with several record formats

A file that mixes header, detail and trailer records needs a LineMapper that chooses a tokenizer per line prefix:

@Bean
public FlatFileItemReader<Object> mixedRecordReader() {
    PatternMatchingCompositeLineMapper<Object> lineMapper =
            new PatternMatchingCompositeLineMapper<>();

    lineMapper.setTokenizers(Map.of(
            "HDR*", headerTokenizer(),
            "DTL*", detailTokenizer(),
            "TRL*", trailerTokenizer()));

    lineMapper.setFieldSetMappers(Map.of(
            "HDR*", new HeaderFieldSetMapper(),
            "DTL*", new DetailFieldSetMapper(),
            "TRL*", new TrailerFieldSetMapper()));

    return new FlatFileItemReaderBuilder<Object>()
            .name("mixedRecordReader")
            .resource(new FileSystemResource("/data/in/mixed.txt"))
            .lineMapper(lineMapper)
            .build();
}

The patterns use the same */? wildcards as flow transitions.

Multi-line records

When one logical item spans several physical lines, wrap the flat-file reader in a reader that assembles them. The typical shape is a delegate plus a loop that reads until the record’s terminator:

public class TradeGroupReader implements ItemStreamReader<TradeGroup> {

    private final FlatFileItemReader<Object> delegate;

    public TradeGroupReader(FlatFileItemReader<Object> delegate) {
        this.delegate = delegate;
    }

    @Override
    public TradeGroup read() throws Exception {
        TradeGroup group = null;
        for (Object line = delegate.read(); line != null; line = delegate.read()) {
            if (line instanceof Header header) {
                group = new TradeGroup(header);
            } else if (line instanceof Detail detail && group != null) {
                group.add(detail);
            } else if (line instanceof Trailer && group != null) {
                return group;            // record complete
            }
        }
        return group;                     // last record, or null at end of file
    }

    @Override public void open(ExecutionContext context) { delegate.open(context); }
    @Override public void update(ExecutionContext context) { delegate.update(context); }
    @Override public void close() { delegate.close(); }
}

Delegating open/update/close is what keeps the composite restartable.

Reading a directory of files

MultiResourceItemReader runs one delegate over many resources, tracking which file and which line it is on:

@Bean
@StepScope
public MultiResourceItemReader<TradeCsv> multiFileReader(
        @Value("#{jobParameters['input.dir']}") String inputDir,
        FlatFileItemReader<TradeCsv> tradeReader) throws IOException {

    Resource[] resources = new PathMatchingResourcePatternResolver()
            .getResources("file:" + inputDir + "/trades-*.csv");

    MultiResourceItemReader<TradeCsv> reader = new MultiResourceItemReader<>();
    reader.setName("multiFileReader");
    reader.setResources(resources);
    reader.setDelegate(tradeReader);
    reader.setStrict(true);
    return reader;
}

Sort the resources deterministically (the default comparator is by filename) so a restart sees the same order.

XML with StaxEventItemReader

StAX-based, so the document is streamed rather than loaded. The fragment root element names the item boundary, and an Unmarshaller (JAXB here) turns each fragment into an object:

@Bean
public StaxEventItemReader<Trade> xmlTradeReader() {
    Jaxb2Marshaller marshaller = new Jaxb2Marshaller();
    marshaller.setClassesToBeBound(Trade.class);

    return new StaxEventItemReaderBuilder<Trade>()
            .name("xmlTradeReader")
            .resource(new FileSystemResource("/data/in/trades.xml"))
            .addFragmentRootElements("trade")
            .unmarshaller(marshaller)
            .build();
}

JSON with JsonItemReader

JsonItemReader streams a JSON array of objects through a JsonObjectReader. The Jackson-backed implementation is JacksonJsonObjectReader, which in the current line uses Jackson 3:

@Bean
public JsonItemReader<Trade> jsonTradeReader() {
    return new JsonItemReaderBuilder<Trade>()
            .name("jsonTradeReader")
            .resource(new FileSystemResource("/data/in/trades.json"))
            .jsonObjectReader(new JacksonJsonObjectReader<>(Trade.class))
            .build();
}

A newline-delimited JSON file is not a JSON array; read it with a FlatFileItemReader whose LineMapper deserialises each line instead.

Custom readers

Implement ItemStreamReader when the source is neither a file nor a database — a REST endpoint, a queue drain, a proprietary API. Two rules cover most mistakes: return null only at true end of input, and save enough state in update to resume. The watermark example in Steps, executions & the ExecutionContext is a complete template.

Dealing with no input

A reader that returns null on the first call produces a step that completes with readCount = 0. Whether that is success or failure is a business decision — attach a NoWorkFoundStepExecutionListener to fail it, or return a custom exit code and branch on it. Both are covered in Stopping, restart & recovery.

Further reading

For the full detail behind this page: