@@ -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
0 commit comments