Skip to content

json.Unmarshal Inconsistently Preserves Type Information in interface{} Fields Based on Initialization with Pointer vs. Non-Pointer Value #75633

@Jethan-w

Description

@Jethan-w

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:

  1. When Body was initialized with a POINTER (&MyBody{}): The type was preserved. After json.Unmarshal, the type of Body remained *main.MyBody.
  2. When Body was initialized with a NON-POINTER STRUCT VALUE (MyBody{}): The original type information was lost. After json.Unmarshal, the type of Body became map[string]interface {} instead of the expected main.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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions