Skip to main content

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) throws ClassCastException at 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​