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
| Field | Required | Default | Description |
|---|---|---|---|
module | Yes | — | Go package name (snake_case). Used as package declaration and module identifier. |
table | No | {module}s | Database table name (Postgres), collection name (MongoDB), or ref path (RTDB). |
scope_by | No | — | Field name (snake_case) used to scope list queries to the current user. Generates ListByUserID instead of List. |
owner_field | No | — | Field name (snake_case) that holds the owning user's ID. Generates an ownership check in Update and Delete service methods. |
fields | Yes | — | List of field definitions. At least one field is required. |
Field Properties
| Property | Type | Default | Description |
|---|---|---|---|
name | string | (required) | Field name in snake_case. Becomes the Go struct field name (PascalCase). |
type | string | (required) | Schema type. See Field Types. |
primary | bool | false | Marks this field as the primary key. |
auto | bool | false | Generated by DB or always set server-side. Skipped from CreateRequest/CreateParams and UpdateRequest/UpdateParams. Required when using auto_update or auto_increment. |
auto_update | bool | false | Server-side update: field is set to now() in every UPDATE. Use with auto: true on updated_at timestamp fields. |
auto_increment | bool | false | Server-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. |
nullable | bool | false | Allows null/nil. Entity field becomes a pointer type (*T) for scalar types. json and array types remain reference types and are never dereferenced. |
required | bool | false | Adds binding:"required" to CreateRequest for Gin validation. |
default | string | — | Stored 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 name | Go field name |
|---|---|
id | ID |
user_id | UserID |
created_at | CreatedAt |
http_url | HTTPURL |
api_key | APIKey |
json_data | JSONData |
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.