I recently had to do some work that required I store data in JSON into the database. The options I had available to use were PostgreSQL’s JSON or JSONB column data type. I chose JSONB because it is more recent and has some advantages over the JSON column type


I used GORM as my ORM of choice on this project, so I had to find out if GORM had support for the JSONB type. GORM does have support for the type, however it is not out-of-the-box.


One has to import a custom data type (https://gorm.io/docs/data_types.html). It was a bit of a hassle to get it to work seamlessly like other field types, i.e passing a struct and GORM automagically maps the struct key names to the table columns, etc.

Below is how to go about it.

  1. Firstly go get the custom data type. For this we would be using one of the custom datatypes provided by GORM. Run
    go get "gorm.io/datatypes"

Specifically, we are interested in using the JSONMap type under package datatypes . Find the declaration below:

package datatypes
type JSONMap map[string]interface{}

As we can see, this type works with JSON data using a map of strings, i.e "keyname" => value.


Moving on, we can now proceed to set the key name of the struct that is passed to *gorm.DB.AutoMigrate() . For this, we are going to use an arbitrary User struct which represents a `users’ table.

package models

import (
	"gorm.io/datatypes"
	"gorm.io/gorm"
)


type User struct{
    Name string
    Age uint
    Preferences datatypes.JSONMap `gorm:"default:'[]'; null"`
}

Notice that the Preferences field has its type set to datatypes.JSONMap . You may ask, since under the hood, datatypes.JSONMap is simply a type declaration of map[string]interface{} why not just use map[string]interface{} directly ?? That’s a good question.

The unique thing about custom data types is the implementation of the Scanner and Valuer interfaces, so, there is extra action that goes on to process any field that uses any of the datatypes, in this case datatypes.JSONMap that the regular map[string]interface{} does not have.

Now, that this is done, after running *gorm.DB.AutoMigrate(&models.User) you should notice that the preferences colummn in the database has a type of jsonb. We can now use and reference the Preferences column as you would any other data type, but keep in mind that the type of data inserted into the preferences column must always be in the format map[string]interface{}


This was really smooth and easy to use as the other approaches I saw were more complicated than this. This is easier because under the hood, the GORM custom data type that we imported implements methods that do the marshaling and unmarshaling of the JSON see here

// package datatypes, file:json_map.go
// Scan scan value into Jsonb, implements sql.Scanner interface
func (m *JSONMap) Scan(val interface{}) error {
	if val == nil {
		*m = make(JSONMap)
		return nil
	}
	var ba []byte
	switch v := val.(type) {
	case []byte:
		ba = v
	case string:
		ba = []byte(v)
	default:
		return errors.New(fmt.Sprint("Failed to unmarshal JSONB value:", val))
	}
	t := map[string]interface{}{}
	err := json.Unmarshal(ba, &t)
	*m = t
	return err
}

" The customized data type has to implement the Scanner and Valuer interfaces, so GORM knowns to how to receive/save it into the database "

So, essentially, with this we can implement our own custom data types if need be. I hope this was of help to you. Hopefully it is easier for someone that needs to do this. Till next time 😁🙌🏾.