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)
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 thejson
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 v2json
. 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.
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 typefloat64
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?