~ / track C / clojure ecosystem
edn and deps.edn
Basicedn is Clojure's data interchange format — the subset of the language
that's just data: numbers, strings, keywords, vectors, maps, sets, lists.
It's what comes out of pr-str and what clojure.edn/read-string consumes.
And deps.edn is the file that tells the Clojure CLI what your project
depends on — itself a single edn map. Two halves of the same idea: the
format you serialize values in, and the format you describe your project
in.
edn at a glance
Every Clojure value that's just data is also valid edn. Round-tripping through a string is one call in each direction:
pr-str writes the readable form. prnclojure.core/prnLike pr, then newline.view on clojuredocs →, prclojure.core/prPrint the value(s) in a form suitable for the reader.view on clojuredocs →, and binding [*print-readably* true] all produce edn-shaped output that the reader can consume.
This is what makes config files, RPC payloads, log records, and database columns natural in Clojure: there's no schema layer between the value you have and the bytes you write. Save it, send it, read it back — the only edge case is "what if there's a tagged literal."
Tagged literals
A tagged literal is #tag value and lets edn carry types beyond the
built-in ones. Two ship with every reader:
#inst becomes a date/time value, #uuid becomes a UUID. The tag is the
type; the value is whatever the reader does with the form that follows
it.
You can register your own. Passing a :readers map to read-string keeps
the reader scoped to that call — useful for safely accepting untrusted edn:
In a real codebase you'd register money globally in data_readers.clj so
every file that reads edn handles the tag uniformly.
When to use a tagged literal vs a map
Tagged literals are for things the codebase wants to treat as a type: money, durations, IDs, decimals. If the value is "just data" with no behavior, a map with namespaced keys is usually clearer:
;; Tagged literal — when you expect to dispatch on it
#money [30 "USD"]
;; Plain map — when it's just a record
{:price/amount 30 :price/currency "USD"}Don't over-tag. Most domain entities are happier as maps.
Why not JSON?
edn does the same job as JSON, but with three concrete wins for Clojure:
- Sets and keywords as first-class values. No more
"role": "admin"that has to bekeywordd on the other side. - Maps with anything as keys. A
{[lat lng] :city}map round-trips cleanly; JSON would force you to stringify the coordinates. - Tagged literals. Custom types without schemas, version negotiation, or out-of-band documentation.
The cost is that everything talking edn has to understand it — fine for JVM↔ClojureScript, less fine when one side is a Python service. The usual boundary rule applies: edn between Clojure services, JSON at the edge that talks to the rest of the world.
deps.edn — the project file
The Clojure CLI (clojure / clj) is configured by a deps.edn file —
itself a single edn map describing dependencies, source paths, and named
"aliases" for variant commands. A minimal one:
{:paths ["src" "resources"]
:deps {org.clojure/clojure {:mvn/version "1.12.0"}}
:aliases
{:test {:extra-paths ["test"]
:extra-deps {io.github.cognitect-labs/test-runner
{:git/tag "v0.5.1" :git/sha "dfb30dd"}}
:main-opts ["-m" "cognitect.test-runner"]}
:dev {:extra-deps {nrepl/nrepl {:mvn/version "1.0.0"}}}
:uberjar
{:replace-deps {com.github.seancorfield/depstar {:mvn/version "2.1.303"}}
:exec-fn hf.depstar/uberjar
:exec-args {:jar "target/myapp.jar" :main-class "myapp.core"}}}}The three pillars:
:paths— where source lives. Anything you want on the classpath at base.:deps— your library dependencies. The coordinate is a map: Maven artifacts use{:mvn/version "x"}, Git deps use{:git/url "…" :sha "…"}, local paths use{:local/root "../other-lib"}.:aliases— named modifiers you can stack on the command line.clj -M:testruns the test-runner;clj -M:dev:testmerges both. Each alias can add deps, change paths, set main options, or swap dependency versions.
How dependency layers compose
There are three places deps.edn files merge:
~/.clojure/deps.edn— your user-wide tools (Reveal, Portal, an nREPL alias you want everywhere).- Project
deps.edn— the canonical file for this codebase. deps.local.edn(some workflows) or-Sdeps '{…}'on the CLI — one-off overrides.
The CLI computes the effective classpath from the merge and caches it. The
implication: aliases are modifiers, not separate projects. clj -M:test -X my.ns/run runs the test alias's deps and paths, then calls
my.ns/run — composition all the way down.
A practical use: read a config file
A common pattern: ship a config.edn, override per environment with
prod.edn, deep-merge at startup, read the result through a typed
accessor. Reading is one line:
In production the text comes from (slurp "config.edn"). The point: no
schema library is required to start. You can layer Malli or Spec on top
later for validation, but the parse-and-use path is just
read-string + get-inclojure.core/get-inLook up a nested value through a path of keys.view on clojuredocs →.
Check yourself
? quiz
Which is **not** a reason to prefer edn over JSON between two Clojure services?
Exercise
Given the edn string below, parse it and return a vector of only the
admin users' names, sorted alphabetically. Use clojure.edn/read-string,
then plain seq operations — no schema library needed.