Open-source project
go-gorm/datatypes avatar
go-gorm/datatypes

gorm datatypes: JSON columns, dates and UUIDs for GORM

GORM Customized Data Types Collection

831 stars126 forksGoMIT

At a glance

What is it?
The gorm.io/datatypes package is a small MIT licensed collection of column types for GORM, covering a JSON document type with query and update helpers, a typed JSON variant, date and time columns, UUID, binary UUID, URL and a NULL type, across MySQL, PostgreSQL, SQLite and SQL Server. It suits a Go team that has run into the places where the default mapping does not fit, and it is a poor fit if you want a full ORM extension rather than a set of column types.
Who is it for?
gorm datatypes suits a Go team that stores semi structured attributes in a relational database and needs to query and update them without hand writing dialect specific SQL, since the JSON query and set helpers generate the MySQL, PostgreSQL and SQLite forms for you.
Can I use it commercially?
Yes. MIT is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
Is it still maintained?
Yes. The repository last received commits 39 days ago.
What is it written in?
Mainly Go, according to GitHub's language statistics.

Answers come from the project's GitHub data, last synced on October 25, 2026, and from our analysis. They are not legal advice.

Editorial analysis

Column types, not an ORM extension

The repository is a collection of customized data types for GORM, and it is worth being clear about the scope.

The module is gorm.io/datatypes, the licence is MIT, and the primary language is Go. The go.mod declares Go 1.19 as the language version and depends on gorm.io/gorm at version 1.30.0 along with the MySQL, PostgreSQL, SQLite and SQL Server drivers, so it is versioned against the current GORM line rather than pinned to an old one.

The repository root is a flat list of files, and each file is one type: json.go, json_map.go, json_type.go, date.go, time.go, uuid.go, binuuid.go, url.go and null.go, each with a matching test file, plus a test_all.sh script that runs the suite.

That flatness is the design. There is no framework here, no plugin registration, no configuration. You add a field to a struct, give it one of these types, and GORM knows how to write and read it. The documentation for the wider set of data types lives in the GORM documentation site, on the data types page, and this repository is the implementation behind the types that documentation describes.

The audience is a Go developer using GORM who has hit a specific friction: a column whose type the default mapping does not handle well. That is a narrower and more practical audience than a general ORM user, and it is the right frame for judging this package, because the value of each type is in whether it does exactly one awkward thing correctly across several databases.

The JSON type is a byte slice, and the queries are per dialect

The JSON section is the core of the package. The type is datatypes.JSON, which is a []byte underneath, so you can write a document directly.

go
import "gorm.io/datatypes"

type UserWithJSON struct {
	gorm.Model
	Name       string
	Attributes datatypes.JSON
}

DB.Create(&UserWithJSON{
	Name:       "json-1",
	Attributes: datatypes.JSON([]byte(`{"name": "jinzhu", "age": 18, "tags": ["tag1", "tag2"], "orgs": {"orga": "orga"}}`)),
})

The interesting part is the query helper. datatypes.JSONQuery builds a condition on a JSON path, and the README shows the SQL it produces for each dialect.

go
db.Find(&user, datatypes.JSONQuery("attributes").HasKey("role"))
db.Find(&user, datatypes.JSONQuery("attributes").HasKey("orgs", "orga"))

On MySQL that becomes a JSON_EXTRACT on the path being not null, and on PostgreSQL it becomes a jsonb existence check with the ? operator. The same call in Go produces different SQL depending on the driver, and that is the point of the helper.

The same applies to value comparison.

go
DB.First(&user, datatypes.JSONQuery("attributes").Equals("jinzhu", "name"))
DB.First(&user, datatypes.JSONQuery("attributes").Equals("orgb", "orgs", "orgb"))

Which becomes JSON_EXTRACT on MySQL and json_extract_path_text on PostgreSQL.

The database support line for the JSON section is sqlite, mysql and postgres, and there is a note attached to SQLite specifically: it needs to be built with the json1 tag, for example go build --tags json1, with a pointer to the go-sqlite3 usage documentation. That is a build-time requirement on your binary, not something the library can fix for you, and it is the kind of detail that produces a confusing failure on a machine where the tag was forgotten.

JSONSet updates a path instead of rewriting the document

Writing to a JSON column has the same problem as querying it, and the package solves it with JSONSet.

The builder takes a column name and a set of path and value pairs, and produces an update expression.

go
datatypes.JSONSet("attributes").Set("age", 20).Set("tags[0]", "tag2").Set("orgs.orga", "orgb")

DB.Model(&UserWithJSON{}).Where("name = ?", "json-1").UpdateColumn("attributes", datatypes.JSONSet("attributes").Set("age", 20).Set("tags[0]", "tag3").Set("orgs.orga", "orgb"))

On MySQL the generated statement is an UPDATE with a JSON_SET call listing each path and value, which means the database performs the merge. The alternative, reading the document, unmarshalling it into a Go map, changing one field and writing it back, has a race: two concurrent updates to different fields of the same row, one of which is silently lost.

The builder also handles the awkward cases. Setting a whole slice of strings and setting a Go struct are both supported, and for the slice case the README shows the CAST form that MySQL needs to turn a string into a JSON document.

go
DB.Model(&UserWithJSON{}).Where("name = ?", "json-1").UpdateColumn("attributes", datatypes.JSONSet("attributes").Set("phones", []string{"10085", "10086"}))
DB.Model(&UserWithJSON{}).Where("name = ?", "json-1").UpdateColumn("attributes", datatypes.JSONSet("attributes").Set("phones", gorm.Expr("CAST(? AS JSON)", `["10085", "10086"]`)))

And there is a warning attached to that: MariaDB does not support CAST(? AS JSON). So the same code that works on MySQL can fail on MariaDB, which is a common enough production substitution that it is worth checking your target before you rely on it.

PostgreSQL paths are a different language

This is the single most likely source of confusion when you move a JSON update between dialects, and the README handles it in a separate section with a single sentence: the path in PostgreSQL is different.

The MySQL style is a JSONPath expression, with a dollar sign and a dotted path.

go
datatypes.JSONSet("attributes").Set("age", 20).Set("tags[0]", "tag2").Set("orgs.orga", "orgb")

The PostgreSQL style is a path array, in braces, with the elements separated by commas.

go
datatypes.JSONSet("attributes").Set("{age}", 20).Set("{tags, 0}", "tag2").Set("{orgs, orga}", "orgb")

The generated SQL shows why the two are not interchangeable, since PostgreSQL gets nested JSONB_SET calls, one per path, built from the left.

go
// UPDATE "user_with_jsons" SET "attributes" = JSONB_SET(JSONB_SET(JSONB_SET("attributes", '{age}', '20'), '{tags, 0}', '"tag2"'), '{orgs, orga}', '"orgb"') WHERE name = 'json-1'

The same difference applies to the cast for setting a raw JSON value, where PostgreSQL uses the ?::jsonb form rather than the CAST expression that MySQL needs.

go
DB.Model(&UserWithJSON{}).Where("name = ?", "json-1").UpdateColumn("attributes", datatypes.JSONSet("attributes").Set("{phones}", gorm.Expr("?::jsonb", `["10085", "10086"]`)))

So the path syntax is a per dialect decision, and if your codebase targets both you will find yourself writing the same update twice. That is a real cost of the portable approach, and it is worth weighing against simply using the native operators of whichever database you actually run.

Date and Time columns, and the SQLite TEXT workaround

The date and time types exist because Go's time.Time does not map cleanly onto SQL date and time columns in every database.

The Date type is a date without a time component, and the README shows it being created and then queried with the same value.

go
import "gorm.io/datatypes"

type UserWithDate struct {
	gorm.Model
	Name string
	Date datatypes.Date
}

user := UserWithDate{Name: "jinzhu", Date: datatypes.Date(time.Now())}
DB.Create(&user)
// INSERT INTO `user_with_dates` (`name`,`date`) VALUES ("jinzhu","2020-07-17 00:00:00")

The Time type carries an hour, minute and second, and the README shows it being constructed with datatypes.NewTime and stored as a time value.

go
user := UserWithTime{Name: "jinzhu", Time: datatypes.NewTime(1, 2, 3, 0)}
DB.Create(&user)
// INSERT INTO `user_with_times` (`name`,`time`) VALUES ("jinzhu","01:02:03")

The support line for Time is wider than for JSON, covering MySQL, PostgreSQL, SQLite and SQL Server, and the README adds that time with nanoseconds is supported for the databases that support a fractional second scale.

The SQLite caveat is stated explicitly. If the database in use is SQLite, the field column type is defined as TEXT when GORM AutoMigrate runs, because SQLite has no time type. So on SQLite the column is a string, and any index, comparison or aggregation you write against it has to remember that.

One implementation detail is visible in the go.mod rather than the README, and it explains the Date type's behaviour. The module depends on github.com/jinzhu/now, which is the library that normalises a time to the start of its day, and that is why the inserted date is 2020-07-17 00:00:00 rather than the current time of day.

JSONType puts a Go struct inside the column

The newest addition to the collection is the generic JSONType, and it addresses the sharpest edge of storing raw JSON.

With datatypes.JSON, the document is bytes and the compiler cannot help you. The key names are strings that you get wrong at runtime, and a typo in a path is a query that silently matches nothing.

JSONType takes a type parameter, so the document is marshalled from a Go struct.

go
import "gorm.io/datatypes"

type Attribute struct {
	Sex   int
	Age   int
	Orgs  map[string]string
	Tags  []string
	Admin bool
	Role  string
}

type UserWithJSON struct {
	gorm.Model
	Name       string
	Attributes datatypes.JSONType[Attribute]
}

var user = UserWithJSON{
	Name: "hello",
	Attributes: datatypes.NewJSONType(Attribute{
        Age:  18,
        Sex:  1,
        Orgs: map[string]string{"orga": "orga"},
        Tags: []string{"tag1", "tag2", "tag3"},
    }),
}

The struct is marshalled and unmarshalled for you, so the shape of the document is checked by the compiler at the point where you write the code rather than by a test that reads a JSON fixture.

There is a companion type, json_map.go, and the presence of a Map variant alongside the generic one suggests the package is covering both the structured case and the loose case. That pairing is a sensible division: use JSONType when the document has a known shape, use plain JSON or the map type when it does not.

The database support line is the same as for JSON, sqlite, mysql and postgres, so the same json1 build tag requirement on SQLite applies here too.

UUID, binary UUID, URL and the NULL type fill in the rest

The remaining files in the repository cover the column types that come up in almost every schema, and they are worth a quick pass because each one has a specific reason to exist.

The uuid.go file provides a UUID column, and the module depends on github.com/google/uuid for the identifier generation. The binuuid.go file is the binary variant of the same thing, which matters because a UUID stored as text is 36 characters, and a UUID stored as 16 bytes is a different order of magnitude smaller in the index. On a table with millions of rows that difference shows up in both disk and query time.

The url.go file provides a URL column, which is mostly about letting the database treat the value as a URL rather than as free text.

The null.go file is the most interesting of the set, because it is about a behaviour rather than a format. A nullable column in SQL is null, and the difference between a null and a zero value is meaningful in almost every schema. A dedicated type makes the distinction explicit in the Go struct instead of leaving it to a pointer and a convention, which is the kind of thing that is easy to get wrong in a large codebase.

Taken together, the collection is a small, focused answer to a recurring set of complaints about mapping Go structs onto SQL schemas: JSON columns that are hard to query, dates that carry a time component you do not want, identifiers that take more space than they need to, and nullability that is easy to lose.

The version story is short, though. The only listed release is v1.2.6 from 2025-07-01, and the last push to the repository was on 2026-09-18. The module version tracks the GORM line rather than moving independently, so upgrading GORM is likely to be the moment you revisit this dependency.

Editorial conclusion

gorm datatypes suits a Go team that stores semi structured attributes in a relational database and needs to query and update them without hand writing dialect specific SQL, since the JSON query and set helpers generate the MySQL, PostgreSQL and SQLite forms for you. It does not suit a team that needs a broader ORM feature set, since this is a column type collection and nothing more, and it does not suit a PostgreSQL only codebase that would rather use the jsonb operators directly. Before you adopt it, pin the version you tested, because the only listed release is v1.2.6 from 2025-07-01 and the module declares Go 1.19, check whether your SQLite build has the json1 tag enabled as the README requires, and remember that the JSON path syntax differs between MySQL and PostgreSQL, so a query you write against one dialect is not portable to the other.

Frequently asked questions

What is gorm.io/datatypes?

It is an MIT licensed collection of customized data types for GORM, covering a JSON document type with query and update helpers, a generic typed JSON variant, date, time, UUID, binary UUID, URL and a NULL type, with support across MySQL, PostgreSQL, SQLite and SQL Server.

How do I query a JSON column with GORM datatypes?

Use datatypes.JSONQuery with the column name and either HasKey or Equals, for example datatypes.JSONQuery("attributes").HasKey("role"). The package generates the dialect specific SQL, JSON_EXTRACT on MySQL and jsonb operators on PostgreSQL.

Does SQLite need special setup for the JSON type?

Yes. The README notes that SQLite needs to be built with the json1 tag, for example go build --tags json1, referring to the go-sqlite3 usage documentation. The JSON and JSONType sections both list sqlite as supported.

Is JSONSet compatible with MariaDB?

Not for the raw JSON cast. The README states that MariaDB does not support CAST(? AS JSON), which is the form used when setting a JSON value such as a slice of strings or a struct, and PostgreSQL uses a different path syntax and cast as well.

What is the difference between datatypes.JSON and datatypes.JSONType?

datatypes.JSON is a raw byte slice holding a JSON document, while datatypes.JSONType is generic over a Go type such as Attribute, so the document is marshalled from a struct and its shape is checked at compile time.

Official sources

  1. go-gorm/datatypes on GitHub
  2. License: MIT
  3. Project website
  4. README
  5. Releases
Add this badge to your README

If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.

Add this badge to your README

markdown
[![Hysen Labs](https://hysenlabs.com/badge/go-gorm-datatypes.svg)](https://hysenlabs.com/projects/go-gorm-datatypes)

Was this useful?