re-frame.schemas¶
Schema attachment, validation, and data-classification. Schemas are Malli schemas attached to app-db paths via reg-app-schema. In dev builds the runtime validates every app-db write against the matching schemas. It also validates event, fx, and subscription values that carry a :schema. A mismatch emits an :rf.error/schema-validation-failure trace. Production builds elide validation at the call sites.
This namespace owns four surfaces:
- the read-side introspection functions
- the pluggable validator / explainer / printer seams
- the per-slot
:sensitive?/:large?schema walkers that drive failure-trace redaction - the test-support snapshot / restore / clear hooks
The registration macros reg-app-schema / reg-app-schemas are surfaced through the re-frame.core facade (called as rf/reg-app-schema). Their full contract lives here.
Ships in artefact day8/re-frame2-schemas. Most app code touches only Registration and Introspection; the remaining sections are framework-integration and advanced surfaces. See Validate with schemas for the working guide.
Registration¶
The registration macros live in re-frame.core and route through the schemas artefact at registration time. Consumers call them as rf/reg-app-schema / rf/reg-app-schemas. re-frame.core.md lists them briefly; the full contract is here. re-frame.schemas also exports both names as plain functions for programmatic (HoF) registration. The functions carry the same contract but do not capture call-site source coords.
reg-app-schema¶
- Kind: macro
- Signature:
- Description: Attach a Malli schema to an
app-dbpath.- Development-build assertion, not a production guarantee. A production build still performs this registration —
app-schemasandapp-schema-metakeep answering — but the candidate validator is elided, so nothing checks the schema. A candidate that violates it installs silently: no rejection, no rollback, no trace. Your app-db schemas do not run in production builds. Keep the invariant that must hold in production in the handler, and set:boundary? trueon the registration where untrusted input must be validated in production too. - The schema is the positional value slot (rf2-qm7k83 Part A), uniform with the rest of the
reg-*family. The optional middle metadata map carries the frame target under:frame(a frame-id keyword or a frame value), plus:docand open:my/*keys. - The path is the registration id. App-db schemas are path-keyed and live in the schemas artefact's per-frame side-table; they are NOT a registrar kind.
(app-schema-meta {:frame f :path [:user]})looks up by the same path vector. pathis a sequentialget-inpath of concrete segments, normalized to canonical vector form.[]registers a whole-app-dbroot schema. Returns the normalized path.- Raises:
:rf.error/app-schema-bad-metadata— the middle metadata arg in the 3-slot form is not a map.:rf.error/app-schema-bad-path— a non-sequential path, or a non-concrete segment.:rf.error/app-schema-runtime-path— the first segment reaches the runtime-db partition (:rf.runtime/*,:rf.db/runtime, or the legacy:rf/runtimeroot).:rf.error/app-schemas-bad-arg— a:frametarget that does not resolve to a keyword frame id.:rf.error/no-frame-context— no:frameand no established frame scope.
- Dev-only diagnostics: re-registering a schema at a path whose live
app-dbvalue fails the new schema emits an:rf.schema/violationwarning trace. Registering an opaque compiled schema the per-slot walker cannot introspect warns once per process with:rf.warning/schema-walker-opaque.
- Development-build assertion, not a production guarantee. A production build still performs this registration —
- Example:
reg-app-schemas¶
- Kind: macro
- Signature:
- Description: Bulk plural form of
reg-app-schema.- Development-build assertion, exactly as for the singular form: every entry registers in a production build but is never checked there.
- Each entry routes through the singular form and is stamped with this call's source coords.
- The optional second arg names the frame for every entry: an opts map (
{:frame target}), a bare frame-id keyword, or a frame value. One frame per call. - Returns the vector of paths registered, in map-iteration order.
{}is the documented empty no-op. - Every path is shape-checked before any store mutation, so a batch containing one invalid path (
:rf.error/app-schema-bad-path/:rf.error/app-schema-runtime-path) registers nothing. - A
nil/ non-map first arg raises:rf.error/app-schemas-bad-batch.
- Example:
See re-frame.core.md for the reg-* return-value convention this registration participates in.
Introspection¶
The introspection surfaces live in re-frame.schemas and are not re-exported from re-frame.core.
One frame spelling. Every read below takes a single opts map whose :frame is required and names a frame target — a frame-id keyword or a frame value (normalized to its frame id), the same targets rf/registrations accepts. There is no ambient default and no bare-frame-id or trailing-frame-target sugar: a live frame value is itself a map, so a type-sniffing positional argument could never be read locally. A frameless or non-map call raises :rf.error/no-frame-context with a message naming the {:frame f} spelling; a :frame that resolves to a non-keyword target raises :rf.error/app-schemas-bad-arg.
Reading a frame that holds no schemas answers {} / nil / the empty-set digest rather than raising — the frame need not be live.
app-schemas¶
- Kind: function
- Signature:
- Description: Return a frame's whole
{path → registration-metadata}map, or{}.- This is the same
{id → meta}shaperf/registrationsanswers for registrar kinds. - Each value is the meta stamped at
reg-app-schema::path,:schema,:frame, source-coords (:ns/:line/:file), and the rest of:rf/registration-metadata. - Project
:schemawhen only the schema values are wanted.
- This is the same
- Example:
;; every registration in the default frame (schemas/app-schemas {:frame :rf/default}) ;; => {[:user] {:path [:user] :schema [:map [:id :uuid]] ;; :frame :rf/default :ns "my.app.schema" :line 12 :file "..."}} ;; just the schema values (update-vals (schemas/app-schemas {:frame :rf/default}) :schema) ;; => {[:user] [:map [:id :uuid]]}
app-schema-meta¶
- Kind: function
- Signature:
- Description: Return one path's registration-metadata map in a frame, or
nilwhen nothing is registered there.- Contains
:path,:schema,:frame, source-coords (:ns/:line/:file), and the rest of:rf/registration-metadata. - The registered schema alone is
(:schema ...). - Both keys are required.
- Contains
- Example:
;; the registration anchor — schema value plus source coords for click-back (schemas/app-schema-meta {:frame :rf/default :path [:user]}) ;; => {:path [:user] :schema [:map [:id :uuid]] ;; :frame :rf/default :ns "my.app.schema" :line 12 :file "..."} ;; the schema value alone (:schema (schemas/app-schema-meta {:frame :tenant/a :path [:articles]}))
app-schemas-digest¶
- Kind: function
- Signature:
- Description: Return a single hash over the frame's whole schema surface.
- Canonical wire form:
"sha256:"followed by the first 16 lowercase hex chars. - Byte-deterministic across runtimes. The empty schema set produces a stable digest.
- Computed over the
{path → schema}projection ofapp-schemas, so its bytes do not depend on the registration metadata riding alongside. - SSR hydration compatibility checks use it. So do tools that want to know whether the schema corpus changed, without diffing schema-by-schema.
- Canonical wire form:
- Example:
Validation entry points¶
The four dev-time validate-*! functions are the locked validation sites the framework calls for you:
- pre-handler (event)
- pre-fx
- post-commit (
app-db) - post-recompute (subscription)
They are public and manifested — tools and conformance tests call them directly. Ordinary app code does not: you register a :schema and the runtime invokes these. Every body lives inside an interop/debug-enabled? gate, so the whole surface is dead-code-eliminated in production. Each fn then returns true.
On every surface, a structurally malformed registered schema (one that makes the registered validator throw) emits the distinct :rf.error/malformed-schema trace and returns false (fail closed), never a silent pass. That trace carries structural locator slots only — no value-bearing slots.
validate-app-schema!¶
- Kind: function
- Signature:
- Description: After a handler commits
:db, walk every registered app-schema for the named frame and validate the post-commitapp-db. Only the named frame's schemas are walked.- Failures emit
:rf.error/schema-validation-failure(one trace per failing entry) with the registered explainer's output attached. Value-bearing slots are redacted when the failing slot is:sensitive?. - A malformed entry (the validator throws) emits
:rf.error/malformed-schemafor that entry, contributesfalse, and does not stop sibling schemas from validating. event-id(optional) names the handler whose commit prompted the failure — surfaced as:failing-id.- Returns
truewhen every registered schema conformed. Also returnstruewhen no validator is registered, no schemas are registered for the frame, or the build elided validation. - Returns
falsewhen at least one schema failed. The router consumes afalseto roll the:dbeffect back to the pre-handler value. - A hard no-op for every schema when
set-schema-fns!has installed anil:validate.
- Failures emit
validate-event!¶
- Kind: function
- Signature:
- Description: Before an event handler runs, validate the event vector against any
:schemaon the handler's registration metadata.- On failure, emits
:rf.error/schema-validation-failure :where :event. The caller skips the handler (recovery:no-recovery). - A
:sensitive?slot anywhere in the event schema redacts the value-bearing trace slots. The common case is a:cat/:catnpayload slot, such as a:passwordslot in a login schema. - The optional
framearg stamps a:frametag so the violation is captured into the in-flight epoch's:trace-events(read by the Xray Issues / Schema-timeline lens). The runtime passes it; direct callers may omit it. - Returns
true/false.
- On failure, emits
validate-fx!¶
- Kind: function
- Signature:
- Description: Before an fx handler runs, validate its args against any
:schemaon the fx's registration metadata.- On failure, emits
:rf.error/schema-validation-failure :where :fx-args. Only the offending fx is skipped (recovery:skipped). Sibling fx in the same:fxvector still run, and downstream queued events still drain. - The optional
framearg stamps a:frametag for epoch capture. - Returns
true/false.
- On failure, emits
validate-sub!¶
- Kind: function
- Signature:
- Description: After a subscription recomputes, validate its return value against any
:schemaon the sub's registration metadata.- On failure, emits
:rf.error/schema-validation-failure :where :sub-return. The caller replaces the value with the default (recovery:replaced-with-default). - The optional
framearg stamps a:frametag for epoch capture. - Returns
true/false.
- On failure, emits
Boundary validation and redaction seams¶
These three functions are the production-side and cross-artefact seams. validate-with-registered-fn / explain-with-registered-fn are the pure check surface used by the always-on boundary check that {:boundary? true} turns on for a handler (see reg-event), which reaches them through the late-bind table. redact-validation-tags is the one schema-aware redactor; every validation-failure emit site outside this namespace routes through it.
validate-with-registered-fn¶
- Kind: function
- Signature:
- Description: Apply the registered validator to
(schema, value). This is the public check seam the:boundary? truecheck uses. It runs in production, outside thedebug-enabled?gate thevalidate-*!hot path sits behind.- Returns
trueon conform. - Returns
falseon fail, including when a structurally malformed schema makes the validator throw (fail closed). - Returns
truewhen no validator is registered. No-validator means no-validation, mirroring the hot path. - Does NOT emit a trace (the boundary check owns the failure envelope) and does NOT consult
debug-enabled?.
- Returns
explain-with-registered-fn¶
- Kind: function
- Signature:
- Description: Apply the registered explainer to
(schema, value). Companion tovalidate-with-registered-fnfor the boundary check.- Returns the explanation map / data on fail.
- Returns
nilwhen the value conforms, when no explainer is registered, or when the explainer throws. A throwing explainer degrades tonilbecause diagnostics must never change the verdict.
redact-validation-tags¶
- Kind: function
- Signature:
- Description: The shared schema-aware redaction seam for every validation-failure trace emitted OUTSIDE this namespace. Its callers are the always-on boundary check, machine
:datavalidation, the:sub-overridepath, flow-output validation, and the recordable-coeffect:rf.error/cofx-value-invalidemit. Given theschemathe failing value was checked against and a failure-tracetagsmap, it returns the tags with:- a
:sensitive?schema → the value-bearing slots (:value/:received/:explain/:explain-humanized/:rf.fx/args/:rf.sub/query-v) scrubbed to:rf/redacted, and:sensitive? truestamped. An opaque compiled schema the walker cannot introspect fails closed and is treated as sensitive. - a
:large?(non-sensitive) schema → those same slots elided to the:rf.size/large-elidedmarker. - otherwise → the tags ride back verbatim.
- Off-namespace callers reach it through the
:schemas/redact-validation-tagslate-bind hook. When the schemas artefact is absent, the tags fall through verbatim. Idempotent.
- a
Validator extension seam¶
The default validator ships Malli's validate / explain pair (plus an EDN canonical printer for the digest). This seam lets an app swap in its own. The bundle still answers three different questions — validation correctness (:validate), human-readable failure messages (:explain), and stable canonical printing for the digest (:print) — but they are three KEYS of one value rather than three setters: set-schema-fns! installs, schema-fns reads, and default-schema-fns is the framework's own bundle.
set-schema-fns!¶
- Kind: function
- Signature:
- Description: Install any subset of the validator / explainer / printer bundle from a single map. This is the one door onto the validator port.
- Each key is optional; an absent key leaves the existing registration in place, so a one-key call is how you swap a single fn. An explicit
nilis a write:nil:validateor:explaindisables that fn. - A
nil:printcoerces to the default EDN canonicaliser, so the printer is never nil and the digest is never undefined. validate-fnis(fn [schema value] truthy?)— themalli.core/validateshape.explain-fnis(fn [schema value] explanation)— themalli.core/explainshape.print-fnis(fn [schema-value] canonical-string)and must be pure and deterministic across runtimes.- Last-write-wins per key; writes are not transactional.
- Returns the installed bundle map
{:validate … :explain … :print …}, reflecting the live state of all three fns after the call — including keys the call did not touch. A caller wanting back just the fn it installed selects that key. - This is the one-call substitute-Malli boot pattern: the three fns never drift mid-boot. It is also the restore path — hand it a value from
schema-fnsordefault-schema-fns.
- Each key is optional; an absent key leaves the existing registration in place, so a one-key call is how you swap a single fn. An explicit
- Example:
;; install validator + explainer together (e.g. a clojure.spec or Zod port) (schemas/set-schema-fns! {:validate my-validate :explain my-explain}) ;; swap just one fn — the others are untouched (schemas/set-schema-fns! {:validate my-validate}) ;; a non-Malli port registers its own canonical schema serialiser (schemas/set-schema-fns! {:print (fn [schema] (pr-str schema))}) ;; nil disables dev-time validation entirely (schemas/set-schema-fns! {:validate nil})
schema-fns¶
- Kind: function
- Signature:
- Description: Read the currently-installed validator / explainer / printer as one value, in the same shape
set-schema-fns!accepts and returns.(set-schema-fns! (schema-fns))is a no-op — the pair round-trips.:validate/:explainmay benil;:printis nevernil.- This is what test isolation is built from: capture, stub, and reinstate without reaching the framework-internal atoms and without a dedicated snapshot or restore verb. The registry-level counterpart for the per-frame schema store is
snapshot-schemas-by-frame.
- Example:
default-schema-fns¶
- Kind: var (bundle value)
- Signature:
- Description: The framework's own validator bundle as a plain map carrying exactly
:validate,:explainand:print— the state the port holds before an app installs anything.- Install it to restore the framework defaults:
(set-schema-fns! default-schema-fns). - Because the value carries the same fn objects the port was seeded with, the "still on the framework default" check behind
:rf.warning/schema-validator-unavailableanswers true again afterwards. Rebuilding an equal-but-distinct bundle by hand would not. - It is the framework default, not "the Malli bundle":
:printis the EDN canonicaliser rather than anything Malli supplies, and:validate/:explainsoft-pass while the Malli adapter is unloaded.
- Install it to restore the framework defaults:
- Example:
Schema classification walkers¶
The pure-data per-slot flag extractors and predicates. They walk a Malli vector-form EDN schema and report which slots carry :sensitive? true or :large? true per-slot props.
Two kinds of consumers read them: the schema-validation-failure-trace redactor — which is the only consumer for a machine's [:schemas :data] schema and a resource's required :params-schema — and the transient-payload projectors, the HTTP body-privacy projector (which reads the request's :decode schema) and story-mcp's tool-egress projector.
A resource's optional :data-schema is in neither group. It has no runtime validation consumer at all: it is a statically reflected shape fact, surfaced as the resource's process-node :schema and through the :rf/resource registration projection, and runtime shape-validation of a response rides the request's :decode instead (Spec 016 §Resource registration spec). A :sensitive? prop on a :data-schema slot therefore redacts nothing anywhere.
They describe shape, not durable egress policy — for app-db or for anything else. Durable app-db classification is event-owned: a reg-event returns :sensitive / :large alongside :db. Durable machine :data classification is likewise not schema-driven: a machine declares projection-relative :sensitive / :large paths at the top level of its reg-machine spec, and the runtime lowers them per actor instance into the frame's elision registry (Spec 015 §Subsystem projection-relative classification). Putting {:sensitive? true} on a [:schemas :data] slot redacts that slot in the schema's own validation-failure trace and nowhere else — it will not redact the snapshot in SSR hydration, an epoch record, or the Xray Machine Inspector. A compiled / opaque m/schema value is treated as an opaque leaf, so register the vector form when per-slot flags need to be visible.
extract-large-paths-from-schema¶
- Kind: function
- Signature:
- Description: Walk a Malli schema EDN form at
base-pathand return a{path declaration}map for every:large? trueslot found.- Each declaration carries
:source :schema, plus:hintpropagated verbatim when the slot props carry one. - This is the pure-data
:large?extractor the owner-local size-elision consumers read directly. It does NOT feed theapp-dbegress registry — that registry is frame-owned.
- Each declaration carries
- Example:
extract-sensitive-paths-from-schema¶
- Kind: function
- Signature:
- Description: As
extract-large-paths-from-schema, for the:sensitive? trueper-slot flag. Drives the validation-failure-trace redactor's decision about which value-bearing slots to scrub. Memoised by(schema, base-path). Clear the memo for test isolation viaclear-sensitive-paths-cache!.
schema-has-sensitive?¶
- Kind: function
- Signature:
- Description:
truewhen the schema declares ANY slot sensitive. Two cases count: the schema's container-level props carry:sensitive? true, or a nested:sensitive? trueslot lives anywhere inside it. This is the conservative whole-schema check: when any slot is sensitive, the whole trace's value-bearing slots are redacted. - Example:
schema-has-large?¶
- Kind: function
- Signature:
- Description:
truewhen the schema declares ANY slot:large? true. The mirror ofschema-has-sensitive?on the other per-slot flag. Whentrue, the validation emit-site substitutes the:rf.size/large-elidedsize marker for the value-bearing slots. When a slot is also sensitive, sensitive wins.
schema-sensitive-at?¶
- Kind: function
- Signature:
- Description: Path-targeted sensitivity check.
truewhen the slot atin-pathis sensitive. Two cases count: an ancestor along the path is:sensitive?(the failing slot sits under a sensitive container), or a descendant of the slot is:sensitive?(the slot's value carries a sensitive child).in-pathis the value-relative path Malli reports as:in. Anilor emptyin-pathis equivalent toschema-has-sensitive?.- This is the leaf-precise check the
app-dbhot path uses for its narrowed:valueslot. A non-sensitive failing leaf whose sibling is sensitive is therefore not over-redacted.
schema-opaque?¶
- Kind: function
- Signature:
- Description:
truewhenschemais a compiled / opaque value the pure-data walker cannot introspect for per-slot flags. That means any non-vector, non-keyword form: a compiledmalli.core/schemaobject, a map, a fn. A bare keyword (:int,:string, a registry ref) is NOT opaque. The redaction path fails closed on an opaque schema — it redacts as if sensitive, since Malli may honour a:sensitive?slot the walker cannot see. The supported way to make per-slot flags visible is registering the vector form.
Test-support¶
The per-frame registry and diagnostic-latch maintenance hooks. re-frame.test-support's reset-runtime fixture drives the snapshot / restore / clear trio through late-bind hooks. The clear-* latch resetters and cache clearers let a test start each case from a clean diagnostic + cache slate. on-frame-destroyed! is the framework's own frame-teardown cleanup.
snapshot-schemas-by-frame¶
- Kind: function
- Signature:
- Description: Return a snapshot value of the per-frame schema registry. The registry-level counterpart to
schema-fns; restore it withrestore-schemas-by-frame!.
restore-schemas-by-frame!¶
- Kind: function
- Signature:
- Description: Reset the per-frame schema registry to a snapshot taken by
snapshot-schemas-by-frame.
clear-schemas-by-frame!¶
- Kind: function
- Signature:
- Description: Reset the per-frame schema registry to
{}. Used by test fixtures and by the reset-runtime fixture's:clear-app-schemas? truepath. The per-frame registry is the schemas artefact's only mutable registration state.
on-frame-destroyed!¶
- Kind: function
- Signature:
- Description: Drop every schema registered against a destroyed frame so a subsequent
make-frameof the same id starts with a clean schema slate. Called from frame teardown through the:schemas/on-frame-destroyed!late-bind hook. Idempotent — a missing frame entry is a no-op.
clear-validator-unavailable-warned!¶
- Kind: function
- Signature:
- Description: Reset the once-per-process
:rf.warning/schema-validator-unavailablediagnostic latch so each test case starts from a clean slate.
clear-walker-opaque-warned!¶
- Kind: function
- Signature:
- Description: Reset the once-per-process
:rf.warning/schema-walker-opaquediagnostic latch — the nudge emitted when a schema is registered as an opaque compiled value the walker cannot introspect.
clear-edn-print-cache!¶
- Kind: function
- Signature:
- Description: Reset the
app-schemas-digestprinter memo. Returnsnil.- Test-support only. The memo is process-lifetime and bounded by the registered-schema cardinality (schemas register once at boot), so production never needs this.
- A test that registers many distinct fresh schemas clears it in fixture teardown so the cache doesn't grow unbounded across the suite.
clear-sensitive-paths-cache!¶
- Kind: function
- Signature:
- Description: Reset the
extract-sensitive-paths-from-schemawalker memo. Test-support companion toclear-edn-print-cache!; same bounded-cache rationale. Returnsnil.
See also¶
- re-frame.core.md — the
reg-app-schema/reg-app-schemasfacade rows, the:boundary? trueregistration flag, and the commit-plane data-classification effects (:sensitive/:large/:clear-sensitive/:clear-large) that own durableapp-dbclassification. - Validate with schemas — the working guide to schemas at
app-dbpaths. - Keep secrets out of traces — data classification,
:sensitive?, and large values.