go: encoding/json: cannot unmarshal custom interface value

What version of Go are you using (go version)?

$ go version
go version go1.16.3 linux/amd64

Does this issue reproduce with the latest release?

yes

What operating system and processor architecture are you using (go env)?

go env Output
$ go env
GO111MODULE=""
GOARCH="amd64"
GOBIN=""
GOCACHE="/home/user/.cache/go-build"
GOENV="/home/user/.config/go/env"
GOEXE=""
GOFLAGS=""
GOHOSTARCH="amd64"
GOHOSTOS="linux"
GOINSECURE=""
GOMODCACHE="/home/user/go/pkg/mod"
GONOPROXY=""
GONOSUMDB=""
GOOS="linux"
GOPATH="/home/user/go"
GOPRIVATE=""
GOPROXY="https://proxy.golang.org,direct"
GOROOT="/usr/lib/go"
GOSUMDB="sum.golang.org"
GOTMPDIR=""
GOTOOLDIR="/usr/lib/go/pkg/tool/linux_amd64"
GOVCS=""
GOVERSION="go1.16.3"
GCCGO="gccgo"
AR="ar"
CC="gcc"
CXX="g++"
CGO_ENABLED="1"
GOMOD="/home/user/src/go.mod"
CGO_CFLAGS="-g -O2"
CGO_CPPFLAGS=""
CGO_CXXFLAGS="-g -O2"
CGO_FFLAGS="-g -O2"
CGO_LDFLAGS="-g -O2"
PKG_CONFIG="pkg-config"
GOGCCFLAGS="-fPIC -m64 -pthread -fmessage-length=0 -fdebug-prefix-map=/tmp/go-build2204759132=/tmp/go-build -gno-record-gcc-switches"

What did you do?

https://play.golang.org/p/VMWbHeo54sj

p := private(*new(wrap))
err := json.Unmarshal([]byte("1"), &p)
fmt.Println(p, err)

p = new(wrap)
err = json.Unmarshal([]byte("1"), p)
fmt.Println(*p.(*wrap), err)

i := (interface{})(*new(wrap))
err = json.Unmarshal([]byte("1"), &i)
fmt.Println(i, err)

i = new(wrap)
err = json.Unmarshal([]byte("1"), i)
fmt.Println(*i.(*wrap), err)

What did you expect to see?

Any interface should be able to be assignable:

1 <nil>
1 <nil>
1 <nil>
1 <nil>

What did you see instead?

Instead it appears that interface{} is special, and unless main.private implements json.Unmarshaler it will error at runtime:

0 json: cannot unmarshal number into Go value of type main.private
1 <nil>
1 <nil>
1 <nil>

This restriction does not seem meaningful, and I could not find it documented in source why this was enforced. Can we not mutate the pointed value inside a provided interface regardless of type?

About this issue

  • Original URL
  • State: open
  • Created 3 years ago
  • Comments: 21 (15 by maintainers)

Most upvoted comments

There’s quite a bit a functionality that we would like to change with json (as evidenced by the many issues opened against it). Theoretically, we can add a knob for every one of those changes, but that would lead to the json package being incomprehensible. We could also judiciously choose to only add knobs for the most pressing issues (for which this is probably not high on the list).

The best course of action is to systemically look at the entirety of the json package, and ask ourselves whether the summation of all the changes justify a v2 json. Maybe it does, maybe it doesn’t.

@colin-sitehost that’s been happening since last year, see https://twitter.com/mvdan_/status/1280133100673159171.

Generics may provide type safety in some situations, but is not a magic feature that’s going to speed up JSON serialization. In order to make serialization of arbitrary Go data structures faster, you would need to be able to change the serialization logic at compile-time based on arbitrary Go types†. Generics does not provide that. The main ways to speed up JSON serialization‡ is either through the use of unsafe, which won’t happen, or by code generating the (Un)MarshalJSON methods. However, tools for doing that exist outside the standard library.

† The generics proposal does allow type switching over specific types to provide specialized implementations. However, that doesn’t help JSON since the set of possible types to switch over is not finite. ‡ I’m assuming that Go reflection is the bottleneck.

wouldn’t that actually work fine though? adding a v2 package.

I’m not sure I understand the question. Of course a v2 package could fix this issue without worrying about backwards compatibility. That said, a v2 package is beyond the scope of this issue.

@mknyszek can’t it be fixed and be put behind a knob in the decoder?

I consider the inability to unmarshal into an unaddressable value (which can occur with maps and interfaces) a flaw in the implementation. Unfortunately, it’s hard to change this behavior as people have likely come to depend on the current behavior. 😞

Here’s a tweaked program: https://play.golang.org/p/w6_c2eoUF7B

Note that, in the “it works” scenario, you end up decoding into interface{}, so you get a new value of default type float64 for a number.

That fails in the case where the interface has one method, because a basic type cannot satisfy a non-empty interface, hence the error.

What were you expecting should happen, in terms of the final types?