Skip to main content

Schema Overview

The schema YAML file is the single source of truth for a module. It defines the domain fields, their types, and constraints — goclarc reads it and generates all Go code consistently.

Full Example

module: product # Go package name (snake_case)
table: products # DB table / collection / RTDB ref path
# Defaults to "{module}s" if omitted

fields:
# Primary key — auto-generated by the database
- name: id
type: uuid
primary: true
auto: true # Skipped from CreateRequest and CreateParams

# Required string field
- name: title
type: string
required: true # Adds `binding:"required"` to CreateRequest

# Nullable integer — *int32 in Entity, nil-safe in ToView()
- name: stock
type: int
nullable: true

# Float with a default (stored, not enforced by goclarc)
- name: price
type: float
required: true

# Boolean (defaults to false in Go zero value)
- name: in_stock
type: bool

# Arbitrary JSON — type varies by adapter
- name: metadata
type: json
nullable: true

# Array of strings
- name: tags
type: string[]

# Timestamp — auto-managed (skipped from Create/Update)
- name: created_at
type: timestamp
auto: true

# Always set to now() in every UPDATE — never COALESCE
- name: updated_at
type: timestamp
auto: true
auto_update: true

# Optimistic locking counter — increments by 1 in every UPDATE
- name: version
type: int
auto: true
auto_increment: true

# Nullable timestamp — *time.Time in Entity
- name: deleted_at
type: timestamp
nullable: true
auto: true

Top-Level Fields

FieldRequiredDefaultDescription
moduleYesGo package name (snake_case). Used as package declaration and module identifier.
tableNo{module}sDatabase table name (Postgres), collection name (MongoDB), or ref path (RTDB).
scope_byNoField name (snake_case) used to scope list queries to the current user. Generates ListByUserID instead of List.
owner_fieldNoField name (snake_case) that holds the owning user's ID. Generates an ownership check in Update and Delete service methods.
fieldsYesList of field definitions. At least one field is required.

Field Properties

PropertyTypeDefaultDescription
namestring(required)Field name in snake_case. Becomes the Go struct field name (PascalCase).
typestring(required)Schema type. See Field Types.
primaryboolfalseMarks this field as the primary key.
autoboolfalseGenerated by DB or always set server-side. Skipped from CreateRequest/CreateParams and UpdateRequest/UpdateParams. Required when using auto_update or auto_increment.
auto_updateboolfalseServer-side update: field is set to now() in every UPDATE. Use with auto: true on updated_at timestamp fields.
auto_incrementboolfalseServer-side increment: field is set to field + 1 in every UPDATE. Use with auto: true on version integer fields. Migration emits NOT NULL DEFAULT 0.
nullableboolfalseAllows null/nil. Entity field becomes a pointer type (*T) for scalar types. json and array types remain reference types and are never dereferenced.
requiredboolfalseAdds binding:"required" to CreateRequest for Gin validation.
defaultstringStored in schema for reference — not enforced by generated code.

Name Conventions

Field names are written in snake_case in the schema. goclarc converts them to Go-idiomatic PascalCase with correct initialisms:

Schema nameGo field name
idID
user_idUserID
created_atCreatedAt
http_urlHTTPURL
api_keyAPIKey
json_dataJSONData

JSON Tags

The JSON tag on View fields always uses the original snake_case field name:

type View struct {
ID string `json:"id"`
UserID string `json:"user_id"`
CreatedAt string `json:"created_at"`
}

Nullable Fields

A nullable: true field results in a pointer type in the Entity and a nil-safe conversion in ToView():

// Entity
Age *int32

// View
Age int32

// ToView()
if e.Age != nil {
v.Age = *e.Age
}

For nullable timestamps, both the nil check and RFC3339 formatting are generated:

// Entity
DeletedAt *time.Time

// ToView()
if e.DeletedAt != nil {
v.DeletedAt = e.DeletedAt.Format(time.RFC3339)
}

json and array fields (json.RawMessage, []string) are reference types — nullable: true on these fields has no effect on the generated Go types or ToView().

Server-Managed Fields

Use auto_update and auto_increment together with auto: true for fields the database always controls. They are excluded from CreateRequest, UpdateRequest, and all user-supplied parameters.

updated_at — always now()

- name: updated_at
type: timestamp
auto: true
auto_update: true

Generated UPDATE clause: updated_at = now() — no parameter, never COALESCE.

Migration: updated_at TIMESTAMPTZ NOT NULL DEFAULT now()

version — optimistic locking counter

- name: version
type: int
auto: true
auto_increment: true

Generated UPDATE clause: version = version + 1 — no parameter.

Migration: version INT NOT NULL DEFAULT 0

Scoped and Owned Modules

scope_by — user-scoped list

module: workspace
scope_by: user_id

fields:
- name: user_id
type: uuid
required: true

Generates ListByUserID(ctx, userID string) instead of List(ctx). The repository queries WHERE user_id = $1, the service passes it through, and the handler reads userID from the Gin context.

owner_field — ownership-enforced mutations

module: workspace
owner_field: user_id

Generates ownership checks in Update and Delete:

func (s *service) Update(ctx context.Context, id, ownerID string, req UpdateRequest) (*Entity, error) {
existing, err := s.repo.GetByID(ctx, id)
// ...
if existing.UserID != ownerID {
return nil, fmt.Errorf("workspace.service.Update: %w", apperr.ErrForbidden)
}
// ...
}

scope_by and owner_field can point to the same field and are typically used together.