Skip to content

cjson-go/cjson.go

:material-github: View on GitHub · 167 lines · live source, included at build time

// Package cjsongo provides JSON marshaling and unmarshaling helpers, including
// parallel processing of large JSON arrays. It is a port of the cJSON C library
// (github.com/DaveGamble/cJSON).
package cjsongo

import (
    "bytes"
    "encoding/json"
    "errors"
    "fmt"
    "io"
    "runtime"
    "sync"
)

// Unmarshal parses JSON data into the provided interface.
func Unmarshal(data []byte, v interface{}) error {
    if len(data) == 0 {
        return errors.New("empty JSON data")
    }
    if err := json.Unmarshal(data, v); err != nil {
        return fmt.Errorf("unmarshal error: %w", err)
    }
    return nil
}

// UnmarshalToMap parses JSON into a map.
func UnmarshalToMap(data []byte) (map[string]interface{}, error) {
    var m map[string]interface{}
    if err := Unmarshal(data, &m); err != nil {
        return nil, err
    }
    return m, nil
}

// UnmarshalToSlice parses JSON array into a slice.
func UnmarshalToSlice(data []byte) ([]interface{}, error) {
    var s []interface{}
    if err := Unmarshal(data, &s); err != nil {
        return nil, err
    }
    return s, nil
}

// Marshal serializes the provided value to JSON.
func Marshal(v interface{}) ([]byte, error) {
    data, err := json.Marshal(v)
    if err != nil {
        return nil, fmt.Errorf("marshal error: %w", err)
    }
    return data, nil
}

// MarshalIndent serializes with indentation for readability.
func MarshalIndent(v interface{}, prefix, indent string) ([]byte, error) {
    data, err := json.MarshalIndent(v, prefix, indent)
    if err != nil {
        return nil, fmt.Errorf("marshal indent error: %w", err)
    }
    return data, nil
}

// UnmarshalStream parses JSON from an io.Reader. It does not impose a size
// limit, so for untrusted input callers MUST wrap r in an io.LimitReader (or
// http.MaxBytesReader) to bound memory.
func UnmarshalStream(r io.Reader, v interface{}) error {
    decoder := json.NewDecoder(r)
    if err := decoder.Decode(v); err != nil {
        return fmt.Errorf("stream unmarshal error: %w", err)
    }
    return nil
}

// MarshalStream writes JSON to an io.Writer.
func MarshalStream(w io.Writer, v interface{}) error {
    encoder := json.NewEncoder(w)
    if err := encoder.Encode(v); err != nil {
        return fmt.Errorf("stream marshal error: %w", err)
    }
    return nil
}

// MaxArrayItems caps how many elements UnmarshalArrayParallel will process,
// guarding against memory-amplification (a tiny "[0,0,0,...]" body declares
// millions of elements, each eagerly committing slice/channel/map slots). The
// default is 1,048,576. Set it to 0 to disable the cap.
var MaxArrayItems = 1 << 20

// UnmarshalArrayParallel deserializes JSON array items in parallel.
// Useful for large arrays where each item can be processed independently.
//
// On a malformed item it returns a single error wrapping the first failure
// observed by the worker pool; which item that is, is nondeterministic.
func UnmarshalArrayParallel(data []byte) ([]map[string]interface{}, error) {
    // First unmarshal to raw array
    var rawArray []json.RawMessage
    if err := json.Unmarshal(data, &rawArray); err != nil {
        return nil, fmt.Errorf("failed to parse array: %w", err)
    }

    if MaxArrayItems > 0 && len(rawArray) > MaxArrayItems {
        return nil, fmt.Errorf("array has %d items, exceeding the %d-item limit (adjust MaxArrayItems)",
            len(rawArray), MaxArrayItems)
    }

    if len(rawArray) == 0 {
        return []map[string]interface{}{}, nil
    }

    // Process items in parallel
    numWorkers := runtime.NumCPU()
    if len(rawArray) < numWorkers {
        numWorkers = len(rawArray)
    }

    results := make([]map[string]interface{}, len(rawArray))
    errs := make(chan error, numWorkers)
    jobs := make(chan int, len(rawArray))

    var wg sync.WaitGroup

    // Send jobs
    for i := range rawArray {
        jobs <- i
    }
    close(jobs)

    // Start workers
    for i := 0; i < numWorkers; i++ {
        wg.Add(1)
        go func() {
            defer wg.Done()
            for idx := range jobs {
                var item map[string]interface{}
                if err := json.Unmarshal(rawArray[idx], &item); err != nil {
                    errs <- fmt.Errorf("failed to unmarshal item %d: %w", idx, err)
                    return
                }
                results[idx] = item
            }
        }()
    }

    wg.Wait()
    close(errs)

    // Check for errors
    if err := <-errs; err != nil {
        return nil, err
    }

    return results, nil
}

// Valid checks if the data is valid JSON without fully parsing it.
func Valid(data []byte) bool {
    return json.Valid(data)
}

// Compact removes insignificant whitespace from JSON.
func Compact(data []byte) ([]byte, error) {
    var buf bytes.Buffer
    if err := json.Compact(&buf, data); err != nil {
        return nil, fmt.Errorf("compact error: %w", err)
    }
    return buf.Bytes(), nil
}