You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

307 lines
9.0 KiB

# jx [![](https://img.shields.io/badge/go-pkg-00ADD8)](https://pkg.go.dev/github.com/go-faster/jx#section-documentation) [![](https://img.shields.io/codecov/c/github/go-faster/jx?label=cover)](https://codecov.io/gh/go-faster/jx) [![experimental](https://img.shields.io/badge/-experimental-blueviolet)](https://go-faster.org/docs/projects/status#experimental)
Package jx implements encoding and decoding of json [[RFC 7159](https://www.rfc-editor.org/rfc/rfc7159.html)].
Lightweight fork of [jsoniter](https://github.com/json-iterator/go).
```console
go get github.com/go-faster/jx
```
* [Usage and examples](#usage)
* [Roadmap](#roadmap)
* [Non-goals](#non-goals)
## Features
* Directly encode and decode json values
* No reflect or `interface{}`
* Pools and direct buffer access for less (or none) allocations
* Multi-pass decoding
* Validation
See [usage](#Usage) for examples. Mostly suitable for fast low-level json manipulation
with high control. Used in [ogen](https://github.com/ogen-go/ogen) project for
json (un)marshaling code generation based on json and OpenAPI schemas.
For example, we have following OpenTelemetry log entry:
```json
{
"Timestamp": "1586960586000000000",
"Attributes": {
"http.status_code": 500,
"http.url": "http://example.com",
"my.custom.application.tag": "hello"
},
"Resource": {
"service.name": "donut_shop",
"service.version": "2.0.0",
"k8s.pod.uid": "1138528c-c36e-11e9-a1a7-42010a800198"
},
"TraceId": "13e2a0921288b3ff80df0a0482d4fc46",
"SpanId": "43222c2d51a7abe3",
"SeverityText": "INFO",
"SeverityNumber": 9,
"Body": "20200415T072306-0700 INFO I like donuts"
}
```
Flexibility of `jx` enables highly efficient semantic-aware encoding and decoding,
e.g. using `[16]byte` for `TraceId` with zero-allocation `hex` encoding in json:
| Name | Speed | Allocations |
|----------|-----------|-------------|
| Decode | 970 MB/s | 0 allocs/op |
| Validate | 1535 MB/s | 0 allocs/op |
| Encode | 1104 MB/s | 0 allocs/op |
| Write | 2146 MB/s | 0 allocs/op |
See [otel_test.go](./otel_test.go) for example.
## Why
Most of [jsoniter](https://github.com/json-iterator/go) issues are caused by necessity
to be drop-in replacement for standard `encoding/json`. Removing such constrains greatly
simplified implementation and reduced scope, allowing to focus on json stream processing.
* Commas are handled automatically while encoding
* Raw json, Number and Base64 support
* Reduced scope
* No reflection
* No `encoding/json` adapter
* 3.5x less code (8.5K to 2.4K SLOC)
* Fuzzing, improved test coverage
* Drastically refactored and simplified
* Explicit error returns
* No `Config` or `API`
## Usage
* [Decoding](#decode)
* [Encoding](#encode)
* [Writer](#writer)
* [Raw message](#raw)
* [Number](#number)
* [Base64](#base64)
* [Validation](#validate)
* [Multi pass decoding](#capture)
### Decode
Use [jx.Decoder](https://pkg.go.dev/github.com/go-faster/jx#Decoder). Zero value is valid,
but constructors are available for convenience:
* [jx.Decode(reader io.Reader, bufSize int)](https://pkg.go.dev/github.com/go-faster/jx#Decode) for `io.Reader`
* [jx.DecodeBytes([]byte)](https://pkg.go.dev/github.com/go-faster/jx#Decode) for byte slices
* [jx.DecodeStr(string)](https://pkg.go.dev/github.com/go-faster/jx#Decode) for strings
To reuse decoders and their buffers, use [jx.GetDecoder](https://pkg.go.dev/github.com/go-faster/jx#GetDecoder)
and [jx.PutDecoder](https://pkg.go.dev/github.com/go-faster/jx#PutDecoder) alongside with reset functions:
* [jx.Decoder.Reset(io.Reader)](https://pkg.go.dev/github.com/go-faster/jx#Decoder.Reset) to reset to new `io.Reader`
* [jx.Decoder.ResetBytes([]byte)](https://pkg.go.dev/github.com/go-faster/jx#Decoder.ResetBytes) to decode another byte slice
Decoder is reset on `PutDecoder`.
```go
d := jx.DecodeStr(`{"values":[4,8,15,16,23,42]}`)
// Save all integers from "values" array to slice.
var values []int
// Iterate over each object field.
if err := d.Obj(func(d *jx.Decoder, key string) error {
switch key {
case "values":
// Iterate over each array element.
return d.Arr(func(d *jx.Decoder) error {
v, err := d.Int()
if err != nil {
return err
}
values = append(values, v)
return nil
})
default:
// Skip unknown fields if any.
return d.Skip()
}
}); err != nil {
panic(err)
}
fmt.Println(values)
// Output: [4 8 15 16 23 42]
```
### Encode
Use [jx.Encoder](https://pkg.go.dev/github.com/go-faster/jx#Encoder). Zero value is valid, reuse with
[jx.GetEncoder](https://pkg.go.dev/github.com/go-faster/jx#GetEncoder),
[jx.PutEncoder](https://pkg.go.dev/github.com/go-faster/jx#PutEncoder) and
[jx.Encoder.Reset()](https://pkg.go.dev/github.com/go-faster/jx#Encoder.Reset). Encoder is reset on `PutEncoder`.
```go
var e jx.Encoder
e.ObjStart() // {
e.FieldStart("values") // "values":
e.ArrStart() // [
for _, v := range []int{4, 8, 15, 16, 23, 42} {
e.Int(v)
}
e.ArrEnd() // ]
e.ObjEnd() // }
fmt.Println(e)
fmt.Println("Buffer len:", len(e.Bytes()))
// Output: {"values":[4,8,15,16,23,42]}
// Buffer len: 28
```
### Writer
Use [jx.Writer](https://pkg.go.dev/github.com/go-faster/jx#Writer) for low level json writing.
No automatic commas or indentation for lowest possible overhead, useful for code generated json encoding.
### Raw
Use [jx.Decoder.Raw](https://pkg.go.dev/github.com/go-faster/jx#Decoder.Raw) to read raw json values, similar to `json.RawMessage`.
```go
d := jx.DecodeStr(`{"foo": [1, 2, 3]}`)
var raw jx.Raw
if err := d.Obj(func(d *jx.Decoder, key string) error {
v, err := d.Raw()
if err != nil {
return err
}
raw = v
return nil
}); err != nil {
panic(err)
}
fmt.Println(raw.Type(), raw)
// Output:
// array [1, 2, 3]
```
### Number
Use [jx.Decoder.Num](https://pkg.go.dev/github.com/go-faster/jx#Decoder.Num) to read numbers, similar to `json.Number`.
Also supports number strings, like `"12345"`, which is common compatible way to represent `uint64`.
```go
d := jx.DecodeStr(`{"foo": "10531.0"}`)
var n jx.Num
if err := d.Obj(func(d *jx.Decoder, key string) error {
v, err := d.Num()
if err != nil {
return err
}
n = v
return nil
}); err != nil {
panic(err)
}
fmt.Println(n)
fmt.Println("positive:", n.Positive())
// Can decode floats with zero fractional part as integers:
v, err := n.Int64()
if err != nil {
panic(err)
}
fmt.Println("int64:", v)
// Output:
// "10531.0"
// positive: true
// int64: 10531
```
### Base64
Use [jx.Encoder.Base64](https://pkg.go.dev/github.com/go-faster/jx#Encoder.Base64) and
[jx.Decoder.Base64](https://pkg.go.dev/github.com/go-faster/jx#Decoder.Base64) or
[jx.Decoder.Base64Append](https://pkg.go.dev/github.com/go-faster/jx#Decoder.Base64Append).
Same as encoding/json, base64.StdEncoding or [[RFC 4648](https://www.rfc-editor.org/rfc/rfc4648.html)].
```go
var e jx.Encoder
e.Base64([]byte("Hello"))
fmt.Println(e)
data, _ := jx.DecodeBytes(e.Bytes()).Base64()
fmt.Printf("%s", data)
// Output:
// "SGVsbG8="
// Hello
```
### Validate
Check that byte slice is valid json with [jx.Valid](https://pkg.go.dev/github.com/go-faster/jx#Valid):
```go
fmt.Println(jx.Valid([]byte(`{"field": "value"}`))) // true
fmt.Println(jx.Valid([]byte(`"Hello, world!"`))) // true
fmt.Println(jx.Valid([]byte(`["foo"}`))) // false
```
### Capture
The [jx.Decoder.Capture](https://pkg.go.dev/github.com/go-faster/jx#Decoder.Capture) method allows to unread everything is read in callback.
Useful for multi-pass parsing:
```go
d := jx.DecodeStr(`["foo", "bar", "baz"]`)
var elems int
// NB: Currently Capture does not support io.Reader, only buffers.
if err := d.Capture(func(d *jx.Decoder) error {
// Everything decoded in this callback will be rolled back.
return d.Arr(func(d *jx.Decoder) error {
elems++
return d.Skip()
})
}); err != nil {
panic(err)
}
// Decoder is rolled back to state before "Capture" call.
fmt.Println("Read", elems, "elements on first pass")
fmt.Println("Next element is", d.Next(), "again")
// Output:
// Read 3 elements on first pass
// Next element is array again
```
### ObjBytes
The `Decoder.ObjBytes` method tries not to allocate memory for keys, reusing existing buffer.
```go
d := DecodeStr(`{"id":1,"randomNumber":10}`)
d.ObjBytes(func(d *Decoder, key []byte) error {
switch string(key) {
case "id":
case "randomNumber":
}
return d.Skip()
})
```
## Roadmap
- [ ] Rework and export `Any`
- [ ] Support `Raw` for io.Reader
- [x] Support `Capture` for io.Reader
- [ ] Improve Num
- Better validation on decoding
- Support BigFloat and BigInt
- Support equivalence check, like `eq(1.0, 1) == true`
- [ ] Add non-callback decoding of objects
## Non-goals
* Code generation for decoding or encoding
* Replacement for `encoding/json`
* Reflection or `interface{}` based encoding or decoding
* Support for json path or similar
This package should be kept as simple as possible and be used as
low-level foundation for high-level projects like code generator.
## License
MIT, same as jsoniter