SCIM 2.0 makes a distinction between three attribute shapes:
- Simple — a scalar value (
"userName": "alice@example.com"). - Complex — a single nested object (
"name": {"givenName": "Alice", "familyName": "Anderson"}). - Multi-valued — an array of objects (
"emails": [{"value": "alice@x.com", "primary": true}]).
AshScim exposes each via a corresponding DSL entity: map, complex, and
multivalued. This page covers the multi-valued case in detail, since it
has the most options and the most subtle semantics.
A multivalued declaration is always an array on the wire — even if
your data model has only one row. AshScim supports two ways to back that
array, chosen by whether you set the relationship option.
When you don't set relationship:, the multivalued is backed by a single
field on the parent resource. The encoder always emits a one-element
array; the decoder collapses any inbound array to a single entry.
multivalued :emails do
map :value, attribute: :email
map :primary, value: true
map :type, value: "work"
endThis is the right choice when each user has exactly one email (or phone,
or address) and the value is also their identity. The User.email column
is your source of truth; SCIM gets a wrapped view of it.
Decoding behavior: when an IdP sends multiple entries (e.g. a primary
work email plus a personal one), the decoder picks the entry marked
primary: true and falls back to the first entry otherwise. A
Logger.warning fires if any entries are dropped, so operators can see
when an IdP is sending data the model can't preserve.
When you set relationship:, the multivalued is backed by a has_many
relationship. Each array element corresponds to one related row.
multivalued :members do
relationship :memberships
map :value, attribute: :user_id
endThis is the right choice when one parent record can have many entries —
canonically, group members. POST / PUT bodies carry the full list,
and PATCH ops manipulate individual rows.
The sub-map declarations reference attributes on the related
resource (here, Membership's :user_id), not the parent.
A compile-time verifier (AshScim.Verifiers.Relationship) checks at
compile time that:
- the relationship exists on the resource;
- it is a
has_many; - every sub-
map's:attributeis declared on the related resource.
So typos fail early with a clear Spark.Error.DslError.
Inside a multivalued (or complex) block, a map can be backed by a
literal value: instead of an Ash attribute:
multivalued :emails do
map :value, attribute: :email
map :primary, value: true # always emitted as `true`
map :type, value: "work" # always emitted as `"work"`
endThese mappings are emit-only. Inbound primary: false or
type: "home" values are silently ignored — there's no Ash attribute to
write them to. Useful when your data model doesn't track the distinction
the SCIM spec implies (e.g. you only have one email and it's always the
work one).
SCIM clients can target a specific element of a multi-valued attribute using a bracket filter in the PATCH path:
emails[type eq "work"].value
members[value eq "user-id-1"]
How AshScim handles them depends on which kind of multivalued is targeted:
- Single-attribute-backed: the bracket filter is informational. Since there's only one underlying field, the operation applies to it regardless of what the filter says. Syntax errors in the filter still fail fast.
- Relationship-backed: the bracket filter resolves through the sub-
maps to attributes on the related resource.members[value eq "u1"]becomes "the membership row whereuser_id == "u1"", and PATCHremovedeletes that row (via abulk_destroyquery when the relationship is a plainhas_many, or load-and-destroy otherwise).
The standard SCIM core schemas declare these multi-valued attributes:
| Resource | Attribute | Single-attr OK? | Notes |
|---|---|---|---|
| User | emails |
usually yes | most apps store one email per user |
| User | phoneNumbers |
usually yes | same |
| User | addresses |
sometimes | most apps don't model multiple |
| User | groups |
n/a | this is a back-reference, read-only |
| Group | members |
no | groups inherently have many members; relationship-backed only |
| User | ims, photos, entitlements, roles, x509Certificates |
rarely used in practice |
If your app has a real has-many for emails or phones (e.g. a separate
UserEmail resource), use relationship:. If it doesn't, the
single-attribute model is fine — and the warning logs will tell you if an
IdP ever sends data you're dropping.