Input Validation
Every endpoint’s inputs cross a trust boundary. A functional endpoint’s arguments arrive from another microservice, an LLM picking tool arguments, or an internet client through the ingress — and the handler is the first code that assumes they make sense.
Microbus validates declaratively at that boundary, using dv8 directives written as struct tags. The generated marshaler runs them right after decoding, so an invalid payload is rejected 400 Bad Request and the handler never sees it.
Validation is opt-in per type: it fires only on api types that carry dv8 tags, so a project that adds none sees no change in behavior.
Declaring Constraints
Constraints live on the fields of a type in the microservice’s *api package, alongside the json and jsonschema_description tags:
package myserviceapi
// Person is a contact in the directory.
type Person struct {
Name string `json:"name,omitzero" jsonschema_description:"Name is the person's full name" dv8:"notzero,len<=64"`
Email string `json:"email,omitzero" jsonschema_description:"Email is the person's email address" dv8:"trim,tolower,regexp ^.+@.+$"`
Age int `json:"age,omitzero" jsonschema_description:"Age is the person's age in years" dv8:"val>=0,val<=120"`
}The directives used most often:
| Directive | Applies to |
|---|---|
notzero | Requires a non-zero value — non-nil for pointers, slices and maps; true for booleans |
len<=64, len>0 | Length of a string (in runes), slice, or map |
val>=0, val<=120 | Value range for numeric, duration and time fields |
oneof S|M|L | Membership in a set |
regexp ^[0-9]{5}$ | Pattern match; a comma inside the pattern is escaped \, |
trim, tolower, toupper, default=x | Normalizing directives that rewrite the value before the checks run |
each <directive> | Applies a directive to the elements of a slice or the values of a map |
key <directive> | Applies a directive to the keys of a map |
dv8’s own README carries the full set.
Where the Directives Are Enforced
- In structs of functions, web endpoints and inbound events — validated after decoding, before the handler runs.
- Task In structs — validated the same way, and deliberately so. A workflow’s state is a cross-service contract populated by callers, by LLMs, and by tasks hosted in other microservices, so a violated contract should fail the flow at the offending step rather than propagate downstream and fail somewhere less informative.
- Structured configs — validated on the way in and rejected before being committed, as Configuration describes.
- Out structs — tags are inert. Outputs are produced by trusted code and are not validated.
- Direct Go calls — a handler calling another handler in-process bypasses the framework boundary entirely. Where such internal composition needs validation, call
dv8.Validate(ctx, &v)explicitly.
Directives are compiled at startup, over every input type the microservice declares. A malformed directive — a typo, a rule applied to a type it cannot work on — fails the microservice immediately rather than lying dormant until the first request that exercises that field.
Cross-Field Rules: the Validate Method
Invariants that span more than one field cannot be expressed as a per-field tag. Give the type a Validate method instead, and the framework calls it automatically wherever the type is validated, including when it is nested inside another validated type:
// Validate errors when the person record is internally inconsistent.
func (p *Person) Validate(ctx context.Context) error {
if p.Retired && p.Age < 50 {
return errors.New("retirement age is 50")
}
return nil
}The signature is Validate(ctx context.Context) error, or Validate() error where the context is unused — a type implements one or the other, never both.
The method must be a pure check of its fields: no I/O, no downstream calls, no state mutation. It runs on the startup path and on every config refresh, so a validator that calls out turns validation into a distributed dependency. It also runs after the tag directives, so the values it sees are already trimmed and defaulted.
Constraints Reach the OpenAPI Document
The same directives are projected onto the rendered OpenAPI schema, so a constraint stated once reaches both the runtime and every consumer of the document — human readers and LLM tool-calling above all, which otherwise discover the contract by collecting 400s.
len<=64 becomes maxLength, val>=0 becomes minimum, oneof becomes enum, regexp becomes pattern, notzero adds the field to the parent’s required, default= becomes default, and the each / key prefixes descend into items / additionalProperties / propertyNames.
The projection is deliberately looser or equal to what the runtime enforces, so a spec-strict client never rejects a request the server would have accepted. Directives with no schema analog — the mutating ones like trim, and structural ones — are simply skipped, as are numeric bounds whose value is not a JSON number, such as a duration written 24h.
Further Reading
- Configuration — validating structured config values with the same directives.
- Agent Skills — the
add-typeskill, which writes an api type with its tags and optionalValidatemethod. - OpenAPI — the generated document these constraints are projected onto.