gin-gonic / gin

Gin is a HTTP web framework written in Go (Golang). It features a Martini-like API with much better performance -- up to 40 times faster. If you need smashing performance, get yourself some Gin.
https://gin-gonic.com/
MIT License
77.95k stars 7.97k forks source link

How to bind to a struct with json and header struct tags both ? #2309

Open kartavya-ramnani opened 4 years ago

kartavya-ramnani commented 4 years ago

Description

I have to place a request in a struct which uses fields from both headers and json body. Is there a way to bind to a struct with json and header struct tags both ? Like a ShouldBindWith which takes Json and header binding both ?

How to reproduce

Code :

package main

import (
    "github.com/gin-gonic/gin"
)

type Request struct {
    AppCode       string `header:"appCode" binding:"required"`
    SomeId string `json:"someId" binding:"required"`
    UserId      string `header:"userId"`
    EmailId       string `json:"emailId"`
}

func bindTest(ctx *gin.Context) {
    var request Request
    err = ctx.ShouldBindJSON(&request)
    if err != nil {
        err = errors.REQUEST_BIND_FAIL(err, request)
        return
    }

    err = ctx.ShouldBindHeader(&request)
    if err != nil {
        err = errors.REQUEST_BIND_FAIL(err, request)
        return
    }
}

Expectations

  1. A common function which does both. or
  2. When binding required with JSON, it checks only the json struct tags and binding required with Headers only check the header struct tags.

Actual result

Currently, Im getting a binding:required error from a field which only has a header tag and not json tag when I do ShouldBindJSON.

linvis commented 4 years ago
type Request struct {
    AppCode string `header:"appCode" json:"-" `
    SomeID  string `header:"-" json:"someId"`
    UserID  string `header:"userId" json:"-"`
    EmailID string `header:"-" json:"emailId"`
}

just use "-" to remove tag that you don't want.

linvis commented 4 years ago

and binding:"required should not be used if you want to ignore one tag

o10g commented 3 years ago

and binding:"required should not be used if you want to ignore one tag

what if I would like to use validation? OP asks how to combine headers and json/form values in one struct. As far as I see it is not possible right now.

ghost commented 3 years ago

I guess you could create an extra function like:

type Request struct {
    AppCode string `header:"appCode" json:"-" `
    SomeID  *string `header:"-" json:"someId"`
    UserID  string `header:"userId" json:"-"`
    EmailID string `header:"-" json:"emailId"`
}
func (r *Request) Validate(context *gin.Context) bool {
 if r.SomeID == nil {
  // do something with context
  context.abort()
  return false
 }
 return true
}

maibe it's not ideal, but i've been working this way for some specific stuff and all went good so far

Emixam23 commented 1 year ago

Hey! I've got the same issue..

func (api *api) UpdateStuff(c *gin.Context) {
        type updateRequest struct {
            User    string `form:"user" binding:"required,oneof=foo bar doe"`
            UserAgent string `header:"User-Agent" binding:"required"`
        }

    var update updateRequest
    if err := c.BindQuery(&update); err != nil {
        _ = c.Error(err)
        c.JSON(http.StatusBadRequest, errorContainer{Error: err.Error()})
        return
    } else if err := c.BindHeader(&update); err != nil {
        _ = c.Error(err)
        c.JSON(http.StatusBadRequest, errorContainer{Error: err.Error()})
        return
    }

        // ...
        c.JSON(http.StatusOK, updateResponse{
        Count: count,
    })
}

and I always get

{"error":"Key: 'updateRequest.UserAgent' Error:Field validation for 'UserAgent' failed on the 'required' tag"}

I tried without the required field but.. Doesn't help as I wished

Thanks

Parsa-Sedigh commented 7 months ago

I think it's not possible to bind a struct which has both json and header (or other tags like uri). Because gin can bind one of them at a time. You can bind header and json fields in two steps like:

type RequestHeaders struct {
    AppCode  string `header:"appCode" binding:"required"`
    UserId      string `header:"userId"`

}

type RequestBody struct {
     SomeId string `json:"someId" binding:"required"`
     EmailId  string `json:"emailId"`
}

func bindTest(ctx *gin.Context) {
        var body RequestBody
    var headers RequestHeaders
    err = ctx.ShouldBindJSON(&body)
    if err != nil {
        err = errors.REQUEST_BIND_FAIL(err, request)
        return
    }

    err = ctx.ShouldBindHeader(&headers)
    if err != nil {
        err = errors.REQUEST_BIND_FAIL(err, request)
        return
    }
}
zaydek commented 5 months ago

I got curious about this and I like this solution because it keeps everything together but it respects that headers and body needs to be bound separately.

package main

import (
  "bytes"
  "net/http"

  "github.com/gin-gonic/gin"
  "github.com/k0kubun/pp"
)

type Request struct {
  Headers struct {
    UserID        int    `header:"user-id" json:"-" binding:"required"`
    Authorization string `header:"Authorization" json:"-" binding:"required"`
  }
  Body struct {
    Body string `json:"body" binding:"required"`
  }
}

func main() {
  router := gin.Default()

  router.POST("/", func(c *gin.Context) {
    // Bind the request
    var req Request
    if err := c.ShouldBindHeader(&req.Headers); err != nil {
      panic(err)
    }
    if err := c.ShouldBindJSON(&req.Body); err != nil {
      panic(err)
    }

    // Debug print the request
    pp.Println(req)
  })

  go func() {
    body := []byte(`{"body":"foo"}`)
    req, err := http.NewRequest("POST", "http://localhost:8080/", bytes.NewBuffer(body))
    if err != nil {
      panic(err)
    }
    req.Header.Set("user-id", "123")
    req.Header.Set("Authorization", "Bearer AccessToken")
    req.Header.Set("Content-Type", "application/json")
    client := &http.Client{}
    resp, err := client.Do(req)
    if err != nil {
      panic(err)
    }
    defer resp.Body.Close()
  }()

  router.Run(":8080")
}

Here's what the output looks like.

Screenshot 2024-03-26 at 4 24 28 AM
zaydek commented 5 months ago

Nevermind that's kind of dumb just do this lol

func (r *Router) AuthLogoutDevice(c *gin.Context) {
  type Request struct {
    UserID             string `json:"-"`
    AuthorizationToken string `json:"-"`
    DeviceID           string `json:"deviceID"`
  }

  // Bind the request
  var request Request
  var err error
  request.UserID = c.GetHeader("user-id")
  request.AuthorizationToken, err = getAuthorizationTokenFromHeaders(c)
  if err != nil {
    r.debugError(c, http.StatusInternalServerError, fmt.Errorf("failed to get authorization token from headers: %w", err))
    return
  }
  if err := c.ShouldBindJSON(&request); err != nil {
    r.debugError(c, http.StatusInternalServerError, fmt.Errorf("failed to bind JSON: %w", err))
    return
  }

  // TODO
}