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