jsonstreaming package - github.com/ValeryVerkhoturov/json-streaming - Go Packages

jsonstreaming

package module
v0.1.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Aug 30, 2026 License: MIT Imports: 8 Imported by: 0

README

json-streaming

Streaming read/write for JSON arrays with constant memory usage.

  • Writer emits an array element-by-element into an io.Writer through a fixed-size buffer.
  • Reader pulls elements one at a time from an io.Reader into a caller-owned destination.
  • Neither side accumulates elements internally — memory is bounded by the writer's flush buffer and the size of a single JSON element.
  • Backed by goccy/go-json for ~3× throughput over encoding/json at the same allocation profile.

Comparison of standard library and library streams.

Install

go get github.com/ValeryVerkhoturov/json-streaming

Requires Go 1.19+.

Writing

Drain rows from a channel into the writer:

// source <-chan Row
sw, err := jsonstreaming.NewStreamWriter(w)
if err != nil {
    return err
}
var row Row
for r := range source {
    row = r
    if err := sw.SetRow(&row); err != nil {
        return err
    }
}
return sw.Flush()

Options:

  • WithBufferSize(n int) — override the flush buffer (default 1 MiB).
  • WithFields(names ...string) — project each row to only the named top-level JSON fields. Given {"id":1,"name":"a","secret":"x"} and WithFields("id","name"), emits {"id":1,"name":"a"}. Costs a bounded []byte alloc per row.
With cancellation

select lets cancellation preempt a blocked channel receive:

sw, err := jsonstreaming.NewStreamWriter(w)
if err != nil {
    return err
}
var row Row
for {
    select {
    case <-ctx.Done():
        return ctx.Err()
    case r, ok := <-source:
        if !ok {
            return sw.Flush()
        }
        row = r
        if err := sw.SetRow(&row); err != nil {
            return err
        }
    }
}

Reading

Fan decoded rows out to a channel:

// sink chan<- Row
sr, err := jsonstreaming.NewStreamReader(r)
if err != nil {
    return err
}
defer sr.Close()

var row Row
for sr.Next() {
    if err := sr.Decode(&row); err != nil {
        return err
    }
    sink <- row // reusing &row keeps allocations bounded
}
return sr.Err()
With cancellation

select on the send preempts a slow consumer:

sr, err := jsonstreaming.NewStreamReader(r)
if err != nil {
    return err
}
defer sr.Close()

var row Row
for sr.Next() {
    if err := sr.Decode(&row); err != nil {
        return err
    }
    select {
    case <-ctx.Done():
        return ctx.Err()
    case sink <- row:
    }
}
return sr.Err()

Wrapping a stream in a JSON object

MarshalStream writes a struct as a JSON object; any field whose kind is chan (with receive direction) is streamed as a JSON array, receiving until the channel closes. Every other field goes through the normal codec.

Unlike the primitives, MarshalStream takes a context.Context — the library drives the channel receive internally, so cancellation cannot come from outside. Cancelling ctx preempts a stalled producer (via reflect.Select) and any pending write to w.

type Response struct {
    Status string     `json:"status"`
    Rows   <-chan Row `json:"rows"`
    Total  int        `json:"total"`
}

ch := make(chan Row)
go func() {
    defer close(ch)
    for _, r := range source {
        select {
        case <-ctx.Done():
            return
        case ch <- r:
        }
    }
}()

m := jsonstreaming.NewMarshaler()
return m.MarshalStream(ctx, w, Response{Status: "ok", Rows: ch, Total: len(source)})
// {"status":"ok","rows":[{"id":1,...},{"id":2,...},...],"total":N}

Reuse one *Marshaler across many calls to share the configured buffer. Options:

  • WithMarshalerBufferSize(n int) — override the flush buffer (default 1 MiB, DefaultBufferSize).

The package-level jsonstreaming.MarshalStream(ctx, w, v) is a shorthand for NewMarshaler().MarshalStream(ctx, w, v) — same behaviour with defaults.

Field names come from json:"..." tags; ,omitempty (via reflect.Value.IsZero) and - are honored. A nil channel emits [].

Memory model

Reusing the same destination pointer across SetRow / Decode calls keeps per-row work in already-owned memory. The writer never buffers more than bufSize bytes before flushing; the reader's internal buffer grows only to the size of the largest single element.

Documentation

Index

Constants

View Source
const DefaultBufferSize = 1 << 20 // 1 MiB

DefaultBufferSize is the fixed window of memory the writer holds before flushing downstream.

Variables

This section is empty.

Functions

func MarshalStream

func MarshalStream(ctx context.Context, w io.Writer, v any) error

MarshalStream is a convenience for NewMarshaler().MarshalStream(ctx, w, v).

Types

type Marshaler

type Marshaler struct {
	// contains filtered or unexported fields
}

func NewMarshaler

func NewMarshaler(opts ...MarshalerOption) *Marshaler

func (*Marshaler) MarshalStream

func (m *Marshaler) MarshalStream(ctx context.Context, w io.Writer, v any) error

MarshalStream writes v as a JSON object to w with streaming semantics for channel fields.

v must be a struct or pointer to struct. Each exported field is emitted in declaration order using its `json:"..."` tag name (falling back to the field name). Fields whose kind is chan (with receive direction) are streamed as a JSON array — MarshalStream receives from the channel until it closes and emits each value as an array element. All other fields go through json.Marshal.

Example:

type Response struct {
    Status string     `json:"status"`
    Rows   <-chan Row `json:"rows"`
    Total  int        `json:"total"`
}
ch := make(chan Row)
go func() { defer close(ch); ch <- Row{ID: 1}; ch <- Row{ID: 2} }()
err := jsonstreaming.NewMarshaler().MarshalStream(ctx, w, Response{Status: "ok", Rows: ch, Total: 2})
// {"status":"ok","rows":[{"id":1},{"id":2}],"total":2}

The `,omitempty` tag option is honored using reflect.Value.IsZero. A nil channel emits an empty array.

type MarshalerOption

type MarshalerOption func(*Marshaler)

func WithMarshalerBufferSize

func WithMarshalerBufferSize(n int) MarshalerOption

type StreamReader

type StreamReader struct {
	// contains filtered or unexported fields
}

StreamReader pulls elements from a JSON array one at a time.

Usage:

r, err := jsonstreaming.NewStreamReader(src)
if err != nil { return err }
defer r.Close()
var row Row
for r.Next() {
    if err := r.Decode(&row); err != nil { return err }
    // use row; reuse the same pointer to keep allocations bounded
}
if err := r.Err(); err != nil { return err }

The reader materializes at most one element at a time. The decoder's internal buffer grows only to the size of the largest single element.

func NewStreamReader

func NewStreamReader(r io.Reader) (*StreamReader, error)

NewStreamReader wraps r and consumes the opening bracket of a JSON array. It returns an error if the top-level value is not an array.

func (*StreamReader) Close

func (sr *StreamReader) Close() error

Close does not close the underlying io.Reader; callers are responsible for that. It is provided so the iterator satisfies a Closer-style contract.

func (*StreamReader) Decode

func (sr *StreamReader) Decode(v any) error

Decode reads the current element into v. v should be a pointer. Reusing the same pointer across iterations keeps allocations bounded per element.

func (*StreamReader) Err

func (sr *StreamReader) Err() error

Err reports the first error encountered during iteration, if any.

func (*StreamReader) Index

func (sr *StreamReader) Index() int

Index reports how many elements have been decoded so far.

func (*StreamReader) Next

func (sr *StreamReader) Next() bool

Next advances to the next element. It returns false at end of array or on error; check Err to distinguish. Next must be called before the first Decode.

type StreamWriter

type StreamWriter struct {
	// contains filtered or unexported fields
}

StreamWriter serializes a JSON array to an io.Writer one element at a time.

func NewStreamWriter

func NewStreamWriter(w io.Writer, opts ...WriterOption) (*StreamWriter, error)

NewStreamWriter wraps w and writes the opening bracket of a JSON array. The internal buffer size defaults to DefaultBufferSize; override with WithBufferSize.

func (*StreamWriter) Abort

func (sw *StreamWriter) Abort(marker string) error

Abort terminates the stream by appending marker verbatim after the last written element (no closing bracket, no separator) and flushing the buffer downstream. Intended for the "producer cancelled mid-stream" case: the caller decides on a marker (e.g. "Timeout") and downstream consumers see a truncated array with a recognizable sentinel.

After Abort the writer is closed; subsequent SetRow calls return an error and Flush is a no-op. Abort itself is idempotent.

Example, after two rows:

sw.Abort("Timeout")  // output: [{...},{...}Timeout

func (*StreamWriter) Count

func (sw *StreamWriter) Count() int

Count reports how many elements have been written.

func (*StreamWriter) Flush

func (sw *StreamWriter) Flush() error

Flush writes the closing bracket and flushes the buffer downstream. It is safe (and idempotent) to call Flush multiple times; subsequent calls are no-ops after the first success.

func (*StreamWriter) SetRow

func (sw *StreamWriter) SetRow(v any) error

SetRow encodes v as the next element of the array.

v may be any type accepted by json.Marshal. Reusing the same pointer across calls keeps allocations bounded to the largest single element.

type WriterOption

type WriterOption func(*writerConfig)

WriterOption configures a StreamWriter.

func WithBufferSize

func WithBufferSize(n int) WriterOption

WithBufferSize overrides the flush buffer size.

func WithFields

func WithFields(names ...string) WriterOption

WithFields projects each SetRow value to only the named top-level JSON fields; every other field is dropped. Names must match the JSON key (i.e. the struct tag, or the field name if untagged).

Given a row {"id":1,"name":"a","secret":"x"} and WithFields("id","name"):

{"id":1,"name":"a"}

Selection is applied via goccy/go-json's FieldQuery. When set, SetRow uses json.MarshalContext under the hood, which allocates a bounded []byte per row — trading the writer's zero-alloc guarantee for projection.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL