Strongly typed JSON/JSONB fields in GORM

July 13, 2022

GORM is a great ORM for Go, but if you’ve ever tried to use JSON/JSONB fields in your database, you’ve probably been frustrated by the lack of automatic serialization into and out of JSON. The only solutions until now that I’m aware of have been to store a []byte field in your GORM model, or use the json.RawMessage wrapper from datatypes/JSON. Neither of those solutions allow you to store a struct inside your GORM model, with transparent JSON serialization. Luckily, Go generics gives us a new tool for solving this problem.

To paint a picture of what we want, let’s imagine a GORM model like this:

type MyRecord struct {
	gorm.Model
	Blob *MyJSONBlob
}

type MyJSONBlob struct {
	// ... Any JSON-serializable struct
}

The reason this doesn’t work out of the box is because GORM doesn’t know how to serialize MyJSONBlob.

The following solution lets us create a new generic type that wraps your struct, and provides the necessary interfaces for serialization into your DB.

But before the code dump, here is a concrete example of what the above GORM model looks like when using this technique:

type MyRecord struct {
	gorm.Model
	Blob *JSONField[MyJSONBlob]
}

type MyJSONBlob struct {
	// ... Any JSON-serializable struct
}

And here is the definition of JSONField:

// JSONField wraps an arbitrary struct so that it can be included in a GORM model, for use in a JSON/JSONB field
type JSONField[T any] struct {
	Data T
}

// Return a copy of 'data', wrapped in a JSONField object
func MakeJSONField[T any](data T) *JSONField[T] {
	return &JSONField[T]{
		Data: data,
	}
}

func (j *JSONField[T]) Scan(src any) error {
	if src == nil {
		var empty T
		j.Data = empty
		return nil
	}
	srcByte, ok := src.([]byte)
	if !ok {
		return errors.New("JSONField underlying type must be []byte (some kind of Blob/JSON/JSONB field)")
	}
	if err := json.Unmarshal(srcByte, &j.Data); err != nil {
		return err
	}
	return nil
}

func (j JSONField[T]) Value() (driver.Value, error) {
	return json.Marshal(j.Data)
}

func (j JSONField[T]) MarshalJSON() ([]byte, error) {
	return json.Marshal(j.Data)
}

func (j *JSONField[T]) UnmarshalJSON(b []byte) error {
	if bytes.Equal(b, []byte("null")) {
		// According to docs, this is a no-op by convention
		//var empty T
		//j.Data = empty
		return nil
	}
	if err := json.Unmarshal(b, &j.Data); err != nil {
		return err
	}
	return nil
}

Note that we also support the MarshalJSON/UnmarshalJSON interface, because this removes the extra Data field when marshalling the GORM model to JSON.

Also, the above definition does not support GORM automatic migrations. I prefer to write them by hand, but you could incorporate the functionality from datatypes/JSON if you want automatic migrations, and various other JSON-related functionality.

I have only tested this with the SQLite and Postgres driver. To make this work with other databases, you might need to add some of the mechanisms from datatypes/JSON