-
Notifications
You must be signed in to change notification settings - Fork 17.9k
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
encoding/json: clarify what happens when unmarshaling into a non-empty interface{} #26946
Comments
interface{}
holding non-pointer struct
results in map[string]interface{}
Noted that this doesn't seem to be a regression or recent change, as the same behavior can be found on all Go versions I could test (>= 1.2). The documentation for https://play.golang.org/p/cBQv0DOrPhG Also worth noting that this isn't just an issue when essentially passing |
I suppose the Unmarshal docs could elaborate on what happens when a
I don't even think it's possible to do the first thing you want there. Once you put a struct value in an interface, it's no longer addressable and nothing can mutate it. |
Correct, it doesn't seem we're able to modify However, it appears it's possible to create a new zero value for the underlying type with I can try and work on a patch if the current functionality is considered flawed, knowing that this is quite possibly a breaking change that is hard to check for statically or with I confess to a certain amount of confusion on the rules behind set-ability in the If the behavior cannot be changed, documenting it may help, as the underlying rules are somewhat subtle. |
…ce types This work extends the behaviour of `json.Unmarshal` to allow for setting into concrete types embedded in `interface{}` struct fields. For example, the following JSON object: {"A": {"B": "C"}} Being unmarshaled into the following structure: var t = struct{ A interface{} }{A: struct{ B string }{}} Would have previously resulted in field `A` being replaced with a `map[string]interface{}`, disregarding the underlying type given to the `interface{}` field, i.e.: struct { A interface {} }{A:map[string]interface {}{"B":"C"}} Whereas, by resolving the underlying type of the `interface{}` field, we are able to set into and return the correct type, i.e.: struct { A interface {} }{A:struct { B string }{B:"C"}} These rules apply also to `map` types, for JSON objects, and arrays or slices, for JSON arrays. The rules for setting into these fields are exactly the same as those that would apply to the bare types themselves, i.e. attempting to unmarshal into an underlying type that is unable to store the values presented in the JSON input will return a type error. Noted that this is a breaking change for users that relied on having set a concrete type, but handle a `map[string]interface{}` or `[]interface{}` on the other side. Fixes golang#26946
This work extends the behaviour of json.Unmarshal to allow for setting into concrete types embedded in interface{} struct fields. For example, the following JSON object: {"A": {"B": "C"}} Being unmarshaled into the following structure: var t = struct{ A interface{} }{A: struct{ B string }{}} Would have previously resulted in field A being replaced with a map[string]interface{}, disregarding the underlying type given to the interface{} field, i.e.: struct { A interface {} }{A:map[string]interface {}{"B":"C"}} Whereas, by resolving the underlying type of the interface{} field, we are able to set into and return the correct type, i.e.: struct { A interface {} }{A:struct { B string }{B:"C"}} These rules apply also to map types, for JSON objects, and arrays or slices, for JSON arrays. The rules for setting into these fields are exactly the same as those that would apply to the bare types themselves, i.e. attempting to unmarshal into an underlying type that is unable to store the values presented in the JSON input will return a type error. Noted that this is a breaking change for users that relied on having set a concrete type, but handle a map[string]interface{} or []interface{} on the other side. Fixes golang#26946
Change https://golang.org/cl/135421 mentions this issue: |
I've re-labelled this issue as |
Personally, I think that peeking into However, I can't help but wonder if this is really needed. Users already have several workarounds:
I realise that these options may result in lots of boilerplate in some situations, which is why I'm not opposed to the proposed change. |
I think we should document this. The change in https://golang.org/cl/135421 seems like just teaching the
|
Nope, updating the documentation to cover these cases will help with avoiding confusion in the future. Agreed that the updated implementation is inconsistent with expectations when structs provided are pre-populated. |
Thanks - I've abandoned the CL and repurposed this issue. Feel free to send a godoc CL to fix this issue, and we can take it from there. |
Will do, thanks! |
Unmarshalling into non-empty interfaces would help in this case a lot: package main
import (
"encoding/json"
"fmt"
)
var typeMap = map[string]TypedInterface{
"myType": MyImpl{}, // multiple implementations with different properties but same meta data (with different ObjectTypes)
}
type Properties struct {
P1 []string
P2 string
}
type MetaData struct {
ObjectType string
}
func (md MetaData) Type() string {
return md.ObjectType
}
type TypedInterface interface {
Type() string
}
type MyImpl struct {
MetaData
Properties Properties
}
func main() {
from := MyImpl{
MetaData: MetaData{"myType"},
Properties: Properties{
P1: []string{"P11", "P12"},
P2: "P2",
},
}
data, err := json.Marshal(from)
if err != nil {
fmt.Println(err)
return
}
// the type string can be determined by unmarshaling the meta data object
// and take the ObjectType field or Type() method
to := typeMap["myType"]
err = json.Unmarshal(data, &to)
if err != nil {
fmt.Printf("from: %T, to: %T\n", from, to)
fmt.Println(err)
return
}
fmt.Printf("%v\n", to)
} |
Change https://go.dev/cl/619995 mentions this issue: |
… than unmarshalling into the input encoding/json behaves surprisingly ([#27172](golang/go#27172), [#31924](golang/go#31924), [#26946](golang/go#26946), [#21092](golang/go#21092)) when unmarshalling into non-empty structs, and this behaviour is unexpected in this library anyway; no other methods do any user-facing unmarshalling or pointer mutation. This commit changes all of the `Update` methods that unmarshall into their inputs to no longer do that, and return a newly-allocated struct instead.
… than unmarshalling into the input encoding/json behaves surprisingly ([#27172](golang/go#27172), [#31924](golang/go#31924), [#26946](golang/go#26946), [#21092](golang/go#21092)) when unmarshalling into non-empty structs, and this behaviour is unexpected in this library anyway; no other methods do any user-facing unmarshalling or pointer mutation. This commit changes all of the `Update` methods that unmarshall into their inputs to no longer do that, and return a newly-allocated struct instead.
… than unmarshalling into the input encoding/json behaves surprisingly ([#27172](golang/go#27172), [#31924](golang/go#31924), [#26946](golang/go#26946), [#21092](golang/go#21092)) when unmarshalling into non-empty structs, and this behaviour is unexpected in this library anyway; no other methods do any user-facing unmarshalling or pointer mutation. This commit changes all of the `Update` methods that unmarshall into their inputs to no longer do that, and return a newly-allocated struct instead.
… than unmarshalling into the input encoding/json behaves surprisingly ([#27172](golang/go#27172), [#31924](golang/go#31924), [#26946](golang/go#26946), [#21092](golang/go#21092)) when unmarshalling into non-empty structs, and this behaviour is unexpected in this library anyway; no other methods do any user-facing unmarshalling or pointer mutation. This commit changes all of the `Update` methods that unmarshall into their inputs to no longer do that, and return a newly-allocated struct instead.
… than unmarshalling into the input encoding/json behaves surprisingly ([#27172](golang/go#27172), [#31924](golang/go#31924), [#26946](golang/go#26946), [#21092](golang/go#21092)) when unmarshalling into non-empty structs, and this behaviour is unexpected in this library anyway; no other methods do any user-facing unmarshalling or pointer mutation. This commit changes all of the `Update` methods that unmarshall into their inputs to no longer do that, and return a newly-allocated struct instead.
… than unmarshalling into the input encoding/json behaves surprisingly ([#27172](golang/go#27172), [#31924](golang/go#31924), [#26946](golang/go#26946), [#21092](golang/go#21092)) when unmarshalling into non-empty structs, and this behaviour is unexpected in this library anyway; no other methods do any user-facing unmarshalling or pointer mutation. This commit changes all of the `Update` methods that unmarshall into their inputs to no longer do that, and return a newly-allocated struct instead.
… than unmarshalling into the input encoding/json behaves surprisingly ([#27172](golang/go#27172), [#31924](golang/go#31924), [#26946](golang/go#26946), [#21092](golang/go#21092)) when unmarshalling into non-empty structs, and this behaviour is unexpected in this library anyway; no other methods do any user-facing unmarshalling or pointer mutation. This commit changes all of the `Update` methods that unmarshall into their inputs to no longer do that, and return a newly-allocated struct instead.
What version of Go are you using (
go version
)?What operating system and processor architecture are you using (
go env
)?What did you do?
Link to Play: https://play.golang.org/p/37E1QHWofMy
Passing a pointer to a
struct
(anonymous or not) as typeinterface{}
tojson.Unmarshal
will have the original type replaced with amap[string]interface{}
(correctly decoded, however). It appears the difference between correct and incorrect result is changing:to:
In the example above. A
interface{}
containing a pointer to astruct
works as expected, e.g.:is the same as:
What did you expect to see?
The following results, in order of desirability:
struct
type (Object
, for the example above) populated with data from the JSON object given.json.Unmarshal
.What did you see instead?
None of the above? The use case that led to this issue for me was giving constructors types, where these types would be used as "templates" of sorts for populating with data in subsequent requests. For example:
It is implied, then, that
NewHandler
is called with the bare type:Which will give "fresh" values to the
RequestHandler
each time, as they emerge fromRequest.Decode
. If given a pointer, i.e.:Previous requests will populate the pointer to
User
, leaving garbage data for future requests.Apologies for the perhaps obtuse example.
The text was updated successfully, but these errors were encountered: