I'm migrating code from JSON to YAML and have run into an issue with UnmarshalYAML recursing endlessly where UnmarshalJSON works.
I have a struct that has UnmarshalJSON defined and further calls into json.Unmarshal to do most of the unmarshaling, but then does some touch-up/validation afterwards.
Naive implementation does cause endless recursion, but a type alias stops it. See live example here: http://play.golang.org/p/St33ZkWvTN
note foo.ok set to true during unmarshaling which was not in the original message. this is convenient.
now, when converted to equivalent code for YAML, it goes into a recursion loop, as YAML unmarshaler seems to treat type aliases differently than JSON.
it would be nice if this use case was supported.
here's a full test program:
package main
import (
"encoding/json"
"fmt"
"os"
yaml "gopkg.in/yaml.v2"
)
type Foo struct {
Bar string
ok bool
}
type fooJSON *Foo
func (f *Foo) UnmarshalJSON(data []byte) error {
fmt.Printf("unmarshal JSON\n")
if err := json.Unmarshal(data, fooJSON(f)); err != nil {
return err
}
f.ok = true
return nil
}
type fooYAML *Foo
var n = 0
func (f *Foo) UnmarshalYAML(unmarshal func(interface{}) error) error {
n += 1
fmt.Printf("unmarshal YAML\n")
if n > 1 {
os.Exit(1)
}
if err := unmarshal(fooYAML(f)); err != nil {
return err
}
f.ok = true
return nil
}
func main() {
var foo1, foo2 Foo
err := json.Unmarshal([]byte(`{"bar": "baz"}`), &foo1)
if err != nil {
panic(err)
}
fmt.Printf("%+v\n", foo1)
err = yaml.Unmarshal([]byte(`{"bar": "baz"}`), &foo2)
if err != nil {
panic(err)
}
fmt.Printf("%+v\n", foo2)
}
and output it produces:
unmarshal JSON
{Bar:baz ok:true}
unmarshal YAML
unmarshal YAML
There's indeed a difference in behavior, but we cannot simply break compatibility to make the behavior of the yaml package match the json package, as that would break the expectation of existing applications trusting on the implemented behavior.
Appreciate this was 4 years ago so the odds aren't great, but.. did you find a work around for this @rojer?
I have exactly the same use case - the default unmarshalling behaviour works well, but I want to add some extra domain-specific validation on top of it.
@mykter i honestly don't remember what i was trying to do at the time and what i ended up doing. sorry.
Hi @mykter I came across this same issue and was able to work around it by casting the receiver pointer to a pointer to the alias type and then Unmarshaling in to the new pointer like so:
func (f *Foo) UnmarshalYAML(unmarshal func(interface{}) error) error {
// Do extra actions
type fooYAML Foo
fy := (*fooYAML)(f)
if err := unmarshal(fy); err != nil {
return err
}
return nil
}
Hope it works for you too!
Most helpful comment
Hi @mykter I came across this same issue and was able to work around it by casting the receiver pointer to a pointer to the alias type and then Unmarshaling in to the new pointer like so:
Hope it works for you too!