Skip to content

Marshal: nil pointer dereference on an omitempty pointer-to-struct field next to an embedded struct, when that struct is itself embedded (regression since v0.10.1) #581

Description

@gatisr

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.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions