Struct Tags¶
Den uses three struct tags: json for serialization, den for Den-specific metadata, and an optional validate tag for go-playground/validator integration. The den tag carries different sets of values depending on which struct it appears on — a document type, a GroupBy().Into() target, or a Project() target.
Tag Overview¶
| Tag | Purpose | Example |
|---|---|---|
json |
Sets the serialized field name (the key stored in JSONB) | json:"name" |
den |
Den-specific metadata (index/unique/fts on documents; from: on projections; count/sum:/group_key on aggregations) |
den:"index" |
validate |
Struct tag validation rules (always enforced) | validate:"required,email" |
All den: Values by Context¶
Every supported den: value, where it's valid, and what it does. Values are context-specific: an aggregation tag on a document field is rejected at registration; a document tag on an aggregation target is ignored.
den: value |
Valid on | Meaning |
|---|---|---|
index |
Document field | Secondary index for lookups and sorts |
unique |
Document field | Unique index (doubles as a lookup index — index is redundant alongside) |
fts |
Document field | Include this field in full-text search |
omitempty |
Document field | Omit from storage when zero-valued |
eager |
Link[T] / []Link[T] field |
Auto-hydrate this link on every read by default — see Schema-level eager |
unique_together:GROUP |
Document field | Composite unique index keyed by GROUP (multiple fields with the same group form one index) |
index_together:GROUP |
Document field | Composite (non-unique) index keyed by GROUP |
from:path.to.field |
Project() target |
Extract a nested value from the source document |
group_key |
GroupBy().Into() target |
Receives the single group key value |
group_key:N |
GroupBy().Into() target |
Positional group key (slot N, zero-indexed) for multi-field GroupBy(field0, field1, …) |
count |
GroupBy().Into() target |
Count of documents per group |
avg:FIELD |
GroupBy().Into() target |
Average of FIELD per group |
sum:FIELD |
GroupBy().Into() target |
Sum of FIELD per group |
min:FIELD |
GroupBy().Into() target |
Minimum of FIELD per group |
max:FIELD |
GroupBy().Into() target |
Maximum of FIELD per group |
Multiple values combine with commas: den:"index,omitempty". The combinations that make sense are document-field × document-field; mixing across contexts is rejected (document fields refuse from:, projection targets refuse index, etc.).
The json Tag¶
The json tag controls how a field is serialized to JSON. This is the standard Go encoding/json tag.
type Product struct {
document.Base
Name string `json:"name"`
Price float64 `json:"price"`
SKU string `json:"sku"`
}
- The
jsontag value becomes the field's key in the stored JSONB document - Use
json:"field,omitempty"to omit zero-value fields from JSON (standard Go behavior) - Use
json:"-"to exclude a field from serialization entirely
The den Tag¶
The den tag carries Den-specific metadata. It does not set the field name -- that is always the json tag's job.
Options¶
| Option | Description | Example |
|---|---|---|
index |
Create a secondary index on this field | den:"index" |
unique |
Create a unique index on this field | den:"unique" |
fts |
Include this field in full-text search | den:"fts" |
omitempty |
Omit this field from storage when zero-valued | den:"omitempty" |
eager |
Auto-hydrate this Link[T] / []Link[T] field on every read by default — see Schema-level eager |
den:"eager" |
unique_together:group |
Composite unique index — fields with the same group name | den:"unique_together:feed_guid" |
index_together:group |
Composite index (non-unique) — fields with the same group name | den:"index_together:user_date" |
Options can be combined with commas:
index / unique / unique_together / index_together / fts also work on fields of named-struct and pointer-to-struct fields at any nesting depth — see Nested Field Indexes.
unique already implies a lookup index
A unique index is itself a usable lookup index, so den:"unique" alone is enough for equality queries — you do not need to combine it with index. den:"index,unique" is accepted but redundant: Den creates only the unique index and silently drops the plain one.
Complete Example¶
type Product struct {
document.Base
Name string `json:"name" den:"index"`
SKU string `json:"sku" den:"unique"`
Price float64 `json:"price" den:"index"`
Body string `json:"body" den:"fts"`
Tags []string `json:"tags" den:"index,omitempty"`
}
Nullable Unique Fields¶
Pointer fields with unique create a nullable unique constraint. Multiple documents can have nil for the field without violating the constraint:
type User struct {
document.Base
Username string `json:"username" den:"unique"` // always required, always unique
Email *string `json:"email,omitempty" den:"unique"` // optional, unique when set
}
Both backends implement this as a partial unique index (WHERE ... IS NOT NULL).
Composite Unique Constraints¶
Use unique_together to enforce uniqueness across multiple fields. Fields sharing the same group name form a single composite unique index:
type Entry struct {
document.Base
Feed string `json:"feed" den:"unique_together:feed_guid"`
GUID string `json:"guid" den:"unique_together:feed_guid"`
Body string `json:"body"`
}
The combination (feed, guid) must be unique, but individual values can repeat. The group name (feed_guid) becomes part of the index name: idx_entry_feed_guid.
For non-unique composite indexes, use index_together:
type Event struct {
document.Base
UserID string `json:"user_id" den:"index_together:user_date"`
Date string `json:"date" den:"index_together:user_date"`
}
Composite unique indexes include a WHERE ... IS NOT NULL clause for all participating fields, matching the behavior of single-field nullable unique constraints.
The validate Tag¶
Den validates documents using go-playground/validator struct tags before every insert and update operation. Validation is always-on — there is no opt-in option and no way to bypass validate: constraints from inside Den.
type User struct {
document.Base
Name string `json:"name" validate:"required,min=3,max=50"`
Email string `json:"email" validate:"required,email"`
Age int `json:"age" validate:"gte=0,lte=130"`
}
Validation errors are returned as den.ErrValidation and can be unwrapped for per-field details.
Aggregation Struct Tags¶
Used with GroupBy().Into() to define the shape of aggregation results.
| Tag | Description |
|---|---|
den:"group_key" |
Receives the group key value (single-field GroupBy) |
den:"group_key:N" |
Positional group key (slot N, zero-indexed) for multi-field GroupBy |
den:"avg:fieldname" |
Average of fieldname within the group |
den:"sum:fieldname" |
Sum of fieldname within the group |
den:"min:fieldname" |
Minimum of fieldname within the group |
den:"max:fieldname" |
Maximum of fieldname within the group |
den:"count" |
Number of documents in the group |
Two struct fields claiming the same group_key:N slot, or two fields carrying the same aggregate tag (e.g. two den:"sum:price"), are rejected at the Into call — the framework refuses ambiguous targets.
Single-key example¶
type CategoryStats struct {
Category string `den:"group_key"`
AvgPrice float64 `den:"avg:price"`
Total float64 `den:"sum:price"`
Count int64 `den:"count"`
MinPrice float64 `den:"min:price"`
MaxPrice float64 `den:"max:price"`
}
var results []CategoryStats
err := den.NewQuery[Product](db,
where.Field("status").Eq("active"),
).GroupBy("category.name").Into(ctx, &results)
Multi-key example¶
type RegionStats struct {
Category string `den:"group_key:0"` // first GroupBy field
Region string `den:"group_key:1"` // second GroupBy field
Count int64 `den:"count"`
Total float64 `den:"sum:price"`
}
var stats []RegionStats
err := den.NewQuery[Product](db).GroupBy("category", "region").Into(ctx, &stats)
The slot index in group_key:N matches the position in the GroupBy(...) argument list. Unindexed den:"group_key" is shorthand for group_key:0 and is only valid when GroupBy was called with exactly one field.
Projection Struct Tags¶
Used with Project() to select a subset of fields from query results.
Simple projections use json tags for field name resolution. For nested field extraction, use the den:"from:" tag.
| Tag | Description |
|---|---|
json:"fieldname" |
Map to a top-level document field by its JSON key |
den:"from:nested.field" |
Extract a value from a nested field path |
Example¶
// Simple projection -- uses json tags
type ProductSummary struct {
Name string `json:"name"`
Price float64 `json:"price"`
}
// Nested field extraction -- uses den:"from:" tag
type ProductView struct {
Name string `json:"name"`
CategoryName string `den:"from:category.name"`
}
var summaries []ProductSummary
err := den.NewQuery[Product](db).Project(ctx, &summaries)
var views []ProductView
err := den.NewQuery[Product](db).Project(ctx, &views)
Document Base Types¶
Composable embeds for feature opt-in:
| Embed | Purpose |
|---|---|
document.Base |
Required. ID, CreatedAt, UpdatedAt, Rev |
document.SoftDelete |
Opt-in. DeletedAt, DeletedBy, DeleteReason |
document.Tracked |
Opt-in. Snapshot machinery for IsChanged, GetChanges, Revert |
Compose freely: struct { document.Base; document.SoftDelete; document.Tracked; ... }.
Reserved JSON Field Names¶
The standard embeds (document.Base and document.SoftDelete) install JSON keys with an underscore prefix to namespace them from user-defined fields — the same convention MongoDB uses. Whenever you need one of these in code that takes a string (where.Field, Sort, SetFields, After / Before, Project's den:"from:…") prefer the constants exported from the den package over the literal — typos become compile errors and a rename stays safe across the codebase.
| Constant | Value | Comes from | When to query it |
|---|---|---|---|
den.FieldID |
_id |
document.Base.ID |
Lookup-by-id, cursor pagination, sort by insert order (ULIDs sort chronologically) |
den.FieldCreatedAt |
_created_at |
document.Base.CreatedAt |
Time-window filters, "newest first" sorts |
den.FieldUpdatedAt |
_updated_at |
document.Base.UpdatedAt |
"Last touched" filters, change detection |
den.FieldRev |
_rev |
document.Base.Rev |
Rare — usually accessed via IgnoreRevision() instead of a manual where |
den.FieldDeletedAt |
_deleted_at |
document.SoftDelete.DeletedAt |
Soft-deleted-only queries (combine with IncludeDeleted + IsNotNil) |
den.FieldDeletedBy |
_deleted_by |
document.SoftDelete.DeletedBy |
Per-actor audit queries on soft-deleted rows |
den.FieldDeleteReason |
_delete_reason |
document.SoftDelete.DeleteReason |
Per-reason audit queries on soft-deleted rows |
The Go-side fields keep their natural names (doc.ID, doc.CreatedAt, …). Only the JSON tag (and therefore the SQL JSONB access path) uses the underscore form. Storage is independent of the constants — renaming the JSON tag would be a breaking storage change, not a source rename.