yaegi
yaegi copied to clipboard
wrong JSON representation with generics
The following program sample.go
triggers an unexpected result
package main
import (
"bytes"
"encoding/json"
"errors"
"fmt"
"reflect"
)
func unmarshalJSON[T any](b []byte, x *[]T) error {
if *x != nil {
return errors.New("already initialized")
}
if len(b) == 0 {
return nil
}
return json.Unmarshal(b, x)
}
type Slice[T any] struct {
ж []T
}
func (v Slice[T]) MarshalJSON() ([]byte, error) { return json.Marshal(v.ж) }
func (v *Slice[T]) UnmarshalJSON(b []byte) error { return unmarshalJSON(b, &v.ж) }
func StructOfSlice[T any](x []T) Slice[T] {
return Slice[T]{x}
}
type viewStruct struct {
Int int
Strings Slice[string]
StringsPtr *Slice[string] `json:",omitempty"`
}
func main() {
ss := StructOfSlice([]string{"bar"})
in := viewStruct{
Int: 1234,
Strings: ss,
StringsPtr: &ss,
}
var buf bytes.Buffer
encoder := json.NewEncoder(&buf)
encoder.SetIndent("", "")
err1 := encoder.Encode(&in)
b := buf.Bytes()
var got viewStruct
err2 := json.Unmarshal(b, &got)
println(err1 == nil, err2 == nil, reflect.DeepEqual(got, in))
fmt.Println(string(b))
println(string(b))
}
// Output:
// true true true
// {"Int":1234,"Strings":["bar"],"StringsPtr":["bar"]}
//
// {"Int":1234,"Strings":["bar"],"StringsPtr":["bar"]}
Expected result
% go run ./sample.go
true true true
{"Int":1234,"Strings":["bar"],"StringsPtr":["bar"]}
{"Int":1234,"Strings":["bar"],"StringsPtr":["bar"]}
Got
% yaegi run ./sample.go
true true true
{"Int":1234,"Strings":{"Xж":["bar"]},"StringsPtr":{"Xж":["bar"]}}
{"Int":1234,"Strings":{"Xж":["bar"]},"StringsPtr":{"Xж":["bar"]}}
Yaegi Version
on top of https://github.com/traefik/yaegi/pull/1489
Additional Notes
Not sure if it's "only" a representation problem (some funkiness with the Stringer implementation?), or if it's actually a problem with the way yaegi handles the JSON encoding itself.
So it does not seem like "just" a representation problem. For example, if one runs the following program once with Go, then with Yaegi (or vice-versa), the second run fails, because the JSON representation cannot be decoded.
package main
import (
"encoding/json"
"errors"
"io/fs"
"io/ioutil"
"os"
"reflect"
)
func unmarshalJSON[T any](b []byte, x *[]T) error {
if *x != nil {
return errors.New("already initialized")
}
if len(b) == 0 {
return nil
}
return json.Unmarshal(b, x)
}
type Slice[T any] struct {
ж []T
}
func (v Slice[T]) MarshalJSON() ([]byte, error) { return json.Marshal(v.ж) }
func (v *Slice[T]) UnmarshalJSON(b []byte) error { return unmarshalJSON(b, &v.ж) }
func StructOfSlice[T any](x []T) Slice[T] {
return Slice[T]{x}
}
type viewStruct struct {
Int int
Strings Slice[string]
StringsPtr *Slice[string] `json:",omitempty"`
}
func main() {
ss := StructOfSlice([]string{"bar"})
in := viewStruct{
Int: 1234,
Strings: ss,
StringsPtr: &ss,
}
thefile := "/Users/mpl/generics.json"
if _, err := os.Stat(thefile); err != nil {
if !errors.Is(err, fs.ErrNotExist) {
panic(err)
}
println("WRITING FILE")
f, err := os.Create(thefile)
if err != nil {
panic(err)
}
defer f.Close()
encoder := json.NewEncoder(f)
encoder.SetIndent("", "")
if err1 := encoder.Encode(&in); err1 != nil {
panic(err)
}
return
}
println("READING FILE")
data, err := ioutil.ReadFile(thefile)
if err != nil {
panic(err)
}
var got viewStruct
if err2 := json.Unmarshal(data, &got); err2 != nil {
panic(err2)
}
println(reflect.DeepEqual(got, in))
}