Skip to content

encoding/json: json.Unmarshal converts an interface variable with a value to a Map #69875

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
glossd opened this issue Oct 14, 2024 · 17 comments
Closed

Comments

@glossd
Copy link

glossd commented Oct 14, 2024

Go version

go1.23.2 darwin/arm64

Output of go env in your module/workspace:

GO111MODULE=''
GOARCH='arm64'
GOBIN=''
GOCACHE='/Users/dennisgloss/Library/Caches/go-build'
GOENV='/Users/dennisgloss/Library/Application Support/go/env'
GOEXE=''
GOEXPERIMENT=''
GOFLAGS=''
GOHOSTARCH='arm64'
GOHOSTOS='darwin'
GOINSECURE=''
GOMODCACHE='/Users/dennisgloss/go/pkg/mod'
GONOPROXY=''
GONOSUMDB=''
GOOS='darwin'
GOPATH='/Users/dennisgloss/go'
GOPRIVATE=''
GOPROXY='https://proxy.golang.org,direct'
GOROOT='/usr/local/go'
GOSUMDB='sum.golang.org'
GOTMPDIR=''
GOTOOLCHAIN='auto'
GOTOOLDIR='/usr/local/go/pkg/tool/darwin_arm64'
GOVCS=''
GOVERSION='go1.23.2'
GODEBUG=''
GOTELEMETRY='local'
GOTELEMETRYDIR='/Users/dennisgloss/Library/Application Support/go/telemetry'
GCCGO='gccgo'
GOARM64='v8.0'
AR='ar'
CC='clang'
CXX='clang++'
CGO_ENABLED='1'
GOMOD='/dev/null'
GOWORK=''
CGO_CFLAGS='-O2 -g'
CGO_CPPFLAGS=''
CGO_CXXFLAGS='-O2 -g'
CGO_FFLAGS='-O2 -g'
CGO_LDFLAGS='-O2 -g'
PKG_CONFIG='pkg-config'
GOGCCFLAGS='-fPIC -arch arm64 -pthread -fno-caret-diagnostics -Qunused-arguments -fmessage-length=0 -ffile-prefix-map=/var/folders/6l/4kv450ss4pxbqd0kvk685p_80000gn/T/go-build2453477244=/tmp/go-build -gno-record-gcc-switches -fno-common'

What did you do?

I was working with reflect package and wanted to json.Unmarshal a value received from reflect.Value#Interface(). Because the Interface method returns any json.Unmarshal automatically converts the passed value into a map[string]any.

What did you see happen?

It can be reproduced with this code

package main

import (
	"encoding/json"
	"fmt"
	"reflect"
)

func main() {
        type Pet struct {
              Name string
        }
	var p Pet
	var i any = p
	_ = json.Unmarshal([]byte(`{"Name":"Lola"}`), &i)
	fmt.Println(reflect.TypeOf(i)) // prints: map[string]interface {}
}

It happens because of the reflect.Value#Elem() function which unwraps the pointer but does not unwrap the interface and you have to call the Elem() method two times to get the value
e.g.

	var p string
	var i any = p
	fmt.Println(reflect.ValueOf(&i).Elem().Elem().Type()) // prints: string

At first, I fixed the Elem() behaviour but too many packages depended on the double unwrapping. So I worked on json.Unmarshal and here's my fix

I believe the conversion to a map exists for the any interface with nil value.

What did you expect to see?

I want the underlying value of the interface to be updated, instead of reassigning the interface variable.
I'll be happy to make any changes to my fix

@gopherbot
Copy link
Contributor

Change https://go.dev/cl/619995 mentions this issue: encoding/json: fix Unmarshal of value assigned to an interface variable.

@adonovan
Copy link
Member

I think this is just a misunderstanding in the logic surrounding the call to Unmarshal. If you pass Unmarshal a *any, it will produce a result consisting of untyped map[string]any values. Instead, pass (an any value containing) a pointer to a struct type, like so:

https://go.dev/play/p/-1ceojzwcMT

	type S struct {
		X int
	}
	var s S
	var i any = reflect.ValueOf(&s).Interface()
	_ = json.Unmarshal([]byte(`{"X":1}`), i)
	fmt.Println(reflect.TypeOf(i)) // "*main.S"

@seankhliao
Copy link
Member

Duplicate of #26946

@seankhliao seankhliao marked this as a duplicate of #26946 Oct 14, 2024
@seankhliao seankhliao closed this as not planned Won't fix, can't repro, duplicate, stale Oct 14, 2024
@glossd
Copy link
Author

glossd commented Oct 14, 2024

@adonovan Thank you, that's a useful workaround having the interface variable assigned to a pointer. I do still think it's an issue thought, that Unmarshal can't handle the pointer of the interface variable.

@glossd
Copy link
Author

glossd commented Oct 14, 2024

@seankhliao sorry, I missed that, I will add the older issue number to the fix

@adonovan
Copy link
Member

@adonovan Thank you, that's a useful workaround having the interface variable assigned to a pointer. I do still think it's an issue though, that Unmarshal can't handle the pointer of the interface variable.

Unmarshal is handling the pointer to the interface variable exactly as documented. If you want it to populate a struct variable, you need to give it a pointer to a struct variable.

@glossd
Copy link
Author

glossd commented Oct 14, 2024

@adonovan Well, the problem is that I expected it to be unmarshaled into an underlying struct because as far as I understand an interface is not a value, it's more like a constraint on a struct. So unmarshaling into an empty interface doesn't make much sense to me.

@adonovan
Copy link
Member

@adonovan Well, the problem is that I expected it to be unmarshaled into an underlying struct because as far as I understand an interface is not a value, it's more like a constraint on a struct. So unmarshaling into an empty interface doesn't make much sense to me.

An interface type is indeed a kind of constraint: it's the subset of all types that have a particular set of methods. But an interface value is a value capable of representing a variety of (or perhaps any) concrete types: conceptually it is a pair of a concrete type and a value of that type. When you unmarshal into a variable whose type is any, the decoder may update the variable to hold one of half a dozen concrete types: int, float, string, slice, map, or nil.

@glossd
Copy link
Author

glossd commented Oct 14, 2024

@adonovan Yes, that's the use case for the empty any interface, which I kept in the fix. But if an interface has been assigned to a certain type as it is supposed, it would seem Unmarshal should work on the underlying type, because the interface is supposed to be assigned to something.

@ianlancetaylor
Copy link
Member

Personally I think the current behavior is fine.

Much more importantly, we can't change the current behavior. It would certainly break existing working programs.

@adonovan
Copy link
Member

Unmarshal wants a pointer to a variable, which it will update. The type of that variable determines what representation Unmarshal will use. Because Unmarshal long preceded generics, the only way for it to say "I need a pointer to something" is to have a parameter of type any. Your code gave Unmarshal a value of type pointer-to-any, so you requested the dynamic representation (map, etc). It doesn't matter than the variable already contained a value (a pointer to a struct): Unmarshal will overwrite it.

@glossd
Copy link
Author

glossd commented Oct 14, 2024

@ianlancetaylor It would only break programs which assign non-map[string]any value to an interface variable and then expect map[string]any. It's hard to imagine anyone relying on such behaviour.
I think the current behavior is unintuitive because an interface variable is supposed to have a value implementing that interface and the Unmarshal function doesn't consider it.
Besides converting a non-nill any type variable into a map, it creates a weirder result for an interface variable with methods. It doesn't do anything to it. Here's an example. And I would have expected it to change.

@adonovan
Copy link
Member

@ianlancetaylor It would only break programs which assign non-map[string]any value to an interface variable and then expect map[string]any. It's hard to imagine anyone relying on such behaviour.

Unmarshal has always clobbered the contents of the variable, making it independent of whatever junk happened to be in it. Breaking that invariant would certainly break existing programs.

I think the current behavior is unintuitive because an interface variable is supposed to have a value implementing that interface and the Unmarshal function doesn't consider it. Besides converting a non-nill any type variable into a map, it creates a weirder result for an interface variable with methods. It doesn't do anything to it. Here's an example. And I would have expected it to change.

The error returned by Unmarshal explains the problem: "json: cannot unmarshal object into Go value of type main.I". This program is another example of "holding it wrong" by passing Unmarshal an interface variable that it cannot possibly update with any simple JSON value.

@glossd
Copy link
Author

glossd commented Oct 14, 2024

@adonovan I'm confused, there's no error in the example

@adonovan
Copy link
Member

@adonovan I'm confused, there's no error in the example

Unmarshal returns an error, but your example code ignores it. Here's what happens if you report it: https://go.dev/play/p/Mhi3Xlasdmd.

@glossd
Copy link
Author

glossd commented Oct 14, 2024

@adonovan got it, sorry, missing error handling, my fix would unmarshal it though.
As for breaking changes, I see how you could use any type variable for a map and then for an array, but my fix would try to Unmarshal into a map the second time.
However, I could keep the same behaviour by making any type variable always be converted into a map, array, what have you)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

6 participants