traefik / yaegi

Yaegi is Another Elegant Go Interpreter
https://pkg.go.dev/github.com/traefik/yaegi
Apache License 2.0
6.94k stars 343 forks source link

wrong JSON representation with generics #1490

Open mpl opened 1 year ago

mpl commented 1 year ago

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.

mpl commented 1 year ago

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

}