Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Open
kartavya-ramnani opened this issue Mar 31, 2020 · 8 comments
Open

Comments

@kartavya-ramnani
Copy link

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

@linvis
Copy link
Contributor

linvis commented Apr 8, 2020

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
Copy link
Contributor

linvis commented Apr 8, 2020

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

@o10g
Copy link

o10g commented Apr 14, 2021

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
Copy link

ghost commented Jun 30, 2021

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
Copy link

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
Copy link

Parsa-Sedigh commented Feb 10, 2024

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
Copy link

zaydek commented Mar 26, 2024

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
Copy link

zaydek commented Mar 26, 2024

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
}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

6 participants