Description
Hi, i was trying to bind some json array with form-data but it throws can't unmarshal JSON Array so when i cover it by json object and then it works as i expect
How to reproduce
package main
import (
"github.com/gin-gonic/gin"
)
type CreateParams struct {
Person []Person `form:"person" binding:"dive"`
}
type Person struct {
Firstname string `json:"firstname" binding:"required"`
Lastname string `json:"lastname"`
}
func main() {
g := gin.Default()
g.GET("/foo", func(c *gin.Context) {
var createParams CreateParams
c.ShouldBind(&createParams)
c.JSON(http.StatusOK, gin.H{"request" : createParams})
})
g.Run(":9000")
}
Expectations
$ curl 0:4000 -X POST -d 'person=[{"firstname" : "spider", "lastname" : "man"},{"firstname" : "spider", "lastname" : "lady"}]'
{
"request": [
{
"firstname": "spider",
"lastname": "man"
},
{
"firstname": "spider",
"lastname": "lady"
]
}
Actual result
$ curl 0:4000 -X POST -d 'person=[{"firstname" : "spider", "lastname" : "man"},{"firstname" : "spider", "lastname" : "lady"}]'
json: cannot unmarshal array into Go value of type Person
Environment
- go version:1.19.3
- gin version (or commit ref):1.8.1
- operating system:macos
Comment From: woojiahao
I'm curious, why would you want to mix both JSON and form-data in a request body? Is this a limitation of the frontend? To my knowledge, Gin is not able to bind across different body forms.
If you altered CreateParams
, you will see that Gin likely treats the value of the form-data as a string and as such, cannot bind it as a JSON array.
type CreateParams struct {
Person string `form:"person"`
}
{
"request": {
"Person": "[{\"firstname\" : \"spider\", \"lastname\" : \"man\"},{\"firstname\" : \"spider\", \"lastname\" : \"lady\"}]"
}
}
Might I suggest altering how your requests are being made? So that you're not combining these two forms of input.
If you must use this multi-form body, you could parse the initial body as a form with a string to represent the JSON array and then unmarshal it separately:
type CreateParams struct {
Person string `form:"person"`
}
type Person struct {
Firstname string `form:"firstname" json:"firstname" binding:"required"`
Lastname string `form:"lastname" json:"lastname"`
}
func Run() {
g := gin.Default()
g.POST("/foo", func(c *gin.Context) {
var createParams CreateParams
err := c.ShouldBind(&createParams)
if err != nil {
fmt.Println(err)
}
var people []Person
json.Unmarshal([]byte(createParams.Person), &people)
c.JSON(http.StatusOK, gin.H{"request": people})
})
g.Run(":9000")
}
Which yields the intended results:
{
"request": [
{
"firstname": "spider",
"lastname": "man"
},
{
"firstname": "spider",
"lastname": "lady"
}
]
}
But I think this is an extra layer of unnecessary effort. As if you continue using this multi-form request body, you'll have to replicate this behavior across the API
Comment From: hasunpark
Hi, thanks for answer! In my case i have to send multiple users data, i can implement this by sending user data by loop. But i don't wan't to make multiple api calls and this is all related job so have to make it transaction. Finally i'm just curious why binding of json object works and json array binding doesn't works, i thought it was technical issue
Comment From: woojiahao
Could you not parse the multi-user data into a JSON array prior to sending it to the API? 🤔
Finally i'm just curious why binding of json object works and json array binding doesn't work
What do you mean by this? Are you referring to my alternative?
Comment From: hasunpark
I think i make you confused because of my poor english skill. Im sorry about that 😭
Could you not parse the multi-user data into a JSON array prior to sending it to the API?
Yes, i can. There are no problem in that phase
What do you mean by this? Are you referring to my alternative?
Nope, i'm not referring your alternative. it is really nice solution.
The problem is when i send json object by form-data and try to bind with go-struct it successfully works! But when i try to bind json array it doesn't works. I'm curious about what makes these differences
Working Example
package main
import (
"github.com/gin-gonic/gin"
)
type CreateParams struct {
Person Person `form:"person" binding:"dive"`
}
type Person struct {
Firstname string `json:"firstname" binding:"required"`
Lastname string `json:"lastname"`
}
func main() {
g := gin.Default()
g.GET("/foo", func(c *gin.Context) {
var createParams CreateParams
c.ShouldBind(&createParams)
c.JSON(http.StatusOK, gin.H{"request" : createParams})
})
g.Run(":9000")
}
$ curl 0:4000 -X POST -d 'person={"firstname" : "spider", "lastname" : "man"}'
Comment From: kevin19930919
I tried with your code, seems it's ok to bind with array contain with json.
But there are few things makes me confused:
- You curl with POST
, but your original code implement API with GET
- You didn't use -F to declare you want to curl with formdata, not sure is that what u want?
package main
import (
"fmt"
"net/http"
"github.com/gin-gonic/gin"
)
type CreateParams struct {
Person []Person `form:"person" binding:"dive"`
}
type Person struct {
Firstname string `json:"firstname" binding:"required"`
Lastname string `json:"lastname"`
}
func main() {
g := gin.Default()
g.POST("/foo", func(c *gin.Context) {
var createParams CreateParams
if err := c.ShouldBind(&createParams); err != nil {
fmt.Println("error: ", err)
}
fmt.Println("binding result", createParams)
c.JSON(http.StatusOK, gin.H{"request": createParams})
})
g.Run(":9000")
}
This is my curl code, could u try it to see is it work as u want?
curl --request POST \
--url http://0.0.0.0:9000/foo \
--header 'Content-Type: multipart/form-data' \
--form 'person={"firstname": "spider","lastname": "man"}' \
--form 'person={"firstname": "spider","lastname": "man"}'
Comment From: hasunpark
I tried with your code, seems it's ok to bind with array contain with json. But there are few things makes me confused:
- You curl with
POST
, but your original code implement API withGET
- You didn't use -F to declare you want to curl with formdata, not sure is that what u want?
```go package main
import ( "fmt" "net/http"
"github.com/gin-gonic/gin" )
type CreateParams struct { Person []Person
form:"person" binding:"dive"
}type Person struct { Firstname string
json:"firstname" binding:"required"
Lastname stringjson:"lastname"
}func main() { g := gin.Default() g.POST("/foo", func(c *gin.Context) { var createParams CreateParams
if err := c.ShouldBind(&createParams); err != nil { fmt.Println("error: ", err) } fmt.Println("binding result", createParams) c.JSON(http.StatusOK, gin.H{"request": createParams})
}) g.Run(":9000") } ```
This is my curl code, could u try it to see is it work as u want?
curl --request POST \ --url http://0.0.0.0:9000/foo \ --header 'Content-Type: multipart/form-data' \ --form 'person={"firstname": "spider","lastname": "man"}' \ --form 'person={"firstname": "spider","lastname": "man"}'
Thanks for your solution, i thought have to send
--form person=[{"firstname" : "spider", "lastname": "man"}]
like this if i wan't to send json array, but your solution works. my mistake thanks.
what you makes confused is i think from just copy & pasting sry :(
Comment From: kevin19930919
I tried with your code, seems it's ok to bind with array contain with json. But there are few things makes me confused:
- You curl with
POST
, but your original code implement API withGET
- You didn't use -F to declare you want to curl with formdata, not sure is that what u want?
```go package main
import ( "fmt" "net/http"
"github.com/gin-gonic/gin"
)
type CreateParams struct { Person []Person
form:"person" binding:"dive"
}type Person struct { Firstname string
json:"firstname" binding:"required"
Lastname stringjson:"lastname"
}func main() { g := gin.Default() g.POST("/foo", func(c *gin.Context) { var createParams CreateParams
if err := c.ShouldBind(&createParams); err != nil { fmt.Println("error: ", err) } fmt.Println("binding result", createParams) c.JSON(http.StatusOK, gin.H{"request": createParams}) }) g.Run(":9000")
} ```
This is my curl code, could u try it to see is it work as u want?
curl --request POST \ --url http://0.0.0.0:9000/foo \ --header 'Content-Type: multipart/form-data' \ --form 'person={"firstname": "spider","lastname": "man"}' \ --form 'person={"firstname": "spider","lastname": "man"}'
Thanks for your solution, i thought have to send
--form person=[{"firstname" : "spider", "lastname": "man"}]
like this if i wan't to send json array, but your solution works. my mistake thanks.what you makes confused is i think from just copy & pasting sry :(
glad this solved your problem