-
Notifications
You must be signed in to change notification settings - Fork 18.4k
Description
What version of Go are you using (go version
)?
$ go version go version go1.24.4 darwin/arm64
Does this issue reproduce with the latest release?
What operating system and processor architecture are you using (go env
)?
go env
Output
$ go env AR='ar' CC='cc' CGO_CFLAGS='-O2 -g' CGO_CPPFLAGS='' CGO_CXXFLAGS='-O2 -g' CGO_ENABLED='1' CGO_FFLAGS='-O2 -g' CGO_LDFLAGS='-O2 -g' CXX='c++' GCCGO='gccgo' GO111MODULE='' GOARCH='arm64' GOARM64='v8.0' GOAUTH='netrc' GOBIN='' GOCACHE='/Users/home/Library/Caches/go-build' GOCACHEPROG='' GODEBUG='' GOENV='/Users/home/Library/Application Support/go/env' GOEXE='' GOEXPERIMENT='' GOFIPS140='off' GOFLAGS='' GOGCCFLAGS='-fPIC -arch arm64 -pthread -fno-caret-diagnostics -Qunused-arguments -fmessage-length=0 -ffile-prefix-map=/var/folders/vr/zgz43tx16j32742b7gc_rj2m0000gn/T/go-build3067540816=/tmp/go-build -gno-record-gcc-switches -fno-common' GOHOSTARCH='arm64' GOHOSTOS='darwin' GOINSECURE='' GOMOD='/Users/home/Desktop/go/cicd-deploy/go.mod' GOMODCACHE='/Users/home/go/pkg/mod' GONOPROXY='' GONOSUMDB='' GOOS='darwin' GOPATH='/Users/home/go' GOPRIVATE='' GOPROXY='https://goproxy.cn,direct' GOROOT='/opt/homebrew/Cellar/go/1.24.4/libexec' GOSUMDB='sum.golang.org' GOTELEMETRY='local' GOTELEMETRYDIR='/Users/home/Library/Application Support/go/telemetry' GOTMPDIR='' GOTOOLCHAIN='auto' GOTOOLDIR='/opt/homebrew/Cellar/go/1.24.4/libexec/pkg/tool/darwin_arm64' GOVCS='' GOVERSION='go1.24.4' GOWORK='' PKG_CONFIG='pkg-config' uname -v: Darwin Kernel Version 24.6.0: Mon Jul 14 11:30:51 PDT 2025; root:xnu-11417.140.69~1/RELEASE_ARM64_T8112 ProductName: macOS ProductVersion: 15.6 BuildVersion: 24G84 lldb --version: lldb-1700.0.9.502 Apple Swift version 6.1.2 (swiftlang-6.1.2.1.2 clang-1700.0.13.5)
What did you do?
I encountered unexpected and inconsistent behavior from encoding/json.Unmarshal
when unmarshaling JSON into a struct field of type interface{}
. The preservation of the original type information after unmarshaling depends on whether the interface{}
field was initially assigned a pointer to a struct or a non-pointer struct value.
Here is a complete, runnable program demonstrating the issue:
package main
import (
"encoding/json"
"fmt"
)
type MyResponse struct {
Body interface{} `json:"body"`
}
type MyBody struct {
ModifiedTime string `json:"modifiedTime"`
}
func main() {
// Scenario 1: Initialize as pointer -> behavior as expected
p1 := MyResponse{Body: &MyBody{}}
json.Unmarshal([]byte(`{"body": {"modifiedTime": "1759031086345"}}`), &p1)
fmt.Printf("Pointer scenario - Body type: %T\n", p1.Body) // Output: *main.MyBody
// Scenario 2: Initialize as struct -> type lost!
p2 := MyResponse{Body: MyBody{}}
json.Unmarshal([]byte(`{"body": {"modifiedTime": "1759031086345"}}`), &p2)
fmt.Printf("Non-pointer scenario - Body type: %T\n", p2.Body) // Output: map[string]interface {}
}
What did you expect to see?
I expected json.Unmarshal
to behave consistently. Since the field Body
is declared as interface{}
, I understand that the package needs to determine a concrete type during unmarshaling. However, I expected that if a concrete value (like MyBody
or *MyBody
) already existed in the interface{}
field, Unmarshal
would attempt to reuse that existing type to populate the new data, regardless of whether the initial value was a pointer or a non-pointer.
I hoped both cases would either preserve the original type information (*main.MyBody
or main.MyBody
) or consistently handle the type inference in a predictable way.
What did you see instead?
The behavior was inconsistent and depended entirely on whether the interface{}
field was initialized with a pointer or a non-pointer value:
- When
Body
was initialized with a POINTER (&MyBody{}
): The type was preserved. Afterjson.Unmarshal
, the type ofBody
remained*main.MyBody
. - When
Body
was initialized with a NON-POINTER STRUCT VALUE (MyBody{}
): The original type information was lost. Afterjson.Unmarshal
, the type ofBody
becamemap[string]interface {}
instead of the expectedmain.MyBody
.
Output:
Pointer scenario - Body type: *main.MyBody
Non-pointer scenario - Body type: map[string]interface {}
This inconsistency is problematic and non-obvious.
It can lead to runtime panics when performing type assertions if the code assumes the type is preserved based on the initial assignment, as the underlying type has changed unexpectedly.
The loss of type information when a non-pointer value is used contradicts the principle of least astonishment.