jsn

package module
v0.1.0 Latest Latest
Warning

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

Go to latest
Published: Nov 28, 2024 License: MIT Imports: 9 Imported by: 0

README

JSN

Go Reference CI codecov

A high-performance JSON serialization package for Go. JSN offers both a functional-style API and reflection-based marshaling for flexible and efficient JSON generation and parsing.

Features

  • High Performance: Allows to reduce memory allocations for both reading and writing, making it suitable for performance-sensitive applications.

  • Flexible Reading API: Process JSON using callbacks, maps, slices, or a mix of approaches for optimal control and efficiency. No struct tags are needed.

  • Flexible Writing API: Fine-grained control over JSON structure through a callback-based approach or custom type marshaling.

  • Configurable Float Precision: Control the precision of floating-point number formatting

Reading JSON

JSN reads values through a Scanner object that takes input buffer and optional flags:

scanner := jsn.NewScanner(buffer, /*<options>...*/)

By default, the scanner skips the BOM and initial whitespace, which can be disabled using the following options:

Flags:

  • jsn.ScannerFlagDoNotSkipBOM - Do not skip the BOM at the start of the buffer
  • jsn.ScannerFlagDoNotSkipInitialWhitespace - Do not skip initial whitespace at the start of the buffer

JSN provides two approaches to reading JSON:

  1. Direct value reading - returns parsed values:
// Read any JSON value:
value, err := jsn.ReadValue(scanner)    // returns any

// Read a JSON object:
input := `{
    "name": "John",
    "age": 30,
    "address": {
        "city": "New York",
        "location": {"lat": 40.7128, "lon": -74.0060}
    }
}`
obj, err := jsn.ReadObject(scanner)     // returns map[string]any

// Read a JSON array:
input := `[
    42,
    "hello",
    true,
    {"key": "value"},
    [1, 2, 3]
]`
arr, err := jsn.ReadArray(scanner)      // returns []any
  1. Callback-based reading - for memory-efficient processing:
// Process object fields selectively:
input := `{
    "name": "John",
    "age": 30,
    "address": {
        "city": "New York",
        "location": {"lat": 40.7128, "lon": -74.0060}
    }
}`
err := jsn.ReadObjectCallback(scanner, func(key string, value any) error {
    switch key {
    case "name":
        fmt.Printf("name: %v\n", value)
    case "address":
        if addr, ok := value.(map[string]any); ok {
            if loc, ok := addr["location"].(map[string]any); ok {
                fmt.Printf("coordinates: %v,%v\n", loc["lat"], loc["lon"])
            }
        }
    }
    return nil
})

// Filter array elements by type:
input := `[
    {"type": "user", "name": "John"},
    {"type": "order", "id": "A123"}
]`
err := jsn.ReadArrayCallback(scanner, func(value any) error {
    if obj, ok := value.(map[string]any); ok {
        switch obj["type"] {
        case "user":
            fmt.Printf("Found user: %v\n", obj["name"])
        case "order":
            fmt.Printf("Found order: %v\n", obj["id"])
        }
    }
    return nil
})

Example of direct reading:

func main() {
    // ReadValue can parse any JSON value directly
    inputs := []string{
        `42`,
        `3.14159`,
        `"hello"`,
        `true`,
        `[1,2,3]`,
        `{"name":"John"}`,
    }

    for _, input := range inputs {
        scanner := jsn.NewScanner([]byte(input))
        value, err := jsn.ReadValue(scanner)
        if err != nil {
            // Handle error
            continue
        }
        // Values are automatically converted to appropriate Go types:
        // 42 -> float64: 42
        // 3.14159 -> float64: 3.14159
        // "hello" -> string: hello
        // true -> bool: true
        // [1,2,3] -> []interface {}: [1 2 3]
        // {"name":"John"} -> map[string]interface {}: map[name:John]
    }
}

Writing JSON

The package provides flexible ways to write JSON through the Marshal function and custom marshalers.

Supported Types

JSN supports direct marshaling of the following Go types without requiring custom marshalers:

Basic Types:

  • bool - Marshaled as JSON boolean
  • string - Marshaled as JSON string
  • All numeric types (int, int8...int64, uint...uint64, float32, float64) - Marshaled as JSON numbers
  • Custom types based on basic types (e.g., type MyInt int) - Automatically marshaled as their underlying type
  • It is also possible to customize marshaling for basic types using the StrMarshaler interface.

Collection Types:

  • []T where T is any supported type - Marshaled as JSON arrays
  • map[string]T where T is any supported type - Marshaled as JSON objects
  • []byte and [N]byte - Marshaled as JSON strings

Special Types:

  • nil - Marshaled as JSON null
  • any (interface{}) containing any supported type

Callback Types:

  • func(ArrayWriter) - Marshaled as JSON arrays
  • func(ArrayWriter) error - Marshaled as JSON arrays
  • func(ObjectWriter) - Marshaled as JSON objects
  • func(ObjectWriter) error - Marshaled as JSON objects

For other types (like structs), you need to implement one of the marshaler interfaces:

  • ObjMarshaler for types that should be marshaled as JSON objects
  • ArrMarshaler for types that should be marshaled as JSON arrays
  • StrMarshaler for types that should be marshaled as JSON strings
  • Types implementing encoding.TextMarshaler are supported and marshaled as strings.
Basic Usage

JSN supports direct marshaling of primitive types and collections:

// Marshal primitive types
str, _ := jsn.Marshal("hello")     // "hello"
num, _ := jsn.Marshal(42)          // 42
arr, _ := jsn.Marshal([]int{1,2})  // [1,2]

// Control float precision
pi, _ := jsn.Marshal(3.14159, jsn.FloatPrecision{Precision: 3})  // 3.14
Custom Marshalers

Three interfaces are available for custom JSON serialization:

// For custom string values
type StrMarshaler interface {
    MarshalJSN() (string, error)
}

// For custom array values
type ArrMarshaler interface {
    MarshalJSN(w ArrayWriter) error
}

// For custom object values
type ObjMarshaler interface {
    MarshalJSN(w ObjectWriter) error
}

Example usage:

type Person struct {
    name string
    age  int
}

func (p Person) MarshalJSN(w jsn.ObjectWriter) error {
    w.Member("name", p.name)
    w.Member("age", p.age)
    return nil
}

person := Person{name: "John", age: 30}
result, _ := jsn.Marshal(person)  // {"name":"John","age":30}
Functional Writing

The package supports a functional approach to writing JSON:

// Write arrays using functions
writeNumbers := func(w jsn.ArrayWriter) error {
    w.Element(1)
    w.Element(2)
    return nil
}
result, _ := jsn.Marshal(writeNumbers)  // [1,2]

// Write objects using functions
writePerson := func(w jsn.ObjectWriter) error {
    w.Member("name", "John")
    w.Member("hobbies", []string{"reading"})
    return nil
}
result, _ := jsn.Marshal(writePerson)  // {"name":"John","hobbies":["reading"]}
Nested Structures

Writers can be nested to create complex JSON structures. Here's an example of multi-level functional writing:

result, _ := jsn.Marshal(func(w jsn.ObjectWriter) error {
    w.Member("name", "John")
    w.Member("address", func(w jsn.ObjectWriter) error {
        w.Member("street", "123 Main St")
        w.Member("city", "Springfield")
        return nil
    })
    w.Member("hobbies", func(w jsn.ArrayWriter) error {
        w.Element("reading")
        w.Element("coding")
        return nil
    })
    w.Member("scores", map[string]int{
        "math":    95,
        "english": 87,
    })
    return nil
})
// Output: {"name":"John","address":{"street":"123 Main St","city":"Springfield"},"hobbies":["reading","coding"],"scores":{"english":87,"math":95}}

Documentation

Index

Examples

Constants

This section is empty.

Variables

View Source
var (
	ErrUnexpectedToken        = errors.New("unexpected token")
	ErrUnexpectedEOF          = errors.New("unexpected EOF")
	ErrInvalidNumber          = errors.New("invalid number")
	ErrInvalidString          = errors.New("invalid string")
	ErrInvalidUnicodeEscape   = errors.New("invalid unicode escape")
	ErrNumericValueOutOfRange = errors.New("numeric value out of range")
)

Functions

func Marshal

func Marshal(v any, opts ...any) (string, error)

Marshal marshals any supported value into a JSON string.

Example (Array)
// Marshal arrays and slices
numbers := []int{1, 2, 3}
result, _ := Marshal(numbers)
fmt.Println(result)
Output:

[1,2,3]
Example (CustomObject)
// Marshal custom object
person := Person{name: "John", age: 30}
result, _ := Marshal(person)
fmt.Println(result)
Output:

{"name":"John","age":30}
Example (FloatPrecision)
// Control float precision
pi := 3.14159265359
result, _ := Marshal(pi, FloatPrecision{Precision: 3})
fmt.Println(result)
Output:

3.14
Example (FunctionalArray)
result, _ := Marshal(func(w ArrayWriter) error {
	w.Element(1)
	w.Element(2)
	w.Element(3)
	return nil
})
fmt.Println(result)
Output:

[1,2,3]
Example (FunctionalNested)
result, _ := Marshal(func(w ObjectWriter) error {
	w.Member("name", "John")
	w.Member("address", func(w ObjectWriter) error {
		w.Member("street", "123 Main St")
		w.Member("city", "Springfield")
		return nil
	})
	w.Member("hobbies", func(w ArrayWriter) error {
		w.Element("reading")
		w.Element("coding")
		return nil
	})
	w.Member("scores", map[string]int{
		"math":    95,
		"english": 87,
	})
	return nil
})
fmt.Println(result)
Output:

{"name":"John","address":{"street":"123 Main St","city":"Springfield"},"hobbies":["reading","coding"],"scores":{"english":87,"math":95}}
Example (FunctionalObject)
result, _ := Marshal(func(w ObjectWriter) error {
	w.Member("name", "John")
	w.Member("age", 30)
	w.Member("hobbies", []string{"reading", "coding"})
	return nil
})
fmt.Println(result)
Output:

{"name":"John","age":30,"hobbies":["reading","coding"]}
Example (Nested)
container := ArrayContainer{data: []int{1, 2}}
nested := []any{container, "str", 42}
result, _ := Marshal(nested)
fmt.Println(result)
Output:

[[1,2],"str",42]
Example (Primitives)
// Marshal primitive types
fmt.Println(Marshal("hello")) // string
fmt.Println(Marshal(42))      // int
fmt.Println(Marshal(3.14))    // float
fmt.Println(Marshal(true))    // bool
fmt.Println(Marshal(nil))     // null
Output:

"hello" <nil>
42 <nil>
3.14 <nil>
true <nil>
null <nil>

func ReadArray

func ReadArray(s *Scanner) ([]any, error)

ReadArray reads a JSON array and returns it as []any

Example (Direct)
input := `[
		42,
		"hello",
		true,
		{"key": "value"},
		[1, 2, 3]
	]`

scanner := NewScanner([]byte(input))
arr, err := ReadArray(scanner)
if err != nil {
	fmt.Printf("error: %v\n", err)
	return
}

fmt.Printf("%#v\n", arr)
Output:

[]interface {}{42, "hello", true, map[string]interface {}{"key":"value"}, []interface {}{1, 2, 3}}

func ReadArrayCallback

func ReadArrayCallback(s *Scanner, callback func(any) error) error

ReadArrayCallback reads a JSON array and invokes the callback function for each element. This allows for memory-efficient processing of arrays without storing the entire structure.

Example:

err := ReadArrayCallback(scanner, func(value any) error {
    fmt.Printf("value: %v\n", value)
    return nil
})
Example
input := `[
		{"type": "user", "name": "John"},
		{"type": "order", "id": "A123"},
		{"type": "user", "name": "Jane"},
		{"type": "order", "id": "B456"}
	]`

scanner := NewScanner([]byte(input))
err := ReadArrayCallback(scanner, func(value any) error {
	// Type-check and process each array element
	if obj, ok := value.(map[string]any); ok {
		switch obj["type"] {
		case "user":
			fmt.Printf("Found user: %v\n", obj["name"])
		case "order":
			fmt.Printf("Found order: %v\n", obj["id"])
		}
	}
	return nil
})
if err != nil {
	fmt.Printf("error: %v\n", err)
}
Output:

Found user: John
Found order: A123
Found user: Jane
Found order: B456

func ReadObject

func ReadObject(s *Scanner) (map[string]any, error)

ReadObject reads a JSON object and returns it as map[string]any

Example (Direct)
input := `{
		"name": "John",
		"age": 30,
		"hobbies": ["reading", "music"],
		"address": {
			"city": "New York",
			"zip": "10001"
		}
	}`

scanner := NewScanner([]byte(input))
obj, err := ReadObject(scanner)
if err != nil {
	fmt.Printf("error: %v\n", err)
	return
}

fmt.Printf("%#v\n", obj)
Output:

map[string]interface {}{"address":map[string]interface {}{"city":"New York", "zip":"10001"}, "age":30, "hobbies":[]interface {}{"reading", "music"}, "name":"John"}

func ReadObjectCallback

func ReadObjectCallback(s *Scanner, callback func(k string, v any) error) error

ReadObjectCallback reads a JSON object and invokes the callback function for each key-value pair. The callback receives the key as a string and the value as an interface{}. This allows for memory-efficient processing of JSON objects without storing the entire structure.

Example:

err := ReadObjectCallback(scanner, func(key string, value any) error {
    if key == "name" {
        fmt.Printf("name: %v\n", value)
    }
    return nil
})
Example
input := `{
		"name": "John",
		"age": 30,
		"address": {
			"city": "New York",
			"zip": "10001",
			"location": {
				"lat": 40.7128,
				"lon": -74.0060
			}
		},
		"orders": [
			{"id": "A123", "total": 50.00},
			{"id": "B456", "total": 30.00}
		]
	}`

scanner := NewScanner([]byte(input))
err := ReadObjectCallback(scanner, func(key string, value any) error {
	switch key {
	case "name", "age":
		fmt.Printf("%s: %v\n", key, value)

	case "address":
		// Handle nested object
		if addr, ok := value.(map[string]any); ok {
			fmt.Printf("city: %v\n", addr["city"])
			// Handle deeply nested object
			if loc, ok := addr["location"].(map[string]any); ok {
				fmt.Printf("coordinates: %v,%v\n", loc["lat"], loc["lon"])
			}
		}

	case "orders":
		// Handle array of objects
		if orders, ok := value.([]any); ok {
			for _, order := range orders {
				if o, ok := order.(map[string]any); ok {
					fmt.Printf("order: %v = $%v\n", o["id"], o["total"])
				}
			}
		}
	}
	return nil
})
if err != nil {
	fmt.Printf("error: %v\n", err)
}
Output:

name: John
age: 30
city: New York
coordinates: 40.7128,-74.006
order: A123 = $50
order: B456 = $30

func ReadValue

func ReadValue(s *Scanner) (any, error)

ReadValue reads any JSON value and returns it as a Go value. The mapping of JSON types to Go types is as follows:

  • JSON null -> nil
  • JSON boolean -> bool
  • JSON number -> float64
  • JSON string -> string
  • JSON array -> []any
  • JSON object -> map[string]any

This function is recursive and will handle nested structures of any depth, limited only by available stack space.

Example
// ReadValue can parse any JSON value directly
inputs := []string{
	`42`,
	`3.14159`,
	`"hello"`,
	`true`,
	`[1,2,3]`,
	`{"name":"John"}`,
}

for _, input := range inputs {
	scanner := NewScanner([]byte(input))
	value, err := ReadValue(scanner)
	if err != nil {
		fmt.Printf("error: %v\n", err)
		continue
	}
	fmt.Printf("%T: %v\n", value, value)
}
Output:

float64: 42
float64: 3.14159
string: hello
bool: true
[]interface {}: [1 2 3]
map[string]interface {}: map[name:John]

Types

type ArrMarshaler

type ArrMarshaler interface {
	MarshalJSN(w ArrayWriter) error
}

ArrMarshaler is implemented by types that can marshal themselves into a JSON array. This interface provides full control over the array's JSON representation.

type ArrayWriter

type ArrayWriter interface {
	// Element writes supported value as an array element.
	Element(v any)
}

ArrayWriter defines the interface for writing JSON arrays

type FloatPrecision

type FloatPrecision struct {
	Precision int
}

FloatPrecision specifies the number of decimal places to use when formatting floating-point numbers

type ObjMarshaler

type ObjMarshaler interface {
	MarshalJSN(w ObjectWriter) error
}

ObjMarshaler is implemented by types that can marshal themselves into a JSON object. This interface provides full control over the object's JSON representation.

type ObjectWriter

type ObjectWriter interface {
	// Member writes a key-value pair as an object member.
	Member(key string, v any)
}

ObjectWriter defines the interface for writing JSON objects

type Scanner

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

Scanner is a simple parser for JSON data

func NewScanner

func NewScanner(data []byte, opts ...any) *Scanner

NewScanner creates a new scanner and skips the BOM and optional whitespace at the start of the data

func (*Scanner) Finalize

func (s *Scanner) Finalize() error

Finalize ensures that the scanner has consumed all input

func (*Scanner) IsEOF

func (s *Scanner) IsEOF() bool

IsEOF returns true if the scanner has reached the end of input

func (*Scanner) SkipBOM

func (s *Scanner) SkipBOM() bool

SkipBOM skips the UTF-8 Byte Order Mark (BOM) if present at the start of the data

type ScannerFlag

type ScannerFlag int
const (
	ScannerFlagDoNotSkipBOM ScannerFlag = 1 << iota
	ScannerFlagDoNotSkipInitialWhitespace
)

type StrMarshaler

type StrMarshaler interface {
	MarshalJSN() (string, error)
}

StrMarshaler is implemented by types that can marshal themselves into a JSON string value. This is useful for types that need custom string representation in JSON.

type UnsupportedTypeError

type UnsupportedTypeError struct {
	Type reflect.Type
}

UnsupportedTypeError is returned when marshaling encounters a type that cannot be converted into JSON.

func (*UnsupportedTypeError) Error

func (e *UnsupportedTypeError) Error() string

Jump to

Keyboard shortcuts

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