Skip to content

Commit f420015

Browse files
authored
Add JsonStreamResponse documentation for 5.4 (#8255)
* docs: Add JsonStreamResponse documentation for 5.4 * fix: Add blank line before code block for markdown lint * docs: Cross-link JsonView section to JsonStreamResponse Add a short pointer from the JsonView documentation to the streaming response section so users with large payloads discover the streaming alternative directly from the view docs.
1 parent 695cab0 commit f420015

3 files changed

Lines changed: 167 additions & 0 deletions

File tree

docs/en/appendices/5-4-migration-guide.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -104,6 +104,13 @@ version is reported as `unknown`), the header is omitted.
104104
`FormProtectionComponent`.
105105
See [Form Protection Component](../controllers/components/form-protection).
106106

107+
### Http
108+
109+
- Added `JsonStreamResponse` class for memory-efficient streaming of large JSON
110+
datasets using generators. Supports standard JSON arrays and NDJSON formats,
111+
envelope structures with metadata, transform callbacks, and graceful mid-stream
112+
error handling. See [Streaming JSON Responses](../controllers/request-response#streaming-json-responses).
113+
107114
### Database
108115

109116
- Added `notBetween()` method for `NOT BETWEEN` expressions.

docs/en/controllers/request-response.md

Lines changed: 137 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -790,6 +790,143 @@ public function sendIcs()
790790
}
791791
```
792792

793+
<a id="streaming-json-responses"></a>
794+
795+
### Streaming JSON Responses
796+
797+
`class` Cake\\Http\\Response\\**JsonStreamResponse**
798+
799+
When working with large datasets, loading everything into memory before encoding
800+
to JSON can exhaust available memory. `JsonStreamResponse` provides memory-efficient
801+
streaming of JSON data using generators, keeping only one item in memory at a time.
802+
803+
::: info Added in version 5.4.0
804+
:::
805+
806+
#### Basic Usage
807+
808+
```php
809+
use Cake\Http\Response\JsonStreamResponse;
810+
811+
public function index()
812+
{
813+
$query = $this->Articles->find();
814+
815+
// Simple array streaming
816+
return new JsonStreamResponse($query);
817+
// Output: [{"id":1,"title":"First"},{"id":2,"title":"Second"},...]
818+
}
819+
```
820+
821+
#### Constructor Options
822+
823+
The `JsonStreamResponse` constructor accepts an iterable and an options array:
824+
825+
| Option | Type | Default | Description |
826+
|--------|------|---------|-------------|
827+
| `root` | `string\|null` | `null` | Wrap data in `{"root": [...]}` |
828+
| `envelope` | `array` | `[]` | Static metadata merged with streaming data |
829+
| `dataKey` | `string` | `'data'` | Key for streaming data when envelope is used |
830+
| `format` | `string` | `'json'` | Output format: `'json'` or `'ndjson'` |
831+
| `transform` | `callable\|null` | `null` | Transform each item before encoding |
832+
| `flags` | `int` | `DEFAULT_JSON_FLAGS` | JSON encode flags |
833+
834+
#### With Root Wrapper
835+
836+
Wrap the array in an object with a named key:
837+
838+
```php
839+
return new JsonStreamResponse($query, ['root' => 'articles']);
840+
// Output: {"articles":[{"id":1,"title":"First"},{"id":2,"title":"Second"}]}
841+
```
842+
843+
#### With Envelope (Metadata)
844+
845+
Include static metadata alongside the streaming data:
846+
847+
```php
848+
$total = $this->Articles->find()->count();
849+
850+
return new JsonStreamResponse($query, [
851+
'envelope' => ['meta' => ['total' => $total, 'page' => 1]],
852+
'dataKey' => 'articles',
853+
]);
854+
// Output: {"meta":{"total":100,"page":1},"articles":[{"id":1,"title":"First"},...]}
855+
```
856+
857+
#### NDJSON Format
858+
859+
[NDJSON](http://ndjson.org/) (Newline Delimited JSON) outputs one JSON object per
860+
line, useful for streaming to clients that process data incrementally:
861+
862+
```php
863+
return new JsonStreamResponse($query, ['format' => 'ndjson']);
864+
// Output:
865+
// {"id":1,"title":"First"}
866+
// {"id":2,"title":"Second"}
867+
```
868+
869+
The content type is automatically set to `application/x-ndjson; charset=UTF-8`.
870+
871+
#### Transform Callback
872+
873+
Transform each item before JSON encoding. Useful for selecting specific fields
874+
or formatting data:
875+
876+
```php
877+
return new JsonStreamResponse($query, [
878+
'transform' => fn($article) => [
879+
'id' => $article->id,
880+
'title' => $article->title,
881+
'url' => Router::url(['action' => 'view', $article->id]),
882+
],
883+
]);
884+
```
885+
886+
#### Immutability
887+
888+
`JsonStreamResponse` follows PSR-7 immutability patterns. Use `withStreamOptions()`
889+
to create a modified copy:
890+
891+
```php
892+
$response = new JsonStreamResponse($query);
893+
$newResponse = $response->withStreamOptions(['root' => 'articles']);
894+
```
895+
896+
#### Error Handling
897+
898+
`JsonStreamResponse` uses a three-layer error handling strategy:
899+
900+
1. **Pre-validation**: The first item is encoded before output starts. If encoding
901+
fails, an exception is thrown and a proper error response can be returned.
902+
903+
2. **Mid-stream error marker**: If item N (where N > 1) fails to encode, an error
904+
marker is output to maintain valid JSON structure:
905+
906+
```json
907+
[{"id":1},{"__streamError":{"message":"Type is not supported","index":1}}]
908+
```
909+
910+
3. **Server-side logging**: All encoding failures are logged via `Log::error()`.
911+
912+
#### ORM Integration
913+
914+
For true memory-efficient streaming, use unbuffered queries and avoid result
915+
formatters:
916+
917+
```php
918+
// Good - streams one row at a time
919+
$query = $this->Articles->find()->bufferResults(false);
920+
return new JsonStreamResponse($query);
921+
922+
// Avoid - formatters like map(), combine() buffer results internally
923+
$query = $this->Articles->find()->map(fn($row) => $row); // Breaks streaming
924+
```
925+
926+
> [!NOTE]
927+
> Result formatters (`map()`, `combine()`, etc.) buffer results internally,
928+
> which defeats the memory-efficient streaming purpose.
929+
793930
### Setting Headers
794931

795932
`method` Cake\\Http\\Response::**withHeader**(string $name, string|array $value): static

docs/en/views/json-and-xml-views.md

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -200,6 +200,29 @@ $this->viewBuilder()
200200
->setOption('jsonOptions', JSON_FORCE_OBJECT);
201201
```
202202

203+
### Streaming Large JSON Payloads
204+
205+
`JsonView` is a good fit when your action can serialize the complete payload in
206+
memory. For large result sets, use `Cake\Http\Response\JsonStreamResponse`
207+
instead and return it directly from the controller:
208+
209+
```php
210+
use Cake\Http\Response\JsonStreamResponse;
211+
212+
public function export()
213+
{
214+
$query = $this->Articles->find()
215+
->enableHydration(false)
216+
->bufferResults(false);
217+
218+
return new JsonStreamResponse($query);
219+
}
220+
```
221+
222+
See [Streaming JSON Responses](../controllers/request-response#streaming-json-responses)
223+
for the available options, including NDJSON output, item transforms, and
224+
graceful mid-stream error handling.
225+
203226
### JSONP Responses
204227

205228
When using `JsonView` you can use the special view variable `jsonp` to

0 commit comments

Comments
 (0)