RAD forms use Fulcro's form-state validation system to validate user input before submission. Validation can be specified at the attribute level (reusable across forms) or at the form level (contextual to specific forms). Type constraints from attributes (e.g., decimal, integer) provide first-line enforcement, while custom validators handle business logic.
From DevelopersGuide.adoc:1184-1191:
Type-level enforcement: Attribute types (:int, :decimal, :instant, etc.) and rendering styles automatically
prevent invalid input (e.g., typing "abc" into a number field).
Attribute-level validation: General constraints that apply wherever the attribute is used (e.g., "age must be 0-130").
Form-level validation: Contextual constraints between multiple fields (e.g., "from-date must be before to-date").
From DevelopersGuide.adoc:1188-1190:
RAD uses Fulcro's form-state validators, which return:
:valid - The field passes validation:invalid - The field fails validation:unknown - The field isn't ready to be checked yet (e.g., not touched)Validators are created with form-state/make-validator (aliased as fs/make-validator), which respects completion
markers to prevent premature error messages.
Signature: (fs/make-validator (fn [form field] validation-result))
Where:
form is the map of current form propsfield is the qualified keyword being validated:valid, :invalid, or :unknownFrom DevelopersGuide.adoc:1192-1199 and attributes-options.cljc:62-82:
(ns com.example.model.account
(:require
[com.fulcrologic.rad.attributes :refer [defattr]]
[com.fulcrologic.rad.attributes-options :as ao]))
(defattr name :account/name :string
{ao/identities #{:account/id}
ao/schema :production
ao/valid? (fn [value props qualified-key]
(boolean (seq value)))})
(defattr age :account/age :int
{ao/identities #{:account/id}
ao/schema :production
ao/valid? (fn [value props qualified-key]
(and (number? value)
(<= 0 value 130)))})
ao/valid? Signature: (fn [value props qualified-key] boolean?)
From attributes-options.cljc:62-82:
"OPTIONAL. A
(fn [value props qualified-key] boolean?).IMPORTANT: This is ONLY used in the UI when you create a validator using
attr/make-attribute-validator, AND you set that as the form validator with thefo/validatoroption."
Parameters:
value - The current value of the attributeprops - The entire form props (allows checking other fields)qualified-key - The keyword of the attribute being validated (e.g., :account/name)How valid? Works with required?:
From attributes.cljc:172-197:
The attr/valid-value? function (used internally by attr/make-attribute-validator) returns true if:
nil AND the attribute is NOT marked ao/required?ao/valid? and that predicate returns trueao/valid? but IS marked ao/required?, and the value is non-nil (and if string, non-blank)Otherwise returns false.
Important: ao/required? is primarily for UI display hints. It does NOT enforce server-side constraints.
From attributes-options.cljc:52-60:
"Attribute option. OPTIONAL: A boolean or
(fn [this attr] boolean?).A hint to the UI that the given attribute is required. Typically used by default validators generated from attributes. Not necessarily a guarantee that you cannot save without it (you may still need to write and install additional save middleware to enforce this constraint, if you need that level of validation)."
From DevelopersGuide.adoc:1201-1210 and attributes.cljc:222-238:
Use attr/make-attribute-validator to generate a Fulcro form-state validator from attributes:
(ns com.example.model
(:require
[com.fulcrologic.rad.attributes :as attr]
[com.example.model.account :as account]
[com.example.model.invoice :as invoice]))
(def all-attributes
(concat account/attributes
invoice/attributes))
(def all-attribute-validator
(attr/make-attribute-validator all-attributes))
attr/make-attribute-validator Signature:
(attr/make-attribute-validator attributes) - Validates scalar attributes only(attr/make-attribute-validator attributes include-refs?) - If include-refs? is true, validates reference
attributes too (defaults to false)From attributes.cljc:222-238:
"Creates a Fulcro form-state validator function that can be used as a form validator for any form that contains the given
attributes.A field is considered valid in this validator IF AND ONLY IF
attr/valid-value?returns true. See that function's docstring for how that interacts with theao/valid?option of attributes.If
include-refs?is true (default false) then references will be included in the validation."
From DevelopersGuide.adoc:1201-1220 and form-options.cljc:128-131:
Form-level validators completely override attribute-level validators. To combine them, use the attribute validator as a fallback:
(ns com.example.ui.account-form
(:require
[com.fulcrologic.fulcro.ui-state-machines :as uism]
[com.fulcrologic.fulcro.dom :as dom]
[com.fulcrologic.rad.form :as form :refer [defsc-form]]
[com.fulcrologic.rad.form-options :as fo]
[com.fulcrologic.fulcro.algorithms.form-state :as fs]
[com.example.model :as model]
[com.example.model.account :as acct]
[clojure.string :as str]))
(def account-validator
(fs/make-validator
(fn [form field]
(case field
:account/email (str/ends-with? (get form field) "example.com")
;; Default: use attribute validators
(= :valid (model/all-attribute-validator form field))))))
(defsc-form AccountForm [this props]
{fo/id acct/id
fo/attributes [acct/name acct/email acct/age]
fo/validator account-validator})
From form-options.cljc:128-131:
"Form option. OPTIONAL: A Fulcro
form-statevalidator (seemake-validator). Will be used to validate all fields on this form. Subforms may define a validator, but the master form's validator takes precedence."
From DevelopersGuide.adoc:1240-1257:
Form validators can access all form props, enabling validation that depends on multiple fields:
(def account-validator
(fs/make-validator
(fn [form field]
(case field
:account/email
(let [prefix (or
(some-> form
(get :account/name)
(str/split #"\s")
(first)
(str/lower-case))
"")]
(str/starts-with? (get form :account/email) prefix))
;; Default to attribute validators
(= :valid (model/all-attribute-validator form field))))))
Example: Require email to start with lowercase first name from the :account/name field.
(def report-params-validator
(fs/make-validator
(fn [form field]
(case field
:report/from-date
(let [from (get form :report/from-date)
to (get form :report/to-date)]
(or (nil? to) (nil? from)
(dt/before? from to)))
:report/to-date
(let [from (get form :report/from-date)
to (get form :report/to-date)]
(or (nil? to) (nil? from)
(dt/before? from to)))
:valid))))
From DevelopersGuide.adoc:1222-1230 and form-options.cljc:265-268:
(defattr age :thing/age :int
{ao/identities #{:thing/id}
ao/schema :production
ao/valid? (fn [value _ _]
(and (number? value)
(<= 0 value 130)))
fo/validation-message (fn [value]
(str "Age must be between 0 and 130."))})
fo/validation-message on Attribute: (fn [value] string?)
From form-options.cljc:265-268:
"Attribute option. Specify a default validation message for an attribute. Can either be a string or a
(fn [value] string?)."
From DevelopersGuide.adoc:1222-1238 and form-options.cljc:259-263:
Form-level messages override attribute-level messages:
(defsc-form ThingForm [this props]
{fo/id thing/id
fo/attributes [thing/name thing/age]
fo/validator thing-validator
fo/validation-messages {:thing/age (fn [form-props k]
(str (get form-props k) " is an invalid age."))}})
fo/validation-messages: A map from qualified keyword to string or (fn [props qualified-key] string?)
From form-options.cljc:259-263:
"Form option. A map whose keys are qualified-keys, and whose values are strings or
(fn [props qualified-key] string?)to generate the validation message. A default value for these can be given by putting ::form/validation-message on the attribute itself, which has a different signature."
Note: Form message functions receive form-props and qualified-key, while attribute message functions
receive only value.
This allows form-level messages to incorporate context from other fields:
{fo/validation-messages
{:account/email (fn [props k]
(let [name (get props :account/name)]
(str "Email must start with " (str/lower-case name))))}}
(ns com.example.ui.forms
(:require
[com.fulcrologic.fulcro.algorithms.form-state :as fs]
[com.fulcrologic.rad.form :as form :refer [defsc-form]]
[com.fulcrologic.rad.form-options :as fo]
[com.example.model :as model]
[com.example.model.account :as acct]))
(def account-validator
(fs/make-validator
(fn [form field]
(case field
;; Override specific field
:account/email (custom-email-check form field)
;; Use attribute validators for everything else
(= :valid (model/all-attribute-validator form field))))))
(defsc-form AccountForm [this props]
{fo/id acct/id
fo/attributes [acct/name acct/email acct/age]
fo/validator account-validator})
(def invoice-validator
(fs/make-validator
(fn [form field]
(case field
:invoice/tax-id
;; Only require tax ID for business customers
(if (= :business (get form :invoice/customer-type))
(boolean (seq (get form :invoice/tax-id)))
:valid)
(= :valid (model/all-attribute-validator form field))))))
RAD's built-in validation is synchronous only. For async validation (e.g., checking username availability on the server), use Fulcro's mutation system:
(defmutation check-username-available
[{:keys [username]}]
(action [{:keys [state]}]
;; Optimistically mark as checking
(swap! state assoc-in [:ui/username-check] :checking))
(remote [env]
true)
(ok-action [{:keys [state result]}]
(swap! state assoc-in [:ui/username-check]
(if (:available? result) :available :taken)))
(error-action [{:keys [state]}]
(swap! state assoc-in [:ui/username-check] :error)))
Then in your form, trigger the mutation on field blur and display results in custom rendering.
(defattr email :account/email :string
{ao/identities #{:account/id}
ao/schema :production
ao/required? true
ao/valid? (fn [value _ _]
;; More than just non-empty: must be valid email format
(and (seq value)
(re-matches #".+@.+\..+" value)))
fo/validation-message "Please enter a valid email address."})
From form-options.cljc:130-131:
"Subforms may define a validator, but the master form's validator takes precedence."
If you want subforms to have their own validators, you must explicitly call them from the parent validator:
(def parent-validator
(fs/make-validator
(fn [form field]
;; Validate parent fields
(if (parent-field? field)
(validate-parent-field form field)
;; Let subform validators handle their fields
:valid))))
Or rely on the default behavior where the master form validator handles all fields.
Fulcro form-state tracks which fields have been "touched" (interacted with) using completion markers. Validators
created with fs/make-validator automatically respect these markers:
:unknown (no error shown)Validation runs automatically:
form/save! is called)form/mark-complete! callsRAD forms automatically prevent saving if the form has validation errors. The save button will be disabled or the save will be blocked if validation fails.
RAD's ao/valid? is client-side only. For server-side validation:
Save Middleware: Add custom middleware to your RAD save pipeline to validate before database writes.
Mutation Return Values: Return error information from your save mutation, which RAD will display.
Database Constraints: Rely on database-level constraints (e.g., NOT NULL, CHECK constraints, foreign keys) to enforce data integrity.
Example save middleware (conceptual):
(defn validation-middleware
[handler]
(fn [pathom-env params]
(let [{::form/keys [delta]} params
errors (validate-delta delta)]
(if (empty? errors)
(handler pathom-env params)
{:error-messages errors}))))
From DevelopersGuide.adoc:1201-1203:
"If there are validators at both layers then the form one completely overrides all attribute validators."
This is intentional - form validators have complete control. Always use the pattern of calling
model/all-attribute-validator as a fallback if you want to preserve attribute validation.
From attributes-options.cljc:59-60:
"Not necessarily a guarantee that you cannot save without it (you may still need to write and install additional save middleware to enforce this constraint, if you need that level of validation)."
The ao/required? option:
attr/make-attribute-validator for validationAll ao/valid? and fo/validator checks run client-side only. Never rely on them for security or data integrity -
always validate on the server.
From form-options.cljc:130-131:
If both a master form and subform define validators, the master form's validator takes precedence. This means the master form is responsible for validating all fields, including those in subforms.
If you don't specify a validation message, RAD will not display one. The field will simply be marked invalid (typically with visual styling like a red border).
By default, attr/make-attribute-validator does not validate reference (:ref) attributes. Pass true as the
second argument to include them:
(def all-attribute-validator
(attr/make-attribute-validator all-attributes true))
From attributes.cljc:229-230:
"If
include-refs?is true (default false) then references will be included in the validation."
form/save! behaviorao/required? and attribute typesfo/triggersao/required?)ao/valid?)attr/valid-value? function)attr/make-attribute-validator function)fo/validator)fo/validation-messages)fo/validation-message)Can you improve this documentation?Edit on GitHub
cljdoc builds & hosts documentation for Clojure/Script libraries
| Ctrl+k | Jump to recent docs |
| ← | Move to previous article |
| → | Move to next article |
| Ctrl+/ | Jump to the search field |