-
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: confusing errors when unmarshaling custom types #28189
Comments
Here is the example that produces the ...underfoot message: package main
import (
"encoding/json"
"fmt"
)
type Lang int
const (
LangC = Lang(iota)
LangCPP
)
type Project struct {
OptionMap map[Lang]*Option `json:"options"`
Typedefs []string `json:"typedefs"`
}
type Option struct {
Active bool `json:"active"`
}
func main() {
var pr *Project
err := json.Unmarshal([]byte(data), &pr)
if err != nil {
fmt.Println(err)
// Output: JSON decoder out of sync - data changing underfoot?
// Uncommenting UnmarshalText below gets rid of this problem
} else {
fmt.Println("ok")
}
}
func (lang *Lang) unmarshal(b []byte) error {
var s string
err := json.Unmarshal(b, &s)
if err != nil {
return err
}
switch s {
case "C":
*lang = LangC
case "C++":
*lang = LangCPP
default:
return fmt.Errorf("Unsupported Lang '%s'", s)
}
return nil
}
func (lang *Lang) UnmarshalJSON(b []byte) error {
return lang.unmarshal(b)
}
/*
func (lang *Lang) UnmarshalText(b []byte) error {
return lang.unmarshal(b)
}
/**/
const data = `
{
"options": {
"C": { "active": true },
"C++": { "active": false }
},
"typedefs": [ "hello" ]
}` |
Thanks for raising this issue. The "underfoot" error is definitely wrong, so we can start with that one. |
Manually minimized reproducer below - will continue digging.
|
Change https://golang.org/cl/142518 mentions this issue: |
Given a program as follows: data := []byte(`{"F": { "a": 2, "3": 4 }}`) json.Unmarshal(data, &map[string]map[int]int{}) The JSON package should error, as "a" is not a valid integer. However, we'd encounter a panic: panic: JSON decoder out of sync - data changing underfoot? The reason was that decodeState.object would return a nil error on encountering the invalid map key string, while saving the key type error for later. This broke if we were inside another object, as we would abruptly end parsing the nested object, leaving the decoder in an unexpected state. To fix this, simply avoid storing the map element and continue decoding the object, to leave the decoder state exactly as if we hadn't seen an invalid key type. This affected both signed and unsigned integer keys, so fix both and add two test cases. Updates #28189. Change-Id: I8a6204cc3ff9fb04ed769df7a20a824c8b94faff Reviewed-on: https://go-review.googlesource.com/c/142518 Reviewed-by: Ian Lance Taylor <iant@golang.org>
The underfoot panic is now fixed - the example above prints:
I'll try to look at the other cases before the cycle is over. |
I think this block is just the user's bug. If you add When you comment out
And this, as you'd expect, errors with |
To clarify my comment above: I don't think we can improve those errors, because the nested call to As for using
However, the error could be clearer. Instead of |
Smaller repro for the last case:
Not having carefully read the |
It's not clear that the last remaining case needs a fix, so I'm moving this to 1.13 and marking as NeedsDecision. We could modify Perhaps @rsc has some thoughts. |
However, that doesn't quite match what I'm seeing in the current docs, which have (emphasis added):
Note the addition of "implement json.Unmarshaler" into the list. Note as well that this forms a mismatched pair with a similar note for
I am working with a type that implements Do you concur? Should I open a separate issue for that? |
Also worth noting, when I add an |
Thanks for bringing this up again, @snargleplax. The docs were amended in https://go-review.googlesource.com/c/go/+/188821 to better reflect the existing behavior, without changing any of the code. I reviewed and merged that CL without thinking of this issue. The CL also mentions the likely bug in the logic:
So I think this definitely needs a fix; either clarifying in the docs, or a code fix to call UnmarshalJSON even when UnmarshalText is not defined. |
As for which fix to do; I'm already discarding never calling UnmarshalJSON on keys, because that would likely break existing programs, and because the docs have been saying the opposite for a year. I lean towards fixing the code to reflect the current docs. That is, to call UnmarshalJSON on map keys even UnmarshalText is not defined. I don't think it would break a significant amount of existing code, and it would make the logic more consistent and in line with the docs. |
I am also in favor of fixing the code to match the docs. My absolute druthers would be to also change |
I am all for more consistency in the JSON package, but historically I've repeatedly failed at doing that - too much existing code would break. I think we should start fixing this one, and then perhaps try making encoding more consistent with decoding too. |
Thinking about this more (while helping another team member who tripped over it), I realized the likely reason for the inconsistency between marshal and unmarshal behavior. Namely, not all valid JSON values are valid map keys. So, you can't in general assume it's safe to take something that implements So it seems like there's a good reason for the core design decision there, but it's not the most obvious or ergonomic. I don't have a better idea at the moment besides editing the docs to explicitly call out the asymmetry (and perhaps hint at this rationale, though defending design decisions is not really the purpose of standard library GoDoc, so it's a fine line to walk). At least mentioning it, though, seems like it would give folks more of a chance to realize the potential issue sooner. It's natural enough to walk into this context expecting symmetry as a general principle. |
just ran into this. had to do the below hack to get it working. is this really the advised solution? or is the solution "dont use encoding.TextUnmarshaler with JSON types"? I just want Go to define a simple interface that I can implement with any type for Marshaling, and encoding.TextUnmarshaler seems perfect for that use case. but since the JSON package is clobbering, I cant use it, and instead am forced to come up with my own interface. package main
import (
"encoding/json"
"fmt"
)
type fail struct {
Auth_Token string `json:"authToken"`
}
func (v *fail) UnmarshalText(text []byte) error {
return json.Unmarshal(text, v)
}
type pass struct {
Auth_Token string `json:"authToken"`
}
func (v *pass) UnmarshalText(text []byte) error {
type t pass
var p t
err := json.Unmarshal(text, &p)
if err != nil {
return err
}
*v = pass(p)
return nil
}
func main() {
text := []byte(`{"authToken": "hello world"}`)
{
var v fail
err := v.UnmarshalText(text)
// {} json: cannot unmarshal object into Go value of type *main.fail
fmt.Println(v, err)
}
{
var v pass
err := v.UnmarshalText(text)
// {hello world} <nil>
fmt.Println(v, err)
}
} |
In go1.11
There is a minor issue with custom type unmarshaling using "encoding/json". As far as I understand the documentation (and also looking through the internal code), the requirements are:
When both of these interfaces are supported, everything is fine. Otherwise, the produced error messages are a bit cryptic and in some cases really confusing. I believe, adding a test for
Implements(jsonUnmarshallerType)
insideencoding/json/decode.go: func (d *decodeState) object(v reflect.Value)
will make things more consistent.Here is the code (try commenting out
UnmarshalText
and/or/xorUnmarshalJSON
methods):The text was updated successfully, but these errors were encountered: