go-resty / resty

Simple HTTP and REST client library for Go
MIT License
9.89k stars 696 forks source link

SetResult replaces nested struct with a map inside a map #733

Open maticmeznar opened 10 months ago

maticmeznar commented 10 months ago

In the provided sample code, after a successful HTTP request, result["data"] changes type from getIP to map[string]interface{}. I don't know if this is feature or a bug, but it is unexpected and annoying to use. This does not happen if the outer map is replaced with a struct.

package main

import (
    "encoding/json"
    "log"
    "net/http"
    "net/netip"

    "github.com/davecgh/go-spew/spew"
    "github.com/go-resty/resty/v2"
)

type getIP struct {
    Address string `json:"address"`
    Version int    `json:"version"`
}

const serveAddr = ":48916"

func main() {
    http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
        w.Header().Add("Content-Type", "application/json")
        addr := netip.MustParseAddrPort(r.RemoteAddr).Addr().String()
        result := map[string]any{
            "data": getIP{
                Address: addr,
                Version: 4,
            },
        }

        enc := json.NewEncoder(w)
        if err := enc.Encode(result); err != nil {
            log.Fatalln(err)
        }
    })

    go func() {
        if err := http.ListenAndServe(serveAddr, nil); err != http.ErrServerClosed {
            log.Fatalf("Error starting server: %v\n", err)
        }
    }()

    result := map[string]any{
        "data": getIP{},
    }

    spew.Dump(result)

    r := resty.New().R().SetResult(&result)

    httpResp, err := r.Get("http://localhost" + serveAddr)
    if err != nil {
        log.Fatalf("unable to contact service: %v\n", err)
    }

    if httpResp.IsError() {
        log.Fatalf("error contacting service: %s\n", httpResp.Status())
    }

    spew.Dump(result)
}
kecci commented 10 months ago

Hi @maticmeznar

I trace the result is depends on the JSONUnmarshaler.

The resty client are set the default of JSONUnmarshaler using "encoding/json".

To unmarshal JSON into an interface value, Unmarshal stores one of these in the interface value:

...
map[string]interface{}, for JSON objects
...

Refer to the official documentation on Unmarshal() for more information.

Reproduce

To make this clarify we tried to reproduce.

  1. Reproduce without the json unmarshal.
    ...
    r := resty.New().SetJSONUnmarshaler(func(data []byte, v interface{}) error {
        return nil
    }).R().SetResult(&result)
    ...

Dump result without json unmarshal:

(map[string]interface {}) (len=1) {
 (string) (len=4) "data": (main.getIP) {
  Address: (string) "",
  Version: (int) 0
 }
}
(map[string]interface {}) (len=1) {
 (string) (len=4) "data": (main.getIP) {
  Address: (string) "",
  Version: (int) 0
 }
}
  1. Reproduce with the json unmarshal.
    ...
    r := resty.New().SetJSONUnmarshaler(func(data []byte, v interface{}) error {
        return json.Unmarshal(data, &v)
    }).R().SetResult(&result)
    ...

Dump result with json unmarshal:

(map[string]interface {}) (len=1) {
 (string) (len=4) "data": (main.getIP) {
  Address: (string) "",
  Version: (int) 0
 }
}
(map[string]interface {}) (len=1) {
 (string) (len=4) "data": (map[string]interface {}) (len=2) {
  (string) (len=7) "address": (string) (len=3) "::1",
  (string) (len=7) "version": (float64) 4
 }

Conclusion

The result is depends on JSONUnmarshaler. For now, you could set to more suitable unmarshaler (if you have).

However, I think your concerns could be input for other unmarshaler options. Both from internal and external packages.

Could we consider this ? @jeevatkm

jeevatkm commented 6 months ago

@maticmeznar Thanks for reaching out.
I don't know if this expected result could be achieved the way it is. You could try to define a custom type for the outer map.

@kecci Thanks for responding to the comment. Could you explain what to consider? I'm unable to understand.