title: Validation parent: Guides
nav_order: 2
Validation
TL;DR — Declarative
inputchecks (type:,required:,if:, etc.) run automatically before#execute. For everything else, override#validateand calllog_item_error(...)to short-circuit, orlog_item_warning(...)to flag a recoverable issue.LogItem.newraisesArgumentErrorfor invalid attributes in 1.0 — use the helpers, notLogItem.newdirectly.
This guide covers the validation surface beyond the declarative
options on input: the validate hook, the
warning-vs-error decision, the strict LogItem constructor, and
conditional patterns.
What runs automatically
For every Service.input :name, type: T, required: ..., if: ...,
the gem generates and runs:
#valid_type_name?— type check (or multi-type with M3 union).#valid_required_name?— presence check, whenrequired: true.#valid_required_conditional_name?— presence + predicate, whenrequired: trueandif:are both supplied.
#run calls every valid_required_*?, valid_required_conditional_*?,
and valid_type_*? method that matches by naming convention before
calling your #validate. Failures are logged as error-level
LogItems and short-circuit #execute.
Adding your own checks with #validate
Override #validate to log domain-specific errors:
class CreateUser < Assistant::Service
input :email, type: String, required: true
def validate
return if email.include?('@')
log_item_error(source: :validate, detail: :email, message: 'must contain @')
end
def execute
{ email: }
end
end
CreateUser.run(email: 'a@b.com').fetch(:status) # => :ok
CreateUser.run(email: 'oops').fetch(:status) # => :with_errors
#validate runs after the declarative checks. If a declarative
check already added an error, your #validate still runs (it has the
chance to surface additional context), but #execute is skipped.
Warning vs. error: how to choose
| Level | Helper | Effect |
|---|---|---|
:info |
log_item_info(...) |
Recorded on #logs; does not affect #status. |
:warning |
log_item_warning(...) |
Flips #status from :ok to :with_warnings; #execute still runs. |
:error |
log_item_error(...) |
Flips #status to :with_errors; #execute is skipped, #result is nil. |
Rule of thumb:
- Use an error when continuing would produce an invalid or
misleading result (
#executewould have to handle the bad state). - Use a warning when the result is still meaningful but the caller should know something is off (a missing optional input, an in-progress migration shape, a deprecated value).
A worked example:
class CreateUser < Assistant::Service
input :email, type: String, required: true
input :age, type: Integer, allow_nil: true, default: nil
def validate
log_item_error(source: :validate, detail: :email, message: 'invalid email') unless email.include?('@')
log_item_warning(source: :validate, detail: :age, message: 'age missing') if age.nil?
end
def execute
{ email:, age: }
end
end
CreateUser.run(email: 'a@b.com').fetch(:status)
# => :with_warnings — age is missing, but we still build the result
CreateUser.run(email: 'oops').fetch(:status)
# => :with_errors — execute is skipped
Conditional requirements
When a presence check should fire only sometimes, combine
required: true with if::
class UpdateUser < Assistant::Service
input :role, type: Symbol, default: :member
input :reason, type: String, required: true, if: ->(_value) { true }
def execute
{ role:, reason: }
end
end
UpdateUser.run(role: :member).fetch(:status)
# => :with_errors — predicate is truthy, so :reason is required
UpdateUser.run(role: :member, reason: 'audit cleanup').fetch(:status)
# => :ok
The if: predicate is called with the input's own value. The
validator requires the input to be present and the predicate to
be truthy — so the canonical use is "I need this to be present
when some other condition holds". See
inputs.md for the
inverse pattern.
LogItem.new raises in 1.0 (M10)
Constructing a LogItem directly with invalid attributes now raises
ArgumentError. The #valid? family is kept for introspection but
always returns true after a successful new:
Assistant::LogItem.new(level: :info, source: :a, detail: :b, message: 'ok').valid?
# => true
begin
Assistant::LogItem.new(level: :info, source: :a, detail: :b, message: '')
rescue ArgumentError => e
e. # => "invalid LogItem: message must be present"
end
Inside a Service, you almost never need LogItem.new directly:
log_item_info(...), log_item_warning(...), log_item_error(...),
and add_log(level:, source:, detail:, message:) build the item and
append it to #logs for you. See
logging-and-results.md for the full
catalogue.
Common pitfalls
- Returning
falsefrom#validateto signal failure. The hook's return value is ignored. The only way to fail is to log an error-levelLogItem. - Calling
raiseinside#validateor#execute. Don't —assistantis soft-fail. Convert expected failures into log items. Unexpected exceptions propagate (the gem catches exceptions only inbefore_execute/around_execute/after_executehooks). - Building
LogItem.new(...)and pushing it onto#logs. Use the helpers; they apply the same M10 strict construction and keep your call sites readable. - Forgetting that
#validateruns even when a declarative check already failed. Either guard#validatewithreturn if errors.any?, or design it to add complementary errors.
See also
- Inputs guide —
required:,if:, multi-type, the generatedvalid_*predicates. - Logging and results — the helpers, the full result hash, log filtering.
- API reference: LogItem.
- API reference: LogList.