Learn when Specter is better than get-in and update-in for complex nested data transformations, and how navigators, select, transform, and setval fit idiomatic Clojure data work.
Deeply nested immutable data is one of the places where plain get-in, assoc-in, and update-in start to feel repetitive. Those core tools are still the default for simple fixed paths, but once you need to target multiple matching locations, transform recursive structures, or reuse a more expressive traversal, a library can earn its place.
Specter: A Clojure library for selecting and transforming nested immutable data by composing paths from reusable pieces called navigators.
Specter is strongest when the traversal itself is part of the problem. Instead of hand-writing nested update calls or dropping into explicit loops, you describe a path through the data and then apply operations such as select, transform, or setval.
For one simple known path, core Clojure is excellent:
1(get-in order [:customer :address :city])
2(assoc-in order [:customer :address :city] "Toronto")
3(update-in order [:line-items 0 :quantity] inc)
You do not need Specter just to replace assoc-in. Idiomatic code stays with the simplest tool that clearly expresses the intent.
Specter starts to pay for itself when you need to:
update logicSpecter paths are built from small pieces called navigators. Some navigators step into known keys or indices. Others target structural patterns such as every value in a map or every element in a collection.
The three most common operations are:
select to retrieve all matchestransform to apply a function to all matchessetval to replace values at all matchesConsider order data:
1(def order
2 {:id 101
3 :customer {:name "Alice"
4 :tier :gold}
5 :line-items [{:sku "A-1" :qty 2 :price 15}
6 {:sku "B-9" :qty 1 :price 40}]})
With Specter, updating every quantity is concise and explicit:
1(require '[com.rpl.specter :as sp])
2
3(sp/transform [:line-items sp/ALL :qty] inc order)
4;; => {:id 101, :customer {:name "Alice", :tier :gold},
5;; :line-items [{:sku "A-1", :qty 3, :price 15}
6;; {:sku "B-9", :qty 2, :price 40}]}
The path says exactly what changes: step into :line-items, traverse ALL items, then focus the :qty key.
Where Specter becomes compelling is when the traversal is more structural than positional.
1(def inventory
2 {:warehouse-a [{:sku "A-1" :active true :qty 3}
3 {:sku "B-9" :active false :qty 0}]
4 :warehouse-b [{:sku "A-1" :active true :qty 7}]})
5
6(sp/select [sp/MAP-VALS sp/ALL :sku] inventory)
7;; => ["A-1" "B-9" "A-1"]
8
9(sp/transform [sp/MAP-VALS sp/ALL #(true? (:active %)) :qty]
10 #(+ % 10)
11 inventory)
That is the real value proposition: select or transform many matching places without hand-writing loops that rebuild every level manually.
If a traversal corresponds to a real business concept, name it and reuse it.
1(def line-item-prices-path [:line-items sp/ALL :price])
2
3(sp/select line-item-prices-path order)
4;; => [15 40]
5
6(sp/transform line-item-prices-path #(* % 0.9) order)
This keeps traversal logic in one place. If the shape changes later, you update one path instead of chasing repeated update-in chains across the codebase.
The Specter README emphasizes both concision and strong performance for nested immutable data manipulation. That is real value, but teams still need to use the library deliberately.
Specter is a good fit when:
It is a weak fit when:
update-in already says everything clearlyIdiomatic Clojure is not about using the most powerful library available. It is about choosing the most honest level of abstraction for the data problem in front of you.
graph TD;
A["Nested immutable data"] --> B{"Single fixed path?"}
B -->|Yes| C["Prefer get-in / assoc-in / update-in"]
B -->|No| D{"Many matching targets or reusable traversal?"}
D -->|No| E["Explicit reduction or recursion may be clearer"]
D -->|Yes| F["Specter path + select/transform/setval"]
The diagram below is the main judgment call. Specter is valuable when the traversal itself has meaning and would otherwise be duplicated or awkwardly encoded.