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

parse .1 in query param form to decimal.Decimal #4089

Open
zenyusy opened this issue Nov 8, 2024 · 1 comment
Open

parse .1 in query param form to decimal.Decimal #4089

zenyusy opened this issue Nov 8, 2024 · 1 comment

Comments

@zenyusy
Copy link

zenyusy commented Nov 8, 2024

Description

Gin receives HTTP GET call like 127.0.0.1:8000/foo?a=.1, where a is expected to be decimal number. The handling part has defined a struct with a decimal.Decimal field to receive this a. (URL being "a=0.1" vs "a=.1", the result will be different.)

the actual part to look into is BindQuery, the calling chain is a bit long, i think the important functions are

  1. https://github.com/gin-gonic/gin/blob/c8a3adc65703d8958265c07689662e54f037038c/binding/query.go (Parse URL to {"a":[".1"]} map)

  2. func setByForm(value reflect.Value, field reflect.StructField, form map[string][]string, tagValue string, opt setOptions) (isSet bool, err error) {
    (Try to parse ".1" and assign to the destination field)

In the setByForm fn, it goes to default branch, and attempt of trySetCustom is not effective, so finally go to setWithProperType:

the receiving/destination variable is decimal.Decimal, which is a struct, so go to case reflect.Struct in setWithProperType,
then json.Unmarshal

THE BAD THING is .1 is not a valid JSONstring. (0.1 or ".1" are valid JSONstring)

I note inside case reflect.Struct, 2 struct types are specially handled. so regarding my use case, is it a typical one where trySetCustom is supposed to be used? Or, passing query a=.1 is bad practice?

How to reproduce

Gin code:

package main

import (
	"net/http"

	"github.com/gin-gonic/gin"
	"github.com/shopspring/decimal"
)

func main() {
	router := gin.Default()
	router.GET("/foo", func(c *gin.Context) {
		var req struct {
			A decimal.Decimal `form:"a"`
		}
		if err := c.BindQuery(&req); err != nil {
			c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
			return
		}
	})
	router.Run(":8000")
}

127.0.0.1:8000/foo?a=.1

Expectations

no error

Actual result

error

Environment

  • go version: 1.22.3
  • gin version (or commit ref): c8a3adc
  • operating system: x86_64 linux
@ksw2000
Copy link

ksw2000 commented Nov 11, 2024

The setByForm function calls trySetCustom to check if the binding fields implement the BindUnmarshaler interface. However, decimal.Decimal does not implement this interface by default.

Fortunately, you can still implement the interface BindUnmarshaler on your own.

// BindUnmarshaler is the interface used to wrap the UnmarshalParam method.
type BindUnmarshaler interface {
	// UnmarshalParam decodes and assigns a value from an form or query param.
	UnmarshalParam(param string) error
}

Example:

package main

import (
	"fmt"
	"net/http"

	"github.com/gin-gonic/gin"
	"github.com/shopspring/decimal"
)

type myDecimal decimal.Decimal

func (d *myDecimal) UnmarshalParam(val string) error {
	return (*decimal.Decimal)(d).UnmarshalJSON([]byte(val))
}

func main() {
	router := gin.Default()
	router.GET("/foo", func(c *gin.Context) {
		var req struct {
			A myDecimal `form:"a"`
		}
		if err := c.BindQuery(&req); err != nil {
			c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
			return
		} else {
			fmt.Println((decimal.Decimal)(req.A))
		}
	})
	router.Run(":8000")
}
[GIN-debug] Listening and serving HTTP on :8000
0.1
[GIN] 2024/11/11 - 14:52:00 | 200 |     156.327µs |       127.0.0.1 | GET      "/foo?a=.1"

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

2 participants