Fulcro RAD is attribute-centric. An attribute is an RDF-style description of a single fact about your domain, defined as an open map with a qualified keyword name and a type. Attributes are the foundation of RAD - they define your data model, generate resolvers, drive form/report behavior, and enable database schema generation. Unlike rigid class/table schemas, RAD's graph-based approach allows attributes to exist across multiple entities and be resolved from various sources.
Attributes are defined using the defattr macro from com.fulcrologic.rad.attributes:
Signature (from attributes.cljc:54-92):
(defattr symbol qualified-keyword data-type options-map)
Minimal Example:
(ns com.example.model.item
(:require
[com.fulcrologic.rad.attributes :as attr :refer [defattr]]))
(defattr id :item/id :uuid
{::attr/identity? true
::attr/schema :production})
This expands to:
(def id {::attr/qualified-key :item/id
::attr/type :uuid
::attr/identity? true
::attr/schema :production})
Key Points:
symbol: The var name (e.g., id)qualified-keyword: Must be fully-qualified (e.g., :item/id)data-type: One of :string, :uuid, :int, :long, :decimal, :instant, :boolean, :keyword, :symbol,
:ref, :enumoptions-map: Open map for additional configuration.cljc filesOnly two things are required (automatically added by defattr):
::attr/qualified-key - The attribute's keyword name::attr/type - The data typeEverything else is optional, though database adapters typically require additional options like ::attr/schema or
::attr/identities.
Identity attributes act as primary keys for entities. They are marked with ::attr/identity? true (from
attributes-options.cljc:13-18):
(ns com.example.model.account
(:require
[com.fulcrologic.rad.attributes :as attr :refer [defattr]]
[com.fulcrologic.rad.attributes-options :as ao]))
(defattr id :account/id :uuid
{ao/identity? true
ao/schema :production})
What identity? Does:
ao/identities)Multiple Identities: An entity can have multiple identity attributes (e.g., :account/id and :account/email could
both be identities).
Most attributes hold simple values. Here's a complete entity example:
(ns com.example.model.account
(:require
[com.fulcrologic.rad.attributes :as attr :refer [defattr]]
[com.fulcrologic.rad.attributes-options :as ao]))
;; Identity
(defattr id :account/id :uuid
{ao/identity? true
ao/schema :production})
;; Scalars
(defattr name :account/name :string
{ao/required? true
ao/identities #{:account/id}
ao/schema :production})
(defattr email :account/email :string
{ao/required? true
ao/identities #{:account/id}
ao/schema :production})
(defattr active? :account/active? :boolean
{ao/identities #{:account/id}
ao/schema :production})
(defattr balance :account/balance :decimal
{ao/identities #{:account/id}
ao/schema :production})
(defattr created-at :account/created-at :instant
{ao/identities #{:account/id}
ao/schema :production})
;; Collection of all attributes in this namespace
(def attributes [id name email active? balance created-at])
From attributes.cljc comments and DevelopersGuide.adoc:495-545:
| Type | Description | Example Usage |
|---|---|---|
:string | Variable-length text | Names, emails, descriptions |
:uuid | UUID identifier | Primary keys, unique IDs |
:int | 32-bit integer | Counts, small numbers |
:long | 64-bit integer | Large numbers, IDs |
:decimal | Arbitrary-precision number | Money, precise calculations |
:instant | UTC timestamp | Dates, times, created-at |
:boolean | true/false | Flags, switches |
:keyword | EDN keyword | Enum-like values |
:symbol | EDN symbol | Rarely used |
:enum | Enumerated values | Requires ao/enumerated-values |
:ref | Reference to another entity | Relationships (see below) |
Type Extensibility: RAD's type system is open. Database adapters and rendering plugins can add support for custom types.
The ::attr/schema option groups attributes into logical entities (tables/documents) (from attributes-options.cljc:
46-52):
ao/schema
"OPTIONAL. A keyword.
Abstractly names a schema on which this attribute lives. Schemas are just names you make up that allow you to
organize the physical location of attributes in databases."
How Schema Works:
::attr/schema value form a logical entityExample - Two Entities:
;; Account entity
(defattr account-id :account/id :uuid
{ao/identity? true
ao/schema :production}) ; <-- schema
(defattr account-name :account/name :string
{ao/identities #{:account/id}
ao/schema :production}) ; <-- same schema
;; Address entity
(defattr address-id :address/id :uuid
{ao/identity? true
ao/schema :production}) ; <-- different identity, forms separate entity
(defattr street :address/street :string
{ao/identities #{:address/id}
ao/schema :production})
Here we have two entities: Account and Address, both in the :production schema namespace but distinguished by
their identity attributes.
Non-identity attributes use ::attr/identities to declare which entities they belong to (from attributes-options.cljc:
20-32):
ao/identities
"OPTIONAL/REQUIRED. Database adapters usually require this option for persisted attributes.
A set of qualified keys of attributes that serve as an identity for an entity/doc/row. This is how a particular
attribute declares where it \"lives\" in a persistent data model..."
Key Insight: An attribute can belong to MULTIPLE entities by listing multiple identity keys:
;; Password hash lives on multiple entity types
(defattr password-hash :password/hash :string
{ao/required? true
ao/identities #{:account/id :file/id :sftp-endpoint/id}
ao/schema :production})
This tells RAD:
:password/hash can be found via :account/id (stored on Account table):file/id (stored on File table):sftp-endpoint/id (stored on SFTP Endpoint table)Database adapters use this to:
:password/hash from any of those IDsAttributes with type :ref connect entities. They represent edges in your data graph (from DevelopersGuide.adoc:
473-494).
Basic To-One Reference:
(defattr address :account/address :ref
{ao/target :address/id
ao/identities #{:account/id}
ao/schema :production})
This creates:
:one):address/idTo-Many Reference:
(defattr addresses :account/addresses :ref
{ao/target :address/id
ao/cardinality :many
ao/identities #{:account/id}
ao/schema :production})
Now an account can have MANY addresses.
Polymorphic References (v1.3.10+):
(defattr items :order/items :ref
{ao/targets #{:product/id :service/id} ; <-- SET of targets
ao/cardinality :many
ao/identities #{:order/id}
ao/schema :production})
An order can contain products OR services (or both).
Important Options for References (from attributes-options.cljc:84-110):
ao/target - REQUIRED (or use ao/targets). The identity keyword of the target entityao/targets - Alternative to target for polymorphic refs. A SET of identity keywordsao/cardinality - :one (default) or :manyao/component? - Boolean. Indicates exclusive ownership (may enable cascade deletes)For detailed relationship patterns, see: 02-relationships-cardinality.md
Recommended file structure (from DevelopersGuide.adoc:331-365):
src/main/com/example/
├── model/
│ ├── account.cljc ; :account/* attributes
│ ├── address.cljc ; :address/* attributes
│ ├── invoice.cljc ; :invoice/* attributes
│ ├── item.cljc ; :item/* attributes
│ └── line_item.cljc ; :line-item/* attributes
└── model.cljc ; Combines all attributes
Each model namespace (e.g., model/account.cljc):
(ns com.example.model.account
(:require
[com.fulcrologic.rad.attributes :as attr :refer [defattr]]
[com.fulcrologic.rad.attributes-options :as ao]))
(defattr id :account/id :uuid
{ao/identity? true
ao/schema :production})
(defattr name :account/name :string
{ao/required? true
ao/identities #{:account/id}
ao/schema :production})
;; Export all attributes
(def attributes [id name])
;; Export resolvers (if any custom resolvers defined here)
(def resolvers [])
Central model namespace (model.cljc):
(ns com.example.model
(:require
[com.example.model.account :as account]
[com.example.model.address :as address]
[com.example.model.invoice :as invoice]
[com.fulcrologic.rad.attributes :as attr]))
;; Combine all attributes
(def all-attributes (vec (concat
account/attributes
address/attributes
invoice/attributes)))
;; Lookup map (attribute keyword -> attribute)
(def key->attribute (attr/attribute-map all-attributes))
;; Form validator based on attributes
(def default-validator (attr/make-attribute-validator all-attributes))
Why this pattern?
:account/id in model.account)RAD libraries provide *-options namespaces with documented vars for all keys (from DevelopersGuide.adoc:208-234):
(ns com.example.model.item
(:require
[com.fulcrologic.rad.attributes-options :as ao] ; <-- options namespace
[com.fulcrologic.rad.attributes :refer [defattr]]))
(defattr id :item/id :uuid
{ao/identity? true ; <-- use vars instead of ::attr/identity?
ao/schema :production})
Benefits:
Available options namespaces:
com.fulcrologic.rad.attributes-options (ao) - Core attribute optionscom.fulcrologic.rad.form-options (fo) - Form-specific optionscom.fulcrologic.rad.report-options (ro) - Report-specific optionscom.fulcrologic.rad.picker-options (po) - Picker/dropdown optionsKey options from attributes-options.cljc (see 03-attribute-options.md for complete list):
ao/identity? - Boolean. Marks as primary keyao/identities - Set of identity keywords. Where this attribute "lives"ao/schema - Keyword. Logical schema groupingao/required? - Boolean. Validation hint (default: false)ao/type - Auto-added by defattr. The data typeao/qualified-key - Auto-added by defattr. The attribute nameao/target - Keyword. Target identity for :ref typeao/targets - Set. Multiple targets for polymorphic refsao/cardinality - :one or :many (default: :one)ao/component? - Boolean. Indicates exclusive ownershipao/label - String or (fn [this] string). Display labelao/style - Keyword or fn. Format hint (e.g., :USD, :password)ao/field-style-config - Map. Rendering plugin optionsao/valid? - (fn [value props qualified-key] boolean). Custom validatorao/read-only? - Boolean or fn. Prevents writesao/enumerated-values - Set. Legal values for :enum typeao/enumerated-labels - Map. Keyword -> display stringao/pc-output, ao/pc-resolve, ao/pc-input - Pathom 2 resolverao/pathom3-output, ao/pathom3-resolve, ao/pathom3-input - Pathom 3 resolverao/pc-transform, ao/pathom3-transform - Resolver transformersao/pathom3-batch? - Boolean. Batch resolver supportHere's a real-world Account entity with relationships:
(ns com.example.model.account
(:require
[com.fulcrologic.rad.attributes :as attr :refer [defattr]]
[com.fulcrologic.rad.attributes-options :as ao]))
;; Identity
(defattr id :account/id :uuid
{ao/identity? true
ao/schema :production})
;; Scalars
(defattr name :account/name :string
{ao/required? true
ao/identities #{:account/id}
ao/schema :production
ao/label "Account Name"})
(defattr email :account/email :string
{ao/required? true
ao/identities #{:account/id}
ao/schema :production})
(defattr role :account/role :enum
{ao/identities #{:account/id}
ao/schema :production
ao/enumerated-values #{:role/user :role/admin :role/guest}
ao/enumerated-labels {:role/user "User"
:role/admin "Administrator"
:role/guest "Guest"}})
(defattr active? :account/active? :boolean
{ao/identities #{:account/id}
ao/schema :production})
;; To-one reference
(defattr primary-address :account/primary-address :ref
{ao/target :address/id
ao/identities #{:account/id}
ao/schema :production})
;; To-many reference
(defattr addresses :account/addresses :ref
{ao/target :address/id
ao/cardinality :many
ao/identities #{:account/id}
ao/schema :production
ao/component? true}) ; Addresses are owned by account
;; Export
(def attributes [id name email role active? primary-address addresses])
Enums require special configuration (from attributes-options.cljc:34-44):
(defattr status :order/status :enum
{ao/identities #{:order/id}
ao/schema :production
ao/enumerated-values #{:status/pending :status/shipped :status/delivered :status/cancelled}
ao/enumerated-labels {:status/pending "Pending"
:status/shipped "Shipped"
:status/delivered "Delivered"
:status/cancelled "Cancelled"}})
If you omit ao/enumerated-labels, labels default to capitalized keyword names.
Attributes can have custom resolvers for derived data:
(defattr full-name :account/full-name :string
{ao/identities #{:account/id}
ao/pc-input #{:account/first-name :account/last-name}
ao/pc-output [:account/full-name]
ao/pc-resolve (fn [env {:account/keys [first-name last-name]}]
{:account/full-name (str first-name " " last-name)})})
This attribute:
ao/schema):account/first-name and :account/last-name as inputao/pathom3-* for Pathom 3)An attribute can exist on multiple entity types:
;; Shared across account, file, and SFTP endpoint
(defattr created-at :timestamp/created-at :instant
{ao/identities #{:account/id :file/id :sftp-endpoint/id}
ao/schema :production})
;; All three entities get this attribute
;; Database adapters will add it to all three tables
This is powerful for:
From attributes.cljc:57-59: "IF YOU ARE DOING FULL-STACK, THEN THESE MUST BE DEFINED IN CLJC FILES FOR RAD TO WORK!"
.cljc for full-stack apps.clj for JVM-only rendering plugins.cljs for client-side database adapters (rare)Attribute names must be fully-qualified (namespace/name). This ensures uniqueness and supports RAD's model organization.
:account/id): The keyword's namespace partao/schema :production): Logical grouping for database storage:account/id could have ao/schema :admin if desiredAttributes are open maps. Add your own namespaced keys:
(defattr id :account/id :uuid
{ao/identity? true
ao/schema :production
:my.app/audit-log? true ; <-- custom key
:my.app/pii-data? true}) ; <-- custom key
Database adapters, form plugins, and your own code can read these custom keys.
From attributes.cljc:94-129:
;; Check cardinality
(attr/to-many? attribute) ; => true if cardinality is :many
(attr/to-one? attribute) ; => true if cardinality is not :many
;; Generate EQL for attributes
(attr/attributes->eql [id name addresses])
; => [:account/id :account/name {:account/addresses [:address/id]}]
;; Build attribute map
(attr/attribute-map all-attributes)
; => {:account/id {...} :account/name {...} ...}
;; Create validator
(attr/make-attribute-validator all-attributes)
; => (fn [form field] ...) for use with forms
From DevelopersGuide.adoc:589-603:
Attributes are normally immutable maps. For faster development, enable mutable attributes:
# Start JVM with system property
java -Drad.dev=true ...
Or in deps.edn:
:jvm-opts ["-Drad.dev=true"]
With this enabled, re-evaluating a defattr in the REPL updates ALL closures over that attribute immediately. You still
need to reload namespaces when adding/removing attributes, but changes to existing attributes are instant.
com.fulcrologic.rad.attributes/defattr (attributes.cljc:54-92)com.fulcrologic.rad.attributes/new-attribute (attributes.cljc:29-51)com.fulcrologic.rad.attributes-options (attributes-options.cljc:1-373)to-many?, to-one? (attributes.cljc:94-104)attributes->eql, attribute-map, make-attribute-validator (attributes.cljc:116-129)::attr/attribute spec (attributes.cljc:23-24): Requires ::type and ::qualified-key::attr/qualified-key (attributes.cljc:20): Must be a qualified keyword::attr/type (attributes.cljc:21): Must be a keywordCan 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 |