Tag-Driven Behavior
This guide introduces the core idea behind Tagex: struct tags select behavior, directives implement meaning.
We will not validate forms, return error messages to users, or talk about “constraints”. Instead, we will use tags to normalize and transform data as it flows through a system.
The Problem: Inconsistent External Input
Many systems ingest data that is structurally correct but semantically inconsistent:
- User input
- CSV or JSON imports
- Third-party APIs
- Legacy systems
The data fits your structs, but not your expectations. Normalization logic is usually scattered across constructors, helper functions, or ad-hoc cleanup code.
Declaring Semantics with Tags
With Tagex, you declare what a field means using struct tags.
type ImportRow struct {
SKU string `norm:"upper"`
Price string `norm:"currency, locale=nl_NL"`
}
These tags do not describe constraints. They describe behavior.
uppermeans “normalize identifiers”currencymeans “parse locale-aware money values”
Implementing a Directive
A directive is a small, typed piece of Go code that implements a specific semantic operation.
type UpperDirective struct{}
func (d *UpperDirective) Name() string {
return "upper"
}
func (d *UpperDirective) Mode() tagex.DirectiveMode {
return tagex.MutMode
}
func (d *UpperDirective) Handle(val string) (string, error) {
return strings.ToUpper(val), nil
}
This directive:
- Operates on
stringfields - Transforms the value
- Writes the result back (
MutMode)
Directives with Parameters
Directives can receive parameters via their own struct fields.
type CurrencyDirective struct {
Locale string `param:"locale"`
}
func (d *CurrencyDirective) Name() string {
return "currency"
}
func (d *CurrencyDirective) Mode() tagex.DirectiveMode {
return tagex.MutMode
}
func (d *CurrencyDirective) Handle(val string) (string, error) {
cents, err := parseCurrency(val, d.Locale)
if err != nil {
return val, err
}
return strconv.FormatInt(cents, 10), nil
}
Parameters are mapped automatically from the struct tag:
`norm:"currency, locale=nl_NL"`
Creating a Tag Context
A Tag represents a processing context
for a single struct tag key.
normTag := tagex.NewTag("norm")
tagex.RegisterDirective(&normTag, &UpperDirective{})
tagex.RegisterDirective(&normTag, &CurrencyDirective{})
Directives are registered explicitly. Nothing is global and nothing is implicit.
Processing a Struct
row := ImportRow{
SKU: "ab-123",
Price: "€ 1.234,50",
}
_, err := normTag.ProcessStruct(&row)
After processing:
row.SKU == "AB-123"
row.Price == "123450"
The struct is now normalized and ready for use by the rest of your system.
What This Is (and Is Not)
In this guide, Tagex:
- Did not validate user input
- Did not know about databases or HTTP
- Did not enforce business rules
It executed semantic behavior selected by tags and implemented in Go.
Why This Matters
By attaching semantics directly to struct fields:
- Normalization logic is no longer scattered
- Meaning travels with the data
- Behavior is reusable and explicit
The struct becomes a semantic unit — not just a bag of values.
Next Guide
In the next guide, we will look at how tags can be used to select behavior conditionally, allowing the same struct to mean different things in different contexts.