Streaming Cursors
The Marshaller class exposes streaming methods that let you consume a REST response as a
lazy cursor instead of materializing the full response body into a single POJO. This is the
client-side complement of the server-side
REST Streaming Integration page.
See Token-Streaming Basics for a full explanation of cursor types, capability role interfaces, and the per-format capability matrix.
Getting an InputStream from a response
Obtain the raw response stream via
ResponseBody.asStream()
and pass it directly to a Marshaller streaming method:
RestClient client = RestClient.create();
try (var resp = client.get("/api/records").run();
InputStream in = resp.body().asStream()) {
// Stream is open; pass it to the cursor factory
Marshaller m = Json.DEFAULT;
try (RecordReader r = m.readRecords(in)) {
while (r.canRead()) {
MyBean bean = r.read(MyBean.class);
process(bean);
}
}
}
The outer try keeps the HTTP connection open while you iterate; the inner try closes the
cursor (and the stream) when iteration is complete. Do not call ResponseBody.asString()
or any other buffering method before opening the cursor — those consume the stream first.
readRecords() — whole-value record cursor
Marshaller.readRecords(Object) opens a
RecordReader
that yields one deserialized value per call. It works with every format that implements
RecordReadable.
Marshaller m = Jsonl.DEFAULT; // JSONL: one JSON value per line
try (var resp = client.get("/api/export")
.header("Accept", "application/jsonl")
.run();
InputStream in = resp.body().asStream();
RecordReader r = m.readRecords(in)) {
while (r.canRead()) {
MyBean bean = r.read(MyBean.class);
process(bean);
}
}
Stream and cursor can be opened together in a single try resource list.
readArrayRecords() — element-by-element array streaming
When the server returns a single top-level JSON (or CBOR / MsgPack) array,
readArrayRecords() streams each element as a separate record without buffering the
full array:
Marshaller m = Json.DEFAULT;
try (var resp = client.get("/api/items").run();
InputStream in = resp.body().asStream();
RecordReader r = m.readArrayRecords(in)) {
while (r.canRead()) {
Item item = r.read(Item.class);
process(item);
}
}
Only formats that implement ArrayRecordReadable support this method — see the capability matrix.
readTokens() — fine-grained structural cursor
For FULL-tier formats (JSON family, CBOR, MsgPack) you can open a
TokenReader
and navigate the document structure token-by-token via a StAX-like next()/getCurrentToken()
cursor:
Marshaller m = Json.DEFAULT;
try (var resp = client.get("/api/data").run();
InputStream in = resp.body().asStream();
TokenReader r = m.readTokens(in)) {
while (r.next() != TokenType.END_OF_STREAM) {
switch (r.getCurrentToken()) {
case FIELD_NAME -> System.out.print(r.getFieldName() + " = ");
case VALUE_STRING -> System.out.println(r.getString());
case VALUE_NUMBER -> System.out.println(r.getNumber());
case VALUE_BOOLEAN -> System.out.println(r.getBool());
default -> { /* structural tokens: START_OBJECT, END_OBJECT, START_ARRAY, END_ARRAY, VALUE_NULL */ }
}
}
}
Only formats that implement TokenReadable support
readTokens(). Calling it on a PARTIAL-tier format (e.g.Yaml.DEFAULT) throwsClassCastExceptionat runtime.
Using iterator() and stream() views
RecordReader exposes JDK-friendly iteration surfaces:
try (var resp = client.get("/api/events")
.header("Accept", "application/jsonl")
.run();
InputStream in = resp.body().asStream();
RecordReader r = Jsonl.DEFAULT.readRecords(in)) {
// Enhanced-for loop via Iterable adapter
for (Event e : (Iterable<Event>) () -> r.iterator(Event.class)) {
handle(e);
}
}
// Or as a Java Stream (cursor is still closed by the outer try)
try (var resp = client.get("/api/events")
.header("Accept", "application/jsonl")
.run();
InputStream in = resp.body().asStream();
RecordReader r = Jsonl.DEFAULT.readRecords(in)) {
long count = r.stream(Event.class).filter(Event::isActive).count();
}
Checking cursor quality
RecordReader.isStreaming() returns true when the cursor is a true O(1) streaming
cursor (constant memory per record) and false when it is backed by a buffered adapter.
Check it before processing very large responses if memory is a concern:
Marshaller m = Yaml.DEFAULT; // PARTIAL-tier: buffered record adapter
try (var resp = client.get("/api/data")
.header("Accept", "application/yaml")
.run();
InputStream in = resp.body().asStream();
RecordReader r = m.readRecords(in)) {
boolean isStreaming = r.isStreaming(); // false for YAML
// ...
}
See also
- Token-Streaming Basics — cursor types, role interfaces, capability matrix
- Record Streaming —
RecordReader/RecordWritercontract - Token Streaming —
TokenReader/TokenWritercontract - Array-Record Streaming — element-by-element array cursors
- REST Streaming Integration — server-side cursor parameters
- ResponseBody —
ResponseBody.asStream()(NG equivalent; see Response Content for the classic client'sResponseContent)