traefik / yaegi

Yaegi is Another Elegant Go Interpreter

Home Page:https://pkg.go.dev/github.com/traefik/yaegi

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

wrong JSON representation with generics

mpl opened this issue · comments

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 #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))

}