Struct Tags
goninja reads a single struct tag key, goninja, off each field of a model
struct (tag.Get("goninja")). The tag value is a comma-separated list of
verbs and, for relation fields, one modifier.
Verbs and modifier
| Name | Applies to | Effect |
|---|---|---|
list | any field | Include the field in <Model>List (the collection view). Also makes the field orderable and contributes to the whitelist of orderable columns. |
retrieve | any field | Include the field in <Model>Retrieve (the detail view). A relation field tagged retrieve is preloaded. |
create | any field | Include the field in <Model>Create (the POST request body). |
update | any field | Include the field in <Model>Update (the PUT request body). |
filter | any field | Generate a filter field on <Model>Filters. Numeric fields also get Min/Max range filters. |
byid | relation fields only | Expose the related model’s ID instead of nesting the full related type. See Relations. |
These five verbs and one modifier are the complete set. There are no others.
Values are trimmed, so goninja:"list, retrieve" and goninja:"list,retrieve"
are equivalent. Matching is exact string equality, which makes it
case-sensitive: List is rejected, as is every unrecognized or duplicate
tag. byid additionally requires retrieve on a relation field.
Supported field types
goninja supports the Go scalar types string, bool, signed and unsigned
integers, floats, byte, rune, and time.Time. A relation must name
another annotated goninja model from the same generation run.
Defined scalar types in that same models package are supported and preserved in the generated DTOs and filters:
type Status string
type Cents int64
type Book struct {
ID string `goninja:"list,retrieve"`
Status Status `goninja:"list,retrieve,create,update,filter"`
Price Cents `goninja:"list,retrieve,create,update,filter"`
}Pointers and scalar slices, aliases of time.Time, and types from another
package (for example sql.NullString or uuid.UUID) are rejected during
generation for now. This is intentional: a clear generator error is safer
than code that compiles but has ambiguous JSON, filter, or database behavior.
Untagged fields and structs
A field with no goninja tag, or an empty one (goninja:""), is skipped
entirely — it appears in none of the generated output types.
A struct with no goninja-tagged fields at all is silently skipped: no
model is generated for it, and the generator reports no error. If a model
you expect to see generated is missing, check that at least one field
carries a goninja tag.
Why list and retrieve are separate
list and retrieve produce two independent output types by design, not
as an implementation shortcut. <Model>List never preloads relations,
because a collection endpoint that eagerly loads every relation on every
row is a standing N+1 (or at best a wide join) query risk. <Model>Retrieve
is the full detail view: it preloads every relation field tagged
retrieve. Keep this split in mind when deciding which verbs to put on a
relation field — tagging it list does not cause it to be preloaded or
even included on the list type in nested form; only retrieve does that.
retrieve is preloaded with
q.Preload("<FieldName>") — unless it also carries byid, in which case
preloading is skipped entirely and only the related ID is read off the
model’s own foreign key column.Validation
The separate validate:"..." struct tag (from go-playground/validator)
is copied verbatim onto the matching <Model>Create and <Model>Update
fields only:
json:"{{.JSONName}}"{{if .ValidateTag}} validate:"{{.ValidateTag}}"{{end}}<Model>List and <Model>Retrieve fields never carry a validate tag,
because validation only applies to input. See
Validation for how goninja.Validate uses it.
The ID field
A model must have a field literally named ID. The generator derives the
model’s ID Go type from that field:
func (m Model) IDGoType() string {
for _, f := range m.Fields {
if f.Name == "ID" {
return f.GoType
}
}
return "int64"
}int64IDs are parsed out of the URL path withstrconv.ParseInt.- Any other type is taken from the path as-is (a string) and treated as a
UUID primary key —
Createfills it in withid.NewUUID()when the incoming value is empty.
In practice the two supported ID types are int64 and string.
The generator rejects a model with no goninja-tagged field named ID, and
one whose ID is neither int64 nor string, naming the file and the model:
$ goninja generate -models-import myapp/models
goninja: codegen: models/book.go: Book: no goninja-tagged field named ID;
every model needs one, typed int64 or string, and it must carry a goninja
tag to be exposed (e.g. `goninja:"list,retrieve"`)
Every problem across every model is reported in one run, and nothing is written when validation fails — so a rejected model never leaves a half-generated package behind.
The generator also rejects collisions before rendering: duplicate JSON names
within the same generated schema, duplicate filter query parameters (including
numeric _min/_max helpers), and duplicate database columns.
Complete example
package models
import "time"
type Book struct {
ID string `goninja:"list,retrieve" gorm:"primaryKey"`
Title string `goninja:"list,retrieve,create,update,filter" validate:"required,max=200"`
AuthorID string `goninja:"list,retrieve,create,update,filter" validate:"required,uuid4"`
Author Author `goninja:"retrieve,byid"`
Price float64 `goninja:"list,retrieve,create,update,filter" validate:"min=0"`
Published bool `goninja:"list,retrieve,create,update"`
CreatedAt time.Time `goninja:"list,retrieve"`
}Here, Author is a belongs-to relation tagged retrieve,byid: the
generated BookRetrieve type exposes author_id (typed after Author’s
own ID type) instead of a nested AuthorRetrieve, and the query skips
Preload("Author") entirely. Drop byid to get the fully nested relation
instead — see Relations for both cases in detail.