Go gin: "required" binding seems not to work for nested structs

35 Views Asked by At

I was trying to bind a nested struct from a json body using gin. The issue I stumbled upon is that the required binding (seemingly) failed to be applied for the nested struct (Nested in the snippet below). However, it works fine for values of type string, int, etc, even if they are nested.

package main

import (
    "fmt"

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

type Nested struct {
    Hello string `json:"hello"`
}

type TransferCreateBody struct {
    TestNested    Nested `json:"nested" binding:"required"`
    TestNotNested string `json:"not_nested" binding:"required"`
}

func main() {
    g := gin.Default()
    g.POST("/hello", func(c *gin.Context) {
        var req TransferCreateBody
        if err := c.ShouldBindJSON(&req); err != nil {
            fmt.Println(err)
            c.JSON(400, gin.H{"error": err.Error()})
            return
        }
        c.JSON(200, gin.H{"hello": req.TestNested.Hello})
    })
    g.Run(":9000")
}
curl -X POST http://localhost:9000/hello
   -H "Content-Type: application/json"
   -d '{"not_nested": "test"}'

How do I make the binding work with a nested struct ?

1

There are 1 best solutions below

0
Pascal Delange On

Ok, I got an explanation - this behavior is due to the fact that the "required" binding is actually checked after the json has been decoded into the target struct. That is, gin will check if the "required" parameter has its default (zero) value or is "properly" filled.

For a nested struct, the following would work:

package main

import (
    "fmt"

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

type Nested struct {
    Hello string `json:"hello"`
}

type TransferCreateBody struct {
    TestNested    *Nested `json:"nested" binding:"required"`
    TestNotNested string `json:"not_nested" binding:"required"`
}

func main() {
    g := gin.Default()
    g.POST("/hello", func(c *gin.Context) {
        var req TransferCreateBody
        if err := c.ShouldBindJSON(&req); err != nil {
            fmt.Println(err)
            c.JSON(400, gin.H{"error": err.Error()})
            return
        }
        c.JSON(200, gin.H{"hello": req.TestNested.Hello})
    })
    g.Run(":9000")
}

because the zero value of *Nested is nil, not an empty instance of Nested. Another possibly confusing consequence is that the required binding will report an error if a required field is passed in the json body, but with its zero value (e.g. {"not_nested": ""} in the snippet above).

This can also be avoided by using pointers everywhere in the structure to bind, by using a struct like sql.NullXXX to bind the values, etc.