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
307 lines
9.0 KiB
3 years ago
|
# 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
|