• With issues:
  • Use the search tool before opening a new issue.
  • Please provide source code and commit sha if you found a bug.
  • Review existing issues and provide feedback or react to them.

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.

Comment From: linvis

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.

Comment From: linvis

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

Comment From: o10g

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.

Comment From: ghost

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

Comment From: Emixam23

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

Comment From: Parsa-Sedigh

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

Comment From: zaydek

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

Comment From: zaydek

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
}

Comment From: VILJkid

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.

```go 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

I think segregation is the key. I was refactoring my code and then found this. Will check and see how it works out. Thanks for the idea @zaydek!

Comment From: VILJkid

It worked perfectly @zaydek! 🥳 Populating values in a single struct made so many things simpler!

Comment From: appleboy

I have closed the issue. Please don't hesitate to reopen it if you encounter any more problems. Thanks @zaydek