Custom Converters and Semantic Parsing
So far, we have treated directive parameters as simple values: numbers, strings, booleans.
In this guide, we make an important shift: parameters are not values — they are domain literals.
Tagex embraces this by letting each directive define how its own parameters are parsed.
The Problem: One Type, Many Meanings
Consider the following struct:
type Limits struct {
MaxSize int64 `units:"bytes, max=10MiB"`
TimeoutMs int64 `units:"duration, value=250ms"`
}
Both parameters:
- Start as raw strings (
"10MiB","250ms") - End up as
int64
But they clearly do not share a grammar.
Parsing these values with a shared, type-based converter would require:
- Guessing intent
- Branching on directive names
- Hidden coupling between unrelated semantics
Tagex avoids this entirely by making parsing a responsibility of the directive itself.
Directive-Owned Parameter Parsing
A directive may implement custom parameter parsing
by providing a ConvertParam method.
This method receives:
- The directive parameter field
- The raw string from the tag
- The destination value to populate
Nothing is inferred. Nothing is shared. Meaning stays local.
Example: Parsing Byte Sizes
type BytesDirective struct {
Max int64 `param:"max"`
}
func (d *BytesDirective) Name() string { return "bytes" }
func (d *BytesDirective) Mode() tagex.DirectiveMode { return tagex.EvalMode }
func (d *BytesDirective) ConvertParam(
field reflect.StructField,
fieldValue reflect.Value,
raw string,
) error {
if field.Name != "Max" {
return nil
}
n, err := parseBytes(raw) // "10MiB", "512KiB"
if err != nil {
return tagex.NewConversionError(field, raw, "int64")
}
fieldValue.SetInt(n)
return nil
}
The grammar (MiB, KiB, etc.)
is owned entirely by the bytes directive.
Example: Parsing Durations
type DurationDirective struct {
Value int64 `param:"value"`
}
func (d *DurationDirective) Name() string { return "duration" }
func (d *DurationDirective) Mode() tagex.DirectiveMode { return tagex.EvalMode }
func (d *DurationDirective) ConvertParam(
field reflect.StructField,
fieldValue reflect.Value,
raw string,
) error {
if field.Name != "Value" {
return nil
}
dur, err := time.ParseDuration(raw)
if err != nil {
return tagex.NewConversionError(field, raw, "int64")
}
fieldValue.SetInt(int64(dur / time.Millisecond))
return nil
}
This directive parses a completely different grammar, even though it writes to the same Go type.
Why This Design Matters
By making directives responsible for parsing:
- Each directive defines its own literal language
- No global or tag-level logic needs to guess intent
- Multiple directives can coexist safely
- Documentation and code stay aligned
Parsing becomes part of semantics — not infrastructure.
Default Behavior Still Exists
Directives that do not implement custom parsing automatically fall back to Tagex’s default behavior for primitive types.
You only pay for complexity when you need it.
The Bigger Picture
With directive-owned conversion, Tagex now supports:
- Human-readable configuration
- Domain-specific literals
- Multiple grammars for the same Go type
- Strict semantic locality
This completes the core model:
- Tags select semantics
- Directives define meaning
- Parsing is part of that meaning
Where to Go Next
At this point, you have seen the full expressive range of Tagex: behavior, capability, success boundaries, validation, and parsing.
The remaining work is composition — building your own directive libraries and letting tags speak your domain’s language.