go-resty / resty

Simple HTTP and REST client library for Go

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

SetResult replaces nested struct with a map inside a map

maticmeznar opened this issue · comments

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

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

@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.