json.Marshal panics with a nil pointer dereference for a struct that encoding/json encodes without any problem.
The shape that triggers it is:
- a struct (
Inner) that contains an embedded value struct followed by a named pointer-to-struct field tagged omitempty (pointer is nil), and
- that
Inner struct is itself embedded into an outer struct.
This worked correctly up to and including v0.10.0 and started panicking in v0.10.1. It is still broken on the latest release v0.10.6.
Minimal reproduction
package main
import (
stdjson "encoding/json"
"fmt"
json "github.com/goccy/go-json"
)
type Reason struct {
Code string `json:"reasonCode,omitempty"`
}
type Cov struct {
Type string `json:"type"` // plain (non-omitempty) field is required to trigger it
}
type Inner struct {
Cov // embedded value struct
R *Reason `json:"reason,omitempty"` // nil pointer-to-struct, omitempty
}
type Outer struct {
Inner // Inner is embedded again
}
func main() {
std, _ := stdjson.Marshal(Outer{})
fmt.Printf("encoding/json: %s\n", std) // {"type":""}
b, err := json.Marshal(Outer{}) // panics on v0.10.1 .. v0.10.6
fmt.Printf("go-json: %s err=%v\n", b, err)
}
Actual behaviour (go-json v0.10.6)
encoding/json: {"type":""}
panic: runtime error: invalid memory address or nil pointer dereference
[signal SIGSEGV: segmentation violation code=0x1 addr=0x0]
goroutine 1 [running]:
github.com/goccy/go-json/internal/encoder/vm.Run(...)
.../go-json@v0.10.6/internal/encoder/vm/vm.go:26
github.com/goccy/go-json.encodeRunCode(...)
.../go-json@v0.10.6/encode.go:310
github.com/goccy/go-json.encode(...)
.../go-json@v0.10.6/encode.go:235
github.com/goccy/go-json.marshal(...)
.../go-json@v0.10.6/encode.go:150
github.com/goccy/go-json.MarshalWithOption(...)
.../go-json@v0.10.6/json.go:185
github.com/goccy/go-json.Marshal(...)
.../go-json@v0.10.6/json.go:170
main.main()
Expected behaviour
Same as encoding/json: {"type":""}, no panic.
Version bisect
| version |
result of Marshal(Outer{}) |
| v0.9.10 |
{"type":""} (ok) |
| v0.9.11 |
{"type":""} (ok) |
| v0.10.0 |
{"type":""} (ok) |
| v0.10.1 |
panic |
| v0.10.2 – v0.10.6 |
panic |
So the regression was introduced in v0.10.1 and is still present in the current release.
Conditions (each verified independently)
The panic requires all of the following; removing any one makes it encode correctly on v0.10.6:
- the embedded value struct comes before the pointer field (swapping the order encodes fine);
- the pointer field has
omitempty (without it, both versions emit "reason":null correctly);
- the field is a pointer to a struct (
*int etc. does not trigger it);
Inner is embedded into Outer (a non-embedded/flat layout is fine);
- the embedded value struct has at least one non-
omitempty field.
Environment
- go version: go1.26.2 (also reproduced on go1.22; the panic is independent of the Go version)
- go-json: v0.10.6 (also reproduced on v0.10.1–v0.10.5)
encoding/json encodes the same value correctly on all versions.
json.Marshalpanics with a nil pointer dereference for a struct thatencoding/jsonencodes without any problem.The shape that triggers it is:
Inner) that contains an embedded value struct followed by a named pointer-to-struct field taggedomitempty(pointer isnil), andInnerstruct is itself embedded into an outer struct.This worked correctly up to and including v0.10.0 and started panicking in v0.10.1. It is still broken on the latest release v0.10.6.
Minimal reproduction
Actual behaviour (go-json v0.10.6)
Expected behaviour
Same as
encoding/json:{"type":""}, no panic.Version bisect
Marshal(Outer{}){"type":""}(ok){"type":""}(ok){"type":""}(ok)So the regression was introduced in v0.10.1 and is still present in the current release.
Conditions (each verified independently)
The panic requires all of the following; removing any one makes it encode correctly on v0.10.6:
omitempty(without it, both versions emit"reason":nullcorrectly);*intetc. does not trigger it);Inneris embedded intoOuter(a non-embedded/flat layout is fine);omitemptyfield.Environment
encoding/jsonencodes the same value correctly on all versions.