go 107 lines · 3 tabs

Stream and Decode Newline-Delimited JSON (NDJSON) from an HTTP Response Body in Go

Shared by codesnips Jul 2026
3 tabs
package feed

import "time"

type Event struct {
	ID        string    `json:"id"`
	Type      string    `json:"type"`
	Actor     string    `json:"actor"`
	CreatedAt time.Time `json:"created_at"`
}
3 files · go Explain with highlit

Newline-delimited JSON (NDJSON) is a wire format where each line is a self-contained JSON object, which makes it ideal for streaming large result sets over HTTP without buffering the entire payload in memory. This snippet shows how to consume such a stream incrementally in Go, decoding one record at a time as bytes arrive off the socket.

The event.go tab defines the Event value type that each line decodes into. Nothing exotic happens here — it exists so the decoder has a concrete target and so the consumer code reads clearly. Keeping the domain type small and separate is deliberate: the streaming logic should not care what shape the records take.

The ndjson.go tab holds the core reader. StreamEvents wraps the response body in a bufio.Scanner and raises the token buffer with scanner.Buffer so a single oversized line does not trip the default 64KB limit. Rather than returning a slice, it returns a receive-only channel and pushes each decoded Event through it; this gives the caller natural backpressure, since the goroutine blocks on the send until the consumer is ready. The context.Context is checked on every iteration so a cancelled request stops the scan promptly instead of draining the whole body. Blank lines are skipped, JSON errors are surfaced through a second error channel, and defer body.Close() guarantees the socket is released even on early exit. Returning two channels keeps data and failure paths distinct without forcing a sentinel value into the stream.

The client.go tab ties it together. FetchEvents builds a request with the caller's context.Context, sets an Accept header advertising application/x-ndjson, and hands the live resp.Body straight to StreamEvents — the body is never read into memory whole. The consumer uses a select over the two channels so it can react to either a record or an error, and the ok check on the event channel signals clean end-of-stream.

The main trade-off is that a bufio.Scanner-based approach assumes newline framing is reliable and that no single record exceeds the configured buffer; for adversarial or unbounded input a json.Decoder reading tokens directly is safer. For well-behaved server streams, though, this pattern is simple, allocation-light, and cancellable.


Related snips

Share this code

Here's the card — post it anywhere.

Stream and Decode Newline-Delimited JSON (NDJSON) from an HTTP Response Body in Go — share card
Link copied