Skip to content

Commit 26c932b

Browse files
committed
feat: allow custom unmarshalers for form binding: json/text/binary
1 parent de1c4ec commit 26c932b

File tree

3 files changed

+170
-3
lines changed

3 files changed

+170
-3
lines changed

binding/form_mapping.go

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -197,6 +197,18 @@ func setByForm(value reflect.Value, field reflect.StructField, form map[string][
197197
}
198198
}
199199

200+
type jsonUnmarshaler interface {
201+
UnmarshalJSON([]byte) error
202+
}
203+
204+
type textUnmarshaler interface {
205+
UnmarshalText([]byte) error
206+
}
207+
208+
type binaryUnmarshaler interface {
209+
UnmarshalBinary([]byte) error
210+
}
211+
200212
func setWithProperType(val string, value reflect.Value, field reflect.StructField) error {
201213
switch value.Kind() {
202214
case reflect.Int:
@@ -236,6 +248,15 @@ func setWithProperType(val string, value reflect.Value, field reflect.StructFiel
236248
case time.Time:
237249
return setTimeField(val, field, value)
238250
}
251+
if unmarshaler, ok := value.Addr().Interface().(jsonUnmarshaler); ok {
252+
return unmarshaler.UnmarshalJSON(bytesconv.StringToBytes(val))
253+
}
254+
if unmarshaler, ok := value.Addr().Interface().(textUnmarshaler); ok {
255+
return unmarshaler.UnmarshalText(bytesconv.StringToBytes(val))
256+
}
257+
if unmarshaler, ok := value.Addr().Interface().(binaryUnmarshaler); ok {
258+
return unmarshaler.UnmarshalBinary(bytesconv.StringToBytes(val))
259+
}
239260
return json.Unmarshal(bytesconv.StringToBytes(val), value.Addr().Interface())
240261
case reflect.Map:
241262
return json.Unmarshal(bytesconv.StringToBytes(val), value.Addr().Interface())

binding/form_mapping_test.go

Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,9 @@
55
package binding
66

77
import (
8+
"errors"
89
"reflect"
10+
"strings"
911
"testing"
1012
"time"
1113

@@ -288,3 +290,93 @@ func TestMappingIgnoredCircularRef(t *testing.T) {
288290
err := mappingByPtr(&s, formSource{}, "form")
289291
assert.NoError(t, err)
290292
}
293+
294+
// this structure has special json unmarshaller, in order to parse email (as an example) as specific structure
295+
type withJsonUnmarshaller struct {
296+
Name string
297+
Host string
298+
}
299+
300+
func (o *withJsonUnmarshaller) UnmarshalJSON(data []byte) error {
301+
elems := strings.Split(string(data), "@")
302+
if len(elems) != 2 {
303+
return errors.New("cannot parse %q as email")
304+
}
305+
o.Name = elems[0]
306+
o.Host = elems[1]
307+
return nil
308+
}
309+
310+
func TestMappingStructFieldJSONUnmarshaller(t *testing.T) {
311+
var s struct {
312+
Email withJsonUnmarshaller
313+
}
314+
315+
err := mappingByPtr(&s, formSource{"Email": {`[email protected]`}}, "form")
316+
assert.NoError(t, err)
317+
assert.Equal(t, "test", s.Email.Name)
318+
assert.Equal(t, "example.org", s.Email.Host)
319+
320+
err = mappingByPtr(&s, formSource{"Email": {`not an email`}}, "form")
321+
assert.Error(t, err)
322+
}
323+
324+
// this structure has special text unmarshaller, in order to parse email (as an example) as specific structure
325+
type withTextUnmarshaller struct {
326+
Name string
327+
Host string
328+
}
329+
330+
func (o *withTextUnmarshaller) UnmarshalText(data []byte) error {
331+
elems := strings.Split(string(data), "@")
332+
if len(elems) != 2 {
333+
return errors.New("cannot parse %q as email")
334+
}
335+
o.Name = elems[0]
336+
o.Host = elems[1]
337+
return nil
338+
}
339+
340+
func TestMappingStructFieldTextUnmarshaller(t *testing.T) {
341+
var s struct {
342+
Email withTextUnmarshaller
343+
}
344+
345+
err := mappingByPtr(&s, formSource{"Email": {`[email protected]`}}, "form")
346+
assert.NoError(t, err)
347+
assert.Equal(t, "test", s.Email.Name)
348+
assert.Equal(t, "example.org", s.Email.Host)
349+
350+
err = mappingByPtr(&s, formSource{"Email": {`not an email`}}, "form")
351+
assert.Error(t, err)
352+
}
353+
354+
// this structure has special binary unmarshaller, in order to parse email (as an example) as specific structure
355+
type withBinaryUnmarshaller struct {
356+
Name string
357+
Host string
358+
}
359+
360+
func (o *withBinaryUnmarshaller) UnmarshalBinary(data []byte) error {
361+
elems := strings.Split(string(data), "@")
362+
if len(elems) != 2 {
363+
return errors.New("cannot parse %q as email")
364+
}
365+
o.Name = elems[0]
366+
o.Host = elems[1]
367+
return nil
368+
}
369+
370+
func TestMappingStructFieldBinaryUnmarshaller(t *testing.T) {
371+
var s struct {
372+
Email withBinaryUnmarshaller
373+
}
374+
375+
err := mappingByPtr(&s, formSource{"Email": {`[email protected]`}}, "form")
376+
assert.NoError(t, err)
377+
assert.Equal(t, "test", s.Email.Name)
378+
assert.Equal(t, "example.org", s.Email.Host)
379+
380+
err = mappingByPtr(&s, formSource{"Email": {`not an email`}}, "form")
381+
assert.Error(t, err)
382+
}

docs/doc.md

Lines changed: 57 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,7 @@
5757
- [Bind form-data request with custom struct](#bind-form-data-request-with-custom-struct)
5858
- [Try to bind body into different structs](#try-to-bind-body-into-different-structs)
5959
- [Bind form-data request with custom struct and custom tag](#bind-form-data-request-with-custom-struct-and-custom-tag)
60+
- [Bind Query with custom unmarshalers](#bind-query-with-custom-unmarshalers)
6061
- [http2 server push](#http2-server-push)
6162
- [Define format for the log of routes](#define-format-for-the-log-of-routes)
6263
- [Set and get a cookie](#set-and-get-a-cookie)
@@ -1155,7 +1156,7 @@ func main() {
11551156
router.StaticFS("/more_static", http.Dir("my_file_system"))
11561157
router.StaticFile("/favicon.ico", "./resources/favicon.ico")
11571158
router.StaticFileFS("/more_favicon.ico", "more_favicon.ico", http.Dir("my_file_system"))
1158-
1159+
11591160
// Listen and serve on 0.0.0.0:8080
11601161
router.Run(":8080")
11611162
}
@@ -2002,6 +2003,59 @@ func ListHandler(s *Service) func(ctx *gin.Context) {
20022003
}
20032004
```
20042005

2006+
### Bind Query with custom unmarshalers
2007+
2008+
Any structure that has custom `UnmarshalJSON` or `UnmarshalText` or `UnmarshalBinary` can be used to parse input as necessary
2009+
2010+
```go
2011+
package main
2012+
import (
2013+
"fmt"
2014+
"net/http"
2015+
"strings"
2016+
"github.com/gin-gonic/gin"
2017+
)
2018+
// Booking contains data binded using custom unmarshaler.
2019+
type Payload struct {
2020+
Email EmailDetails `form:"email"`
2021+
}
2022+
// this structure has special json unmarshaller, in order to parse email (as an example) as specific structure
2023+
type EmailDetails struct {
2024+
Name string
2025+
Host string
2026+
}
2027+
func (o *EmailDetails) UnmarshalJSON(data []byte) error {
2028+
elems := strings.Split(string(data), "@")
2029+
if len(elems) != 2 {
2030+
return fmt.Errorf("cannot parse %q as email", string(data))
2031+
}
2032+
o.Name = elems[0]
2033+
o.Host = elems[1]
2034+
return nil
2035+
}
2036+
func main() {
2037+
route := gin.Default()
2038+
route.GET("/email", getEmail)
2039+
route.Run(":8085")
2040+
}
2041+
func getEmail(c *gin.Context) {
2042+
var p Payload
2043+
if err := c.ShouldBindQuery(&p); err == nil {
2044+
c.JSON(http.StatusOK, gin.H{"message": "Email information is correct"})
2045+
} else {
2046+
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
2047+
}
2048+
}
2049+
```
2050+
2051+
```console
2052+
$ curl "localhost:8085/[email protected]"
2053+
{"message":"Email information is correct"}
2054+
2055+
$ curl "localhost:8085/email?email=test-something-else"
2056+
{"error":"cannot parse \"test-something-else\" as email"}
2057+
```
2058+
20052059
### http2 server push
20062060

20072061
http.Pusher is supported only **go1.8+**. See the [golang blog](https://go.dev/blog/h2push) for detail information.
@@ -2134,7 +2188,7 @@ or network CIDRs from where clients which their request headers related to clien
21342188
IP can be trusted. They can be IPv4 addresses, IPv4 CIDRs, IPv6 addresses or
21352189
IPv6 CIDRs.
21362190

2137-
**Attention:** Gin trust all proxies by default if you don't specify a trusted
2191+
**Attention:** Gin trust all proxies by default if you don't specify a trusted
21382192
proxy using the function above, **this is NOT safe**. At the same time, if you don't
21392193
use any proxy, you can disable this feature by using `Engine.SetTrustedProxies(nil)`,
21402194
then `Context.ClientIP()` will return the remote address directly to avoid some
@@ -2163,7 +2217,7 @@ func main() {
21632217
```
21642218

21652219
**Notice:** If you are using a CDN service, you can set the `Engine.TrustedPlatform`
2166-
to skip TrustedProxies check, it has a higher priority than TrustedProxies.
2220+
to skip TrustedProxies check, it has a higher priority than TrustedProxies.
21672221
Look at the example below:
21682222

21692223
```go

0 commit comments

Comments
 (0)