This is my application:
func main() {
router := gin.Default()
router.POST("user", func(c *gin.Context) {
type address struct {
City string `form:"city"`
Country string `form:"country"`
}
type user struct {
Name string `form:"name"`
Addresses []address `form:"addresses"`
}
var payload user
err := c.Bind(&payload)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{})
return
}
file, err := c.FormFile("image")
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{})
return
}
fmt.Printf("TODO: save user image %s\n", file.Filename)
fmt.Printf("TODO: save user %s with addresses %d\n", payload.Name, len(payload.Addresses))
})
router.Run(":8080")
}
This is my request:
curl --location 'http://localhost:8080/user' \
--form 'name="John Smith"' \
--form 'addresses[0][city]="London"' \
--form 'addresses[0][country]="United Kingdom"' \
--form 'image=@"/Users/me/Documents/app/fixtures/user.jpeg"'
The problem is that addresses are nil. I'm not able to find example of how to correctly deserialize multipart request with embedded slice data. what am I doing wrong?
update:
I exported the curl command from Postman. apparently, the POST method is implicit if you have "--form" parameter. here are logs from my app:
TODO: save user image user.jpeg
TODO: save user John Smith with addresses 0
update 2:
Turns out that sending array like this is wrong:
--form 'addresses[0][city]="London"'
What gin supports by default is sending multiple fields with the same name, which then will be turned into an array:
--form 'addresses=... --form 'addresses=...
so that led me to a somewhat hacky solution of passing each address as query parameters and then parsing them individually:
request:
curl --location 'http://localhost:8080/user' \
--form 'name="John Smith"' \
--form 'addresses="?city=London&country=UK"' \
--form 'addresses="?city=Berlin&country=Germany"'
--form 'image=@"/Users/foo/bar/baz/user.jpeg"' \
the app:
func main() {
router := gin.Default()
router.POST("user", func(c *gin.Context) {
type address struct {
City string `form:"city"`
Country string `form:"country"`
}
type user struct {
Name string `form:"name"`
Addresses []string `form:"addresses"` // collection_format:"multi"
}
var payload user
err := c.Bind(&payload)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{})
return
}
var addresses []address
for _, path := range payload.Addresses {
parsed, err := url.Parse(path)
if err != nil {
continue
}
query := parsed.Query()
addresses = append(addresses, address{
City: query.Get("city"),
Country: query.Get("country"),
})
}
file, err := c.FormFile("image")
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{})
return
}
fmt.Printf("TODO: save user image %s\n", file.Filename)
fmt.Printf("TODO: save user %s with addresses %d\n", payload.Name, len(addresses))
})
router.Run(":8080")
}
So please, if someone have a better solution please let me know.
One Solution is instead of defining addresses as slice of string define it as slice of address and pass each address as json instead of query params.
request -
curl --location 'http://localhost:8080/getForm' \
--form 'name ="Zargam"' \
--form 'addresses="{\"city\":\"Delhi\",\"country\":\"India\"}"' \
--form 'addresses="{\"city\":\"Shanghai\",\"country\":\"China\"}"'
Code-
package main
import (
"encoding/json"
"net/http"
"github.com/gin-gonic/gin"
)
type address struct {
City string `form:"city"`
Country string `form:"country"`
}
type user struct {
Name string `form:"name"`
Addresses []address `form:"addresses"` // collection_format:"multi"
}
func processForm(c *gin.Context) {
var payload user
err := c.Bind(&payload)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{})
return
}
payloadByte, _ := json.Marshal(payload)
c.JSON(http.StatusOK, gin.H{"message": "success", "data": string(payloadByte)})
return
}
func main() {
r := gin.Default()
r.POST("/getForm", processForm)
r.Run() // listen and serve on 0.0.0.0:8080 (for windows "localhost:8080")
}