Understand how GORM derives table names, column names, primary keys, and timestamps — and how to override each convention.
GORM follows a set of conventions to map Go structs to database tables without requiring explicit configuration. Understanding these conventions lets you write models with minimal boilerplate, and knowing how to override them gives you full control when defaults don’t fit.
GORM automatically treats a field named ID as the primary key for any model. This lookup happens in schema/schema.go:
// From schema/schema.go — GORM looks up "id" or "ID" to set the primary keyprioritizedPrimaryField := schema.LookUpField("id")if prioritizedPrimaryField == nil { prioritizedPrimaryField = schema.LookUpField("ID")}
If the ID field is an integer or unsigned integer type, GORM also marks it as auto-increment:
// Integer/uint primary keys get auto-increment enabled automaticallyswitch field.GORMDataType {case Int, Uint: if _, ok := field.TagSettings["AUTOINCREMENT"]; !ok { field.HasDefaultValue = true field.AutoIncrement = true }}
This means the following two structs behave identically:
// Explicittype User struct { ID uint `gorm:"primaryKey;autoIncrement"`}// Conventional — same resulttype User struct { ID uint}
Field names are converted to snake_case using NamingStrategy.toDBName. The strategy handles common initialisms (ID, URL, HTTP, JSON, etc.) from the Go lint list so they convert predictably:
GORM recognizes fields named CreatedAt and UpdatedAt by convention and manages them automatically. The detection logic in schema/field.go:
// autoCreateTime is set when field is named "CreatedAt" and has time/int/uint data typeif v, ok := field.TagSettings["AUTOCREATETIME"]; (ok && utils.CheckTruth(v)) || (!ok && field.Name == "CreatedAt" && (field.DataType == Time || field.DataType == Int || field.DataType == Uint)) { // ...}// autoUpdateTime is set when field is named "UpdatedAt"if v, ok := field.TagSettings["AUTOUPDATETIME"]; (ok && utils.CheckTruth(v)) || (!ok && field.Name == "UpdatedAt" && (field.DataType == Time || field.DataType == Int || field.DataType == Uint)) { // ...}
Timestamp fields can be time.Time, int64, or uint64. When using integer types, GORM stores Unix timestamps:
type User struct { ID uint Name string CreatedAt time.Time // set on insert UpdatedAt time.Time // set on insert and update}// Unix timestamp variantstype Event struct { ID uint Name string CreatedAt int64 `gorm:"autoCreateTime"` // Unix seconds UpdatedAt int64 `gorm:"autoUpdateTime:milli"` // Unix milliseconds}
DeletedAt requires the gorm.DeletedAt type to enable soft-delete behavior:
type User struct { ID uint Name string DeletedAt gorm.DeletedAt `gorm:"index"` // enables soft delete}
gorm.Model is the quickest way to get standard lifecycle fields:
// gorm.Model definitiontype Model struct { ID uint `gorm:"primarykey"` CreatedAt time.Time UpdatedAt time.Time DeletedAt gorm.DeletedAt `gorm:"index"`}
If you need different field types, different primary key types, or no soft-delete, define the fields yourself:
type Post struct { gorm.Model Title string Content string}// Table: posts// Columns: id, created_at, updated_at, deleted_at, title, content
If you use gorm.Model, all deletes are soft deletes. Queries automatically filter rows where deleted_at IS NOT NULL. Use db.Unscoped() to include soft-deleted records.