Git Product home page Git Product logo

jettison's Introduction

Jettison

GoCaptain

Jettison is a fast and flexible JSON encoder for the Go programming language, inspired by bet365/jingo, with a richer features set, aiming at 100% compatibility with the standard library.



Installation

Jettison uses Go modules. Releases are tagged according to the SemVer format, prefixed with a v, starting from 0.2.0. You can get the latest release using the following command.

$ go get github.com/wI2L/jettison@latest

⚠️ From version v0.7.4, the package requires Go 1.17+ to build, due to the usage of the new build constraints.

Key features

  • Fast, see benchmarks
  • No dynamic memory allocations in hot paths
  • Behavior identical to the standard library by default
  • No code generation required
  • Clear and concise API
  • Configurable with opt-in functional options
  • Native support for many standard library types, see improvements
  • Custom AppendMarshaler interface to avoid allocations
  • Extensive testsuite that compares its output against encoding/json

Overview

The goal of Jettision is to take up the idea introduced by the bet365/jingo package and build a fully-featured JSON encoder around it, that comply with the behavior of the encoding/json package. Unlike the latter, Jettison does not use reflection during marshaling, but only once to create the instruction set for a given type ahead of time. The drawback to this approach requires to instantiate an instruction-set once for each type that needs to be marshaled, but that is overcomed with a package cache.

The package aims to have a behavior similar to that of the standard library for all types encoding and struct tags, meaning that the documentation of the json.Marshal function is applicable for Jettison, with a few exceptions described in this section. As such, most of the tests compare their output against it to guarantee that.

Implementation details

The main concept of Jettison consists of using pre-build instructions-set to reduce the cost of using the reflect package at runtime. When marshaling a value, a set of instructions is recursively generated for its type, which defines how to iteratively encode it. An instruction is a function or a closure, that have all the information required to read the data from memory using unsafe operations (pointer type conversion, arithmetic...) during the instruction set execution.

Differences with encoding/json

All notable differences with the standard library behavior are listed below. Please note that these might evolve with future versions of the package.

Improvements

  • The time.Time and time.Duration types are handled natively. For time values, the encoder doesn't invoke MarshalJSON or MarshalText, but use the time.AppendFormat function instead, and write the result to the stream. Similarly, for durations, it isn't necessary to implements the json.Marshaler or encoding.TextMarshaler interfaces on a custom wrapper type, the encoder uses the result of one of the methods Minutes, Seconds, Nanoseconds or String, based on the duration format configured.

  • The sync.Map type is handled natively. The marshaling behavior is similar to the one of a standard Go map. The option UnsortedMap can also be used in cunjunction with this type to disable the default keys sort.

  • The omitnil field tag's option can be used to specify that a field with a nil pointer should be omitted from the encoding. This option has precedence over the omitempty option. Note that struct fields that implement the json.Marshaler interface will be omitted too, if they return the literal JSON null value.

Bugs

Go1.13 and backward
  • Nil map keys values implementing the encoding.TextMarshaler interface are encoded as empty strings, while the encoding/json package currently panic because of that. See this issue for more details.[1]

  • Nil struct fields implementing the encoding.TextMarshaler interface are encoded as null, while the encoding/json package currently panic because of that. See this issue for more details.[1]

1: The issues mentioned above have had their associated CL merged, and was released with Go 1.14.

Usage

Basic

As stated above, the library behave similarly to the encoding/json package. You can simply replace the json.Marshal function with jettison.Marshal, and expect the same output with better performances.

type X struct {
   A string `json:"a"`
   B int64  `json:"b"`
}
b, err := jettison.Marshal(X{
   A: "Loreum",
   B: 42,
})
if err != nil {
   log.Fatal(err)
}
os.Stdout.Write(b)
Result
{"a":"Loreum","b":42}

Advanced

If more control over the encoding behavior is required, use the MarshalOpts function instead. The second parameter is variadic and accept a list of functional opt-in options described below:

name description
TimeLayout Defines the layout used to encode time.Time values. The layout must be compatible with the AppendFormat method.
DurationFormat Defines the format used to encode time.Duration values. See the documentation of the DurationFmt type for the complete list of formats available.
UnixTime Encode time.Time values as JSON numbers representing Unix timestamps, the number of seconds elapsed since January 1, 1970 UTC. This option has precedence over TimeLayout.
UnsortedMap Disables map keys sort.
ByteArrayAsString Encodes byte arrays as JSON strings rather than JSON arrays. The output is subject to the same escaping rules used for JSON strings, unless the option NoStringEscaping is used.
RawByteSlice Disables the base64 default encoding used for byte slices.
NilMapEmpty Encodes nil Go maps as empty JSON objects rather than null.
NilSliceEmpty Encodes nil Go slices as empty JSON arrays rather than null.
NoStringEscaping Disables string escaping. NoHTMLEscaping and NoUTF8Coercion are ignored when this option is used.
NoHTMLEscaping Disables the escaping of special HTML characters such as &, < and > in JSON strings. This is similar to json.Encoder.SetEscapeHTML(false).
NoUTF8Coercion Disables the replacement of invalid bytes with the Unicode replacement rune in JSON strings.
AllowList Sets a whitelist that represents which fields are to be encoded when marshaling a Go struct.
DenyList Sets a blacklist that represents which fields are ignored during the marshaling of a Go struct.
NoCompact Disables the compaction of JSON output produced by MarshalJSON method, and json.RawMessage values.
NoNumberValidation Disables the validation of json.Number values.
WithContext Sets the context.Context to be passed to invocations of AppendJSONContext methods.

Take a look at the examples to see these options in action.

Benchmarks

If you'd like to run the benchmarks yourself, use the following command.

go get github.com/cespare/prettybench
go test -bench=. | prettybench

Results -short

These benchmarks were run 10x (statistics computed with benchstat) on a MacBook Pro 15", with the following specs:

OS:  macOS Catalina (10.15.7)
CPU: 2.6 GHz Intel Core i7
Mem: 16GB
Go:  go version go1.17 darwin/amd64
Tag: v0.7.2
Stats
name                    time/op
Simple/standard-8          573ns ± 1%
Simple/jsoniter-8          547ns ± 0%
Simple/segmentj-8          262ns ± 1%
Simple/jettison-8          408ns ± 1%
Complex/standard-8        11.7µs ± 0%
Complex/jsoniter-8        11.6µs ± 1%
Complex/segmentj-8        7.96µs ± 0%
Complex/jettison-8        5.90µs ± 1%
CodeMarshal/standard-8    6.71ms ± 0%
CodeMarshal/jsoniter-8    6.35ms ± 1%
CodeMarshal/segmentj-8    4.38ms ± 1%
CodeMarshal/jettison-8    5.56ms ± 1%
Map/standard-8            1.83µs ± 1%
Map/jsoniter-8            1.65µs ± 0%
Map/segmentj-8            1.61µs ± 0%
Map/jettison-8             772ns ± 1%
Map/jettison-nosort-8      507ns ± 1%

name speed Simple/standard-8 236MB/s ± 1% Simple/jsoniter-8 247MB/s ± 0% Simple/segmentj-8 516MB/s ± 1% Simple/jettison-8 331MB/s ± 1% Complex/standard-8 72.9MB/s ± 0% Complex/jsoniter-8 70.6MB/s ± 0% Complex/segmentj-8 108MB/s ± 0% Complex/jettison-8 144MB/s ± 1% CodeMarshal/standard-8 289MB/s ± 0% CodeMarshal/jsoniter-8 306MB/s ± 1% CodeMarshal/segmentj-8 443MB/s ± 1% CodeMarshal/jettison-8 349MB/s ± 1% Map/standard-8 46.6MB/s ± 1% Map/jsoniter-8 51.5MB/s ± 0% Map/segmentj-8 52.8MB/s ± 0% Map/jettison-8 110MB/s ± 1% Map/jettison-nosort-8 168MB/s ± 1%

name alloc/op Simple/standard-8 144B ± 0% Simple/jsoniter-8 152B ± 0% Simple/segmentj-8 144B ± 0% Simple/jettison-8 144B ± 0% Complex/standard-8 4.05kB ± 0% Complex/jsoniter-8 3.95kB ± 0% Complex/segmentj-8 2.56kB ± 0% Complex/jettison-8 935B ± 0% CodeMarshal/standard-8 1.97MB ± 0% CodeMarshal/jsoniter-8 2.00MB ± 0% CodeMarshal/segmentj-8 1.98MB ± 2% CodeMarshal/jettison-8 1.98MB ± 2% Map/standard-8 888B ± 0% Map/jsoniter-8 884B ± 0% Map/segmentj-8 576B ± 0% Map/jettison-8 96.0B ± 0% Map/jettison-nosort-8 160B ± 0%

name allocs/op Simple/standard-8 1.00 ± 0% Simple/jsoniter-8 2.00 ± 0% Simple/segmentj-8 1.00 ± 0% Simple/jettison-8 1.00 ± 0% Complex/standard-8 79.0 ± 0% Complex/jsoniter-8 71.0 ± 0% Complex/segmentj-8 52.0 ± 0% Complex/jettison-8 8.00 ± 0% CodeMarshal/standard-8 1.00 ± 0% CodeMarshal/jsoniter-8 2.00 ± 0% CodeMarshal/segmentj-8 1.00 ± 0% CodeMarshal/jettison-8 1.00 ± 0% Map/standard-8 19.0 ± 0% Map/jsoniter-8 14.0 ± 0% Map/segmentj-8 18.0 ± 0% Map/jettison-8 1.00 ± 0% Map/jettison-nosort-8 2.00 ± 0%

Simple [source]

Basic payload with fields of type string, int and bool.

Simple Benchmark Graph

Complex [source]

Large payload with a variety of composite Go types, such as struct, map, interface, multi-dimensions array and slice, with pointer and non-pointer value types.

Please note that this test is somewhat positively influenced by the performances of map marshaling.

Complex Benchmark Graph

CodeMarshal [source]

Borrowed from the encoding/json tests. See testdata/code.json.gz.

CodeMarshal Benchmark Graph

Map [source]

Simple map[string]int with 6 keys.

Map Graph

Credits

This library and its design has been inspired by the work of others at @bet365 and @segmentio. See the following projects for reference:

License

Jettison is licensed under the MIT license. See the LICENSE file.

This package also uses some portions of code from the Go encoding/json package. The associated license can be found in LICENSE.golang.

jettison's People

Contributors

wi2l avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar

jettison's Issues

Panic when marshaling maps in Go 1.18

Hi, I just updated to Go 1.18 and jettison (v0.7.3) started failing when marshaling maps. This code:

package main

import "github.com/wI2L/jettison"

func main() {
	jettison.Marshal(map[string]interface{}{
		"test": 123,
	})
}

will fail using Go 1.18 with this stacktrace:

fatal error: unexpected signal during runtime execution
[signal SIGSEGV: segmentation violation code=0x1 addr=0x100 pc=0x460e05]

goroutine 1 [running]:
runtime.throw({0x4ca188?, 0xc00011c078?})
	/home/flusflas/.gvm/gos/go1.18/src/runtime/panic.go:992 +0x71 fp=0xc00008da38 sp=0xc00008da08 pc=0x430bd1
runtime.sigpanic()
	/home/flusflas/.gvm/gos/go1.18/src/runtime/signal_unix.go:802 +0x3a9 fp=0xc00008da88 sp=0xc00008da38 pc=0x444789
sync.(*Pool).Get(0x56b100)
	/home/flusflas/.gvm/gos/go1.18/src/sync/pool.go:129 +0x25 fp=0xc00008dac0 sp=0xc00008da88 pc=0x460e05
github.com/wI2L/jettison.encodeSortedMap(0x56b0c0, {0xc000138000, 0x1, 0x1000}, {{0x4e5730, 0xc00012c028}, {0x4c8fa0, 0x23}, 0x5, 0x0, ...}, ...)
	/home/flusflas/go/pkg/mod/github.com/w!i2!l/[email protected]/encode.go:415 +0x7a fp=0xc00008dc00 sp=0xc00008dac0 pc=0x49a13a
github.com/wI2L/jettison.encodeMap(0x2?, {0xc000138000, 0x0, 0x1000}, {{0x4e5730, 0xc00012c028}, {0x4c8fa0, 0x23}, 0x5, 0x0, ...}, ...)
	/home/flusflas/go/pkg/mod/github.com/w!i2!l/[email protected]/encode.go:364 +0x337 fp=0xc00008dce0 sp=0xc00008dc00 pc=0x499cd7
github.com/wI2L/jettison.newMapInstr.func1(0x56b000?, {0xc000138000?, 0xc00010c2b0?, 0xc000138000?}, {{0x4e5730, 0xc00012c028}, {0x4c8fa0, 0x23}, 0x5, 0x0, ...})
	/home/flusflas/go/pkg/mod/github.com/w!i2!l/[email protected]/instruction.go:400 +0x72 fp=0xc00008dd70 sp=0xc00008dce0 pc=0x49ff52
github.com/wI2L/jettison.wrapInlineInstr.func1(0xc00010e150, {0xc000138000?, 0x203000?, 0x0?}, {{0x4e5730, 0xc00012c028}, {0x4c8fa0, 0x23}, 0x5, 0x0, ...})
	/home/flusflas/go/pkg/mod/github.com/w!i2!l/[email protected]/instruction.go:406 +0x65 fp=0xc00008dde0 sp=0xc00008dd70 pc=0x4a0165
github.com/wI2L/jettison.marshalJSON({0x4b2a80?, 0xc00010e150?}, {{0x4e5730, 0xc00012c028}, {0x4c8fa0, 0x23}, 0x5, 0x0, 0x0, 0x0})
	/home/flusflas/go/pkg/mod/github.com/w!i2!l/[email protected]/json.go:167 +0xd9 fp=0xc00008dea8 sp=0xc00008dde0 pc=0x4a0eb9
github.com/wI2L/jettison.Marshal({0x4b2a80?, 0xc00010e150?})
	/home/flusflas/go/pkg/mod/github.com/w!i2!l/[email protected]/json.go:115 +0xbf fp=0xc00008df48 sp=0xc00008dea8 pc=0x4a0d7f
main.main()
	/home/flusflas/main/main.go:6 +0x7a fp=0xc00008df80 sp=0xc00008df48 pc=0x4a601a

I tried to look for any reported bug with pools in Go 1.18, but I didn't find anything, and I feel a little lost here.

AllowList appear to be propagated

From the docs it seams that field selection through an AllowList, should only apply to first-level elements:

AllowList sets the list of first-level fields which are to be considered when encoding a struct. The fields are identified by the name that is used in the final JSON payload. See DenyFields documentation for more information regarding joint use with this option.

However, it looks like they are applied to nested struct elements as well:

https://go.dev/play/p/a89nZXNQ7HD

fatal error: runtime: name offset out of range

I'm getting this amazing error on go version go1.18.1 linux/amd64:

runtime: nameOff 0x1276120 out of range 0x1029000 - 0x17d770e
fatal error: runtime: name offset out of range
goroutine 57 [running]:
runtime.throw({0x1317f5a?, 0xc0001aec20?})
        /usr/lib/go/src/runtime/panic.go:992 +0x71 fp=0xc00060ca70 sp=0xc00060ca40 pc=0x435751
runtime.resolveNameOff(0x11c5e60?, 0x1276120)
        /usr/lib/go/src/runtime/type.go:198 +0x265 fp=0xc00060cac8 sp=0xc00060ca70 pc=0x45ba25
reflect.resolveNameOff(0x12ccb80?, 0x179e620?)
        /usr/lib/go/src/runtime/runtime1.go:498 +0x19 fp=0xc00060cae8 sp=0xc00060cac8 pc=0x460ed9
reflect.(*rtype).nameOff(...)
        /usr/lib/go/src/reflect/type.go:729
reflect.(*rtype).String(0x179e620)
        /usr/lib/go/src/reflect/type.go:799 +0x25 fp=0xc00060cb08 sp=0xc00060cae8 pc=0x4af745
reflect.(*rtype).ptrTo(0x179e620)
        /usr/lib/go/src/reflect/type.go:1456 +0x65 fp=0xc00060cb98 sp=0xc00060cb08 pc=0x4b29e5
reflect.PointerTo(...)
        /usr/lib/go/src/reflect/type.go:1442
reflect.PtrTo(...)
        /usr/lib/go/src/reflect/type.go:1437
github.com/wI2L/jettison.newMarshalerTypeInstr({0x17b0fe0, 0x179e620}, 0x0)
        /home/fiatjaf/comp/go/pkg/mod/github.com/w!i2!l/[email protected]/instruction.go:139 +0x58 fp=0xc00060cbd0 sp=0xc00060cb98 pc=0x9a2638
github.com/wI2L/jettison.newInstruction({0x17b0fe0?, 0x179e620}, 0x20?, 0xe6?)
        /home/fiatjaf/comp/go/pkg/mod/github.com/w!i2!l/[email protected]/instruction.go:94 +0x52 fp=0xc00060cc08 sp=0xc00060cbd0 pc=0x9a21f2
github.com/wI2L/jettison.cachedInstr({0x17b0fe0?, 0x179e620})
        /home/fiatjaf/comp/go/pkg/mod/github.com/w!i2!l/[email protected]/instruction.go:45 +0x85 fp=0xc00060cc40 sp=0xc00060cc08 pc=0x9a1f85
github.com/wI2L/jettison.encodeInterface(0xc000022d00?, {0xc00037d000, 0xf, 0x1000}, {{0x17a6490, 0xc000040068}, {0x131a373, 0x23}, 0x5, 0xb1, ...})
        /home/fiatjaf/comp/go/pkg/mod/github.com/w!i2!l/[email protected]/encode.go:70 +0x87 fp=0xc00060ccd0 sp=0xc00060cc40 pc=0x99bec7
github.com/wI2L/jettison.encodeStruct(0xc0001e50e0, {0xc00037d000?, 0x99b885?, 0x2021b00?}, {{0x17a6490, 0xc000040068}, {0x131a373, 0x23}, 0x5, 0xb1, ...}, ...)
        /home/fiatjaf/comp/go/pkg/mod/github.com/w!i2!l/[email protected]/encode.go:244 +0x4f0 fp=0xc00060cdf0 sp=0xc00060ccd0 pc=0x99d1b0
github.com/wI2L/jettison.newStructFieldsInstr.func2(0x1346e501a90b46?, {0xc00037d000?, 0x7f2645981538?, 0x40?}, {{0x17a6490, 0xc000040068}, {0x131a373, 0x23}, 0x5, 0xb1, ...})
        /home/fiatjaf/comp/go/pkg/mod/github.com/w!i2!l/[email protected]/instruction.go:319 +0x65 fp=0xc00060ce78 sp=0xc00060cdf0 pc=0x9a3a25
github.com/wI2L/jettison.marshalJSON({0x12a17a0?, 0xc0001e50e0?}, {{0x17a6490, 0xc000040068}, {0x131a373, 0x23}, 0x5, 0xb1, 0x0, 0x0})
        /home/fiatjaf/comp/go/pkg/mod/github.com/w!i2!l/[email protected]/json.go:167 +0xd9 fp=0xc00060cf40 sp=0xc00060ce78 pc=0x9a5499
github.com/wI2L/jettison.MarshalOpts({0x12a17a0, 0xc0001e50e0}, {0xc00060d028, 0x4, 0xc0003cbc00?})
        /home/fiatjaf/comp/go/pkg/mod/github.com/w!i2!l/[email protected]/json.go:142 +0x1a9 fp=0xc00060d000 sp=0xc00060cf40 pc=0x9a5289
github.com/lnbits/infinity/utils.JSONMarshal({0x12a17a0?, 0xc0001e50e0?})
        /home/fiatjaf/comp/infinity/utils/json.go:24 +0x6c fp=0xc00060d058 sp=0xc00060d000 pc=0x9ad2cc
github.com/lnbits/infinity/api/apiutils.SendJSON({0x17a57a8, 0xc00002e2a0}, {0x12a17a0?, 0xc0001e50e0?})
        /home/fiatjaf/comp/infinity/api/apiutils/jsonresponse.go:10 +0x33 fp=0xc00060d088 sp=0xc00060d058 pc=0xb7c733
github.com/lnbits/infinity/api.LnurlScan({0x17a57a8, 0xc00002e2a0}, 0xc0005ecb00)
        /home/fiatjaf/comp/infinity/api/wallet.go:347 +0x786 fp=0xc00060d5c8 sp=0xc00060d088 pc=0xf14ba6
net/http.HandlerFunc.ServeHTTP(0xc0003a28f0?, {0x17a57a8?, 0xc00002e2a0?}, 0xc0005ecb00?)
        /usr/lib/go/src/net/http/server.go:2084 +0x2f fp=0xc00060d5f0 sp=0xc00060d5c8 pc=0x6d8e6f

Panic when marshalling a struct with a map and a custom MarshalJSON()

Marshalling panics when a struct containing a map field and a custom MarshalJSON() is provided.
Tested on Apple M1 Pro, running macOS 14.0.

The below test code:

type Fields struct {
	AdditionalProperties map[string]string `json:"-"`
}

func (a Fields) MarshalJSON() ([]byte, error) {
	var err error
	object := make(map[string]json.RawMessage)

	for fieldName, field := range a.AdditionalProperties {
		object[fieldName], err = json.Marshal(field)
		if err != nil {
			return nil, fmt.Errorf("error marshaling '%s': %w", fieldName, err)
		}
	}
	return json.Marshal(object)
}

func TestJettison(t *testing.T) {
	v := Fields{
		AdditionalProperties: map[string]string{
			"foo": "bar",
		},
	}
	_, err := jettison.Marshal(v)
	if err != nil {
		t.Fatal(err)
	}
}

results in:

unexpected fault address 0x6bff37c970
fatal error: fault
[signal SIGBUS: bus error code=0x1 addr=0x6bff37c970 pc=0x100fa32d8]

goroutine 6 [running]:
runtime.throw({0x101105eaf?, 0x100fa07b4?})
	/usr/local/go/src/runtime/panic.go:1077 +0x40 fp=0x1400005d910 sp=0x1400005d8e0 pc=0x100fcabd0
runtime.sigpanic()
	/usr/local/go/src/runtime/signal_unix.go:858 +0x178 fp=0x1400005d970 sp=0x1400005d910 pc=0x100fe26c8
runtime.evacuated(...)
	/usr/local/go/src/runtime/map.go:205
runtime.mapiternext(0x1400005dab8)
	/usr/local/go/src/runtime/map.go:897 +0xe8 fp=0x1400005d9f0 sp=0x1400005d980 pc=0x100fa32d8
runtime.mapiterinit(0xb?, 0x0?, 0x10117aa40?)
	/usr/local/go/src/runtime/map.go:864 +0x2a0 fp=0x1400005da20 sp=0x1400005d9f0 pc=0x100fa31b0
ef-studio/catalyst/tests/lib.Fields.MarshalJSON({0x1400005db40?})
	/Users/ziemekobel/ws/ef-studio/backend/go/catalyst/tests/lib/jettison_test.go:21 +0x58 fp=0x1400005db20 sp=0x1400005da20 pc=0x101105078
github.com/wI2L/jettison.encodeJSONMarshaler({0x10118e100?, 0x1400005dd98}, {0x1400016a000, 0x0, 0x1000}, {{0x1011b98c0, 0x10131db00}, {0x10110eada, 0x23}, 0x5, ...}, ...)
	/Users/ziemekobel/go/pkg/mod/github.com/w!i2!l/[email protected]/encode.go:692 +0x7c fp=0x1400005dbc0 sp=0x1400005db20 pc=0x1010fb9ec
github.com/wI2L/jettison.encodeMarshaler(0x1400005dd98, {0x1400016a000, 0x0, 0x1000}, {{0x1011b98c0, 0x10131db00}, {0x10110eada, 0x23}, 0x5, 0x0, ...}, ...)
	/Users/ziemekobel/go/pkg/mod/github.com/w!i2!l/[email protected]/encode.go:668 +0x2b4 fp=0x1400005dc80 sp=0x1400005dbc0 pc=0x1010fb524
github.com/wI2L/jettison.newMarshalerTypeInstr.newJSONMarshalerInstr.func5(0x1400000e1f8?, {0x1400016a000?, 0x1400005dd58?, 0x1010f7134?}, {{0x1011b98c0, 0x10131db00}, {0x10110eada, 0x23}, 0x5, 0x0, ...})
	/Users/ziemekobel/go/pkg/mod/github.com/w!i2!l/[email protected]/instruction.go:241 +0x68 fp=0x1400005dd10 sp=0x1400005dc80 pc=0x1010fdec8
github.com/wI2L/jettison.cachedInstr.wrapInlineInstr.func1(0x14000108ea0, {0x1400016a003, 0x0, 0x1000}, {{0x1011b98c0, 0x10131db00}, {0x10110eada, 0x23}, 0x5, 0x0, ...})
	/Users/ziemekobel/go/pkg/mod/github.com/w!i2!l/[email protected]/instruction.go:406 +0x94 fp=0x1400005dd90 sp=0x1400005dd10 pc=0x1010fd094
github.com/wI2L/jettison.marshalJSON({0x10118e100, 0x14000108ea0?}, {{0x1011b98c0, 0x10131db00}, {0x10110eada, 0x23}, 0x5, 0x0, 0x0, 0x0})
	/Users/ziemekobel/go/pkg/mod/github.com/w!i2!l/[email protected]/json.go:167 +0xe0 fp=0x1400005de70 sp=0x1400005dd90 pc=0x1011007b0
github.com/wI2L/jettison.Marshal({0x10118e100?, 0x14000108ea0?})
	/Users/ziemekobel/go/pkg/mod/github.com/w!i2!l/[email protected]/json.go:115 +0x84 fp=0x1400005df10 sp=0x1400005de70 pc=0x101100674
ef-studio/catalyst/tests/lib.TestJettison(0x140001524e0)
	/Users/ziemekobel/ws/ef-studio/backend/go/catalyst/tests/lib/jettison_test.go:36 +0x7c fp=0x1400005df60 sp=0x1400005df10 pc=0x10110525c

Error when marshalling a structure with a field being an interface

When trying to marshal a structure that has a field declared as an interface, an error gets returned.
Tested using Go 1.21, MacOS 14.0, M1 Pro.

The following test demonstrates it:

package lib

import (
	"testing"

	"github.com/wI2L/jettison"
)

type Circle struct {
	Radius int `json:"radius"`
}

func (c Circle) Area() int {
	return c.Radius * c.Radius
}

type Shape interface {
	Area() int
}

type Foo struct {
	S Shape `json:"s"`
}

func TestJettisonMarshal(t *testing.T) {
	foo := Foo{S: Circle{Radius: 1}}

	_, err := jettison.Marshal(foo)
	if err != nil {
		t.Fatal(err)
	}
}

returns:

=== RUN   TestJettisonMarshal
runtime: nameOff 0x2af780 out of range 0x10029c000 - 0x1002d5a08
fatal error: runtime: name offset out of range

goroutine 6 [running]:
runtime.throw({0x1002704f4?, 0x14000003ba0?})
	/usr/local/go/src/runtime/panic.go:1077 +0x40 fp=0x14000125570 sp=0x14000125540 pc=0x1001994c0
runtime.resolveNameOff(0x14000125608?, 0x2af780)
	/usr/local/go/src/runtime/type.go:119 +0x214 fp=0x140001255d0 sp=0x14000125570 pc=0x1001c2f04
reflect.resolveNameOff(0x1400000e101?, 0x2d3f48?)
	/usr/local/go/src/runtime/runtime1.go:604 +0x1c fp=0x140001255f0 sp=0x140001255d0 pc=0x1001c8b1c
reflect.(*rtype).nameOff(...)
	/usr/local/go/src/reflect/type.go:526
reflect.(*rtype).String(0x1002d3f48)
	/usr/local/go/src/reflect/type.go:542 +0x24 fp=0x14000125610 sp=0x140001255f0 pc=0x1001fa964
fmt.(*pp).handleMethods(0x1400007ad00, 0x418108?)
	/usr/local/go/src/fmt/print.go:673 +0x21c fp=0x14000125870 sp=0x14000125610 pc=0x100206b9c
fmt.(*pp).printArg(0x1400007ad00, {0x1002ce3c0?, 0x1002d3f48}, 0x73)
	/usr/local/go/src/fmt/print.go:756 +0x5fc fp=0x14000125910 sp=0x14000125870 pc=0x1002075cc
fmt.(*pp).doPrintf(0x1400007ad00, {0x10026e5ea, 0x1a}, {0x14000125ab8?, 0x1, 0x1})
	/usr/local/go/src/fmt/print.go:1077 +0x2dc fp=0x14000125a20 sp=0x14000125910 pc=0x100209dcc
fmt.Sprintf({0x10026e5ea, 0x1a}, {0x14000125ab8, 0x1, 0x1})
	/usr/local/go/src/fmt/print.go:239 +0x4c fp=0x14000125a80 sp=0x14000125a20 pc=0x10020462c
github.com/wI2L/jettison.(*UnsupportedTypeError).Error(0x1002d5701?)
	/Users/ziemekobel/go/pkg/mod/github.com/w!i2!l/[email protected]/json.go:67 +0x50 fp=0x14000125ad0 sp=0x14000125a80 pc=0x100264060
fmt.(*pp).handleMethods(0x1400007a9c0, 0x0?)
	/usr/local/go/src/fmt/print.go:667 +0x158 fp=0x14000125d30 sp=0x14000125ad0 pc=0x100206ad8
fmt.(*pp).printArg(0x1400007a9c0, {0x1002b03e0?, 0x140000242b0}, 0x76)
	/usr/local/go/src/fmt/print.go:756 +0x5fc fp=0x14000125dd0 sp=0x14000125d30 pc=0x1002075cc
fmt.(*pp).doPrintln(0x1400007a9c0, {0x14000125f48?, 0x1, 0x14000024220?})
	/usr/local/go/src/fmt/print.go:1223 +0x3c fp=0x14000125e50 sp=0x14000125dd0 pc=0x10020acfc
fmt.Sprintln({0x14000125f48, 0x1, 0x1})
	/usr/local/go/src/fmt/print.go:321 +0x3c fp=0x14000125ea0 sp=0x14000125e50 pc=0x10020489c
testing.(*common).Fatal(0x14000003a00, {0x14000125f48?, 0x14000073f48?, 0x1001de19c?})
	/usr/local/go/src/testing/testing.go:1075 +0x3c fp=0x14000125f00 sp=0x14000125ea0 pc=0x10021f75c
ef-studio/catalyst/tests/lib.TestJettisonMarshal(0x14000003a00)
	/Users/ziemekobel/ws/ef-studio/backend/go/catalyst/tests/lib/jettison_test.go:30 +0x94 fp=0x14000125f60 sp=0x14000125f00 pc=0x100268bd4

More flexible allow/deny

For my use case, I only want to apply the AllowList to the top level of my object. I'm thinking that rather than having an AllowList, there could be an Allower interface:

type Allower interface {
    Allow(jsonKey string) bool
    Recurse(jsonKey string) Allower
}

Recurse could return nil to indicate that there is no filtering with the key.

If this idea is acceptable to you, I might try my hand at building it sometime in the next year. While I would use it immediately if it were available, I can work around not having it for a while.

The current AllowList and DenyList could be re-implemented as things that generate an Allower.

Omitting a value that marshals into `null` isn't possible?

Hey,

I've tried to replace encoding/json with jettison, since jettison has the omitnil tag.

There are certain fields that I am using a custom type for. The idea behind the type is to allow a HTTP request to set a field to null via a PATCH request, but not automatically null all omitted fields. For that to work, null fields must not be see as empty when unmarshalling. However, when marshalling, these null-values have no worth and should be omitted. Is something like that possible?

Let's say you had the following type:

type Thing {
    A *NullableString
    B *NullableString
}

If you now had a resource where both A and B already had a value of Hello and you'd send the following request:

{
    "A": null,

Then field A should be nulled, while B will stay unchanged, since A was explicitly defined, but B was not.
The easy solution would be to make two versions of all structs. One for requests and one for replies, however I think that isn't desirable, as it bloats the code and doesn't allow sharing code to work on these structs.

Here's a small example. The second case will panic with json: error calling MarshalJSON for type *flows_service.NullableString: json: invalid value.

package flows_service

import (
	"encoding/json"
	"fmt"
	"testing"

	"github.com/wI2L/jettison"
)

type NullableString struct {
	// Set indicates whether unmarshalled JSON contained the field.
	Set bool
	// This field is only relevant if Set is `true`
	NonNull bool
	// Val; Only relevant if NonNull and Set are `true`.
	Val string
}

// MarshalJSON implements json.Marshaler.
func (v *NullableString) MarshalJSON() ([]byte, error) {
	if !v.Set || !v.NonNull {
		return nil, nil
	}

	return json.Marshal(v.Val)
}

// UnmarshalJSON implements json.Unmarshaler.
func (v *NullableString) UnmarshalJSON(data []byte) error {
	// If this method was called, the value was set.
	v.Set = true

	if string(data) == "null" {
		return nil
	}

	if err := json.Unmarshal(data, &v.Val); err != nil {
		return err
	}

	v.NonNull = true
	return nil
}

type Something struct {
	Field *NullableString `json:"field,omitempty,omitnil"`
}

func TestMarshalling(t *testing.T) {
	cases := []*Something{
		{},
		{
			Field: &NullableString{},
		},
		{
			Field: &NullableString{
				Set: true,
			},
		},
	}

	for i, c := range cases {
		t.Run(fmt.Sprintf("%d", i), func(t *testing.T) {
			data, err := jettison.Marshal(c)
			if err != nil {
				panic(err)
			}
			t.Log(string(data))
		})
	}
}

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.