gogo-ginsqlc

How to Marshal/Unmarsal user json input to my models?


I am building a REST API with Go, Gin, and sqlc.
I love the DX with generating my models via sqlc, but there are cases where it's not compatible with Gin's c.BindJSON So I have to manually marsal/unmarshal json <-> model.

This is what my code looks like:

// generated by sqlc
// models.go
type Author struct {
    Name         string             `json:"name"`
    Bio          sql.NullTime       `json:"bio"`
}
// controller.go
// Cannot marshal sql.NullTime
var authorParams data.InsertFileAuthorParams
if err := c.BindJSON(&authorParams); err != nil {
  ...
}

// Have to create an "api" struct
type apiAuthor struct {
    Name         string `json:"name"`
    Bio          string `json:"string"`
}

func (a *apiAuthor) toData() data.InsertFileAuthorParams {
    return data.InsertFileAuthorParams{
        Name: a.Name,
        Bio: sql.NullTime{
            Time:  a.Bio,
            Valid: true,
        },
    }
}

// Marshal the user input to "apiAuthor" first
var authorParams apiAuthor
if err := c.BindJSON(&authorParams); err != nil {
 ...
}

// Then convert apiAuthor -> data.InsertFileAuthorParams
data := apiAuthor.toData()

When I return to the data the user, I also need to convert data.Author -> apiAuthor.

Is there a smart way around this? The duplication feels unnecessary and it feels hacky to me. Ideally, I want to provide the logic to c.BindJSON if Gin cannot marshal the user input automatically.

Note that models.go is auto-generated by sqlc, so I don't want to add code to it.


Solution

  • Even though it seems a strange and a double hustle at this point, having structures that are for the database and those that are exposed on the API is actually something you want to do.

    Simple example, let's say you have an ID inside your database structure, but you don't want to leak it. You would create an almost same structure for the API, but without containing the ID.

    That being said. You could implement your own Marshaller and Unmarshaller interface. There, you could setup your sql.NullTime however you wish to parse it.

    Ending up in something like:

    import (
        "database/sql"
        "encoding/json"
        "fmt"
    )
    
    func main() {
    
        a := Author{
            Name: "John Doe",
            Bio:  sql.NullTime{},
        }
    
        marshalledJson, err := json.Marshal(a)
        if err != nil {
            panic(err)
        }
        fmt.Println(string(marshalledJson))
    
        err = json.Unmarshal(marshalledJson, &a)
        if err != nil {
            panic(err)
        }
        fmt.Println(a.Name)
    
        // Prints out:
        // "Encode it how you want"
        // "Encode it how you want"
    }
    
    
    type Author struct {
        Name         string             `json:"name"`
        Bio          sql.NullTime       `json:"bio"`
    }
    
    func (a *Author) UnmarshalJSON(bytes []byte) error {
        a.Name = string(bytes)
        return nil
    }
    
    func (a Author) MarshalJSON() ([]byte, error) {
        var b []byte
        b = append(b, "\"Encode it how you want\""...)
        return b, nil
    }
    

    This is a very basic example of course, but should get you in the right direction.