Validate Untrusted JavaScript Data at ClojureScript Boundaries
Use clojure.spec.alpha to check API responses and JavaScript objects before they reach your ClojureScript components.
02 Sept 2026, 08:04 UTC

Untrusted data does not become safe just because it is in ClojureScript
You call fetch, parse the response, and pass the result to a Reagent component. The remote API adds a field, removes one, or returns null where you expected a map. Because JavaScript objects carry no schema at runtime, your component crashes with Cannot read property ... of undefined. ClojureScript’s immutable data structures protect you after data is inside your program, but they do not validate the edge where untrusted JavaScript enters.
Use clojure.spec.alpha as an explicit gate
clojure.spec.alpha is a library for declarative data contracts. A spec is a predicate that describes the shape of a value. s/valid? returns true or false. s/conform returns the value if it matches, or indicates failure. s/explain prints a human-readable failure message. Spec is opt-in: nothing is checked unless you call it, so you control exactly which boundaries are guarded.
Worked example: validate an API user record
Assume a ClojureScript project using a recent release that includes clojure.spec.alpha. No extra dependency is required for the spec library itself.
In a .cljs namespace, define a spec for a user record and a validation function. Use your project’s preferred JavaScript-to-Clojure conversion helper; this example uses js->clj.
(ns app.boundary
(:require [clojure.spec.alpha :as s]))
(s/def ::name string?)
(s/def ::email string?)
(s/def ::age pos-int?)
(s/def ::user
(s/keys :req-un [::name ::email]
:opt-un [::age]))
(defn validate-user [raw]
(let [data (js->clj raw :keywordize-keys true)]
(if (s/valid? ::user data)
{:ok (s/conform ::user data)}
{:error (s/explain ::user data)})))
Call it immediately after receiving data from JavaScript, before the value reaches a component.
(defn handle-user-response [raw-js-user]
(let [result (validate-user raw-js-user)]
(if (= :ok (:ok result))
(do
(js/console.log "Valid user" (:ok result))
(:ok result))
(do
(js/console.error "Invalid user" (:error result))
nil))))
Verify the behavior in a ClojureScript REPL or browser console. A successful call returns true.
(require '[app.boundary :as boundary])
(s/valid? ::user {:name "Ada" :email "[contact removed]"})
;; returns true
(s/explain ::user {:name "Ada" :age "thirty"})
;; returns a development-oriented failure message describing the missing email and invalid age
Replace ::user and the sample maps with your own contract. Qualified keywords such as ::user avoid collisions with JavaScript properties and are safer to serialize than bare spec names.
Trade-offs and how to check the result
- Spec does not run automatically at JavaScript interop boundaries. You must invoke
s/valid?ors/conformyourself, typically right after converting a JavaScript object. - Adding spec increases compiled bundle size and macro compilation time compared with hand-written predicate functions.
s/explainoutput is verbose and is intended for development and testing, not production error displays.- Spec generators are for test.check integration, not for validating live requests.
Practical check: compile with :advanced and inspect the emitted JavaScript. Qualified keywords used in specs should remain recognizable rather than mangled. If you see spec names being renamed, rely on qualified keywords in serialized data and avoid passing bare spec symbols across the boundary.
Rollback
Because adding a namespace and call sites changes your project state, roll back by deleting the app.boundary namespace, removing its call sites, and rebuilding. No database or runtime state is affected.
Closing
Start with one boundary, such as your most volatile API endpoint. Define the smallest spec that catches real failures, validate immediately after interop, and expand only when the contract proves useful. Spec does not make JavaScript safe by default, but it gives you an explicit, synchronous place to reject bad data before it reaches your UI.