Skip to content

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

NameApplies toEffect
listany fieldInclude the field in <Model>List (the collection view). Also makes the field orderable and contributes to the whitelist of orderable columns.
retrieveany fieldInclude the field in <Model>Retrieve (the detail view). A relation field tagged retrieve is preloaded.
createany fieldInclude the field in <Model>Create (the POST request body).
updateany fieldInclude the field in <Model>Update (the PUT request body).
filterany fieldGenerate a filter field on <Model>Filters. Numeric fields also get Min/Max range filters.
byidrelation fields onlyExpose 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.

A relation field tagged 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:

internal/codegen/templates/model.go.tmpl
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"
}
  • int64 IDs are parsed out of the URL path with strconv.ParseInt.
  • Any other type is taken from the path as-is (a string) and treated as a UUID primary key — Create fills it in with id.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

models/book.go
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.