Phel ships two eval-free interchange modules for exchanging data with other Clojure-aligned runtimes, or with itself across process boundaries:
phel.edn: Extensible Data Notation, the Clojure-native subset of reader syntax.phel.transit: Transit layered on top of JSON.
Neither goes through eval, so both are safe on untrusted input.
Looking for JSON or CSV instead? The Cookbook has ready-to-run recipes for reading a CSV file and parsing JSON via
phel.json. Use EDN/Transit when you need Phel's richer value types (keywords, sets, symbols, tagged literals) to survive the round trip.
phel.edn#
EDN is the right choice for Phel-to-Phel config files, source-like data, and
human-edited input that benefits from comments and #_ discard forms.
Public API#
| Function | Purpose |
|---|---|
(read-string s) / (read-string s opts) | Read the first EDN form from s. |
(read-string-all s) / (read-string-all s opts) | Read every form; returns a vector. |
(write-string v) | Serialise v to EDN. |
(write-string-all xs) | Serialise a sequence; values joined by a single space. |
Options map#
| Key | Value | Behaviour |
|---|---|---|
:readers | {tag fn} | Per-call tag handlers. Tags may be symbols, keywords, or strings; the handler receives the already-parsed form. The global TagRegistry is snapshotted before the call and restored in a finally. |
:eof | any | Returned by read-string when input is empty, whitespace-only, or comment-only (default nil). |
Type mapping#
phel.edn delegates reading to Phel's own reader and writing to
Printer::readable(), so every value Phel can print readably round-trips:
nil, booleans, integers, floats, strings, characters, keywords, symbols, lists,
vectors, maps, sets, the #_ discard form, ; comments, and built-in tagged
literals (#uuid, #inst, #regex).
The lexer accepts EDN's full tag grammar: namespaced tags (#my.app/Person,
#myapp/double, #my.app.module) lex as a single tag, and unqualified tags
(#uuid, #inst) work unchanged.
Examples#
(ns my-app.edn-demo
(:require phel.edn :as edn))
;; Read a single form
(edn/read-string "{:host \"localhost\" :port 4000}")
;; => {:host "localhost" :port 4000}
;; Read every form into a vector
(edn/read-string-all "1 2 3")
;; => [1 2 3]
;; Serialise back to EDN
(edn/write-string {:users [{:name "Alice"} {:name "Bob"}]})
;; => "{:users [{:name \"Alice\"} {:name \"Bob\"}]}"
;; Custom tag: register a handler for #my.app/Point literals
(edn/read-string "#my.app/Point [1 2]"
{:readers {'my.app/Point (fn [[x y]] {:x x :y y})}})
;; => {:x 1 :y 2}
;; Empty input with a sentinel
(edn/read-string "" {:eof :no-data})
;; => :no-dataphel.transit#
Transit is the right choice for talking to Clojure, Java, or JS over HTTP, and for round-tripping JSON-shaped data that carries rich types.
Public API#
| Function | Purpose |
|---|---|
(read-string s) / (read-string s opts) | Decode one Transit+JSON-Verbose value. |
(write-string v) | Encode v to a Transit+JSON-Verbose string. |
This first iteration implements JSON-Verbose only (no key caching): maps with
string keys serialise to ordinary JSON objects; any other map serialises as a
~#cmap.
Options map#
| Key | Value | Behaviour |
|---|---|---|
:handlers | {tag-string fn} | Decoder for ~#tag arrays. The handler receives the already-decoded representation. |
:default-handler | (fn [tag rep] …) | Fallback for any unknown tag (scalar or array). Receives the bare tag name and decoded rep. Without it, unknown tags throw InvalidArgumentException. |
Type mapping#
| Phel | Transit JSON-Verbose |
|---|---|
nil | null |
| bool | true / false |
| int, float | JSON number |
| string | JSON string (with ~/^/` escape) |
| keyword | "~:name" |
| symbol | "~$name" |
Phel\Lang\UUID | "~uXXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX" |
DateTimeImmutable | "~tISO-8601" |
| vector | JSON array […] |
| list | ["~#list", [...]] |
| set | ["~#set", [...]] |
| map (string keys) | JSON object {...} |
| map (composite keys) | ["~#cmap", [k1, v1, k2, v2, …]] |
Plain Phel strings whose first character is ~, ^, or ` are escaped on
write with a leading ~, so the reader does not mistake them for a Transit tag.
Examples#
(ns my-app.transit-demo
(:require phel.transit :as transit))
;; String-keyed map -> plain JSON object
(transit/write-string {"name" "Alice"})
;; => "{\"name\":\"Alice\"}"
;; Keyword keys are composite keys, so the map becomes a ~#cmap
(transit/write-string {:status :ok :count 42})
;; => "[\"~#cmap\",[\"~:status\",\"~:ok\",\"~:count\",42]]"
;; Reading a string-keyed object yields keyword keys back
(transit/read-string "{\"~:status\":\"~:ok\",\"~:count\":42}")
;; => {:status :ok :count 42}
;; Custom extension type (read-side)
(transit/read-string "[\"~#point\",[1,2]]"
{:handlers {"point" (fn [[x y]] {:x x :y y})}})
;; => {:x 1 :y 2}
;; Catch-all for unknown tags
(transit/read-string "[\"~#myapp/Foo\",[1,2]]"
{:default-handler (fn [tag rep] [:unknown tag rep])})
;; => [:unknown "myapp/Foo" [1 2]]Intentionally out of scope (for now)#
- Cached keys (the non-verbose Transit+JSON encoding with
^cache markers). - Transit+MessagePack.
- Write-side extension hooks: there is no symmetric
:handlersmap onwrite-stringyet. Encode custom types yourself before handing them towrite-string.
Choosing between EDN and Transit#
| Need | Use |
|---|---|
| Phel-to-Phel config files, source-like data | EDN |
| Talking to Clojure/Java/JS over HTTP, MessagePack-ready wire format | Transit |
Human-edited input with comments and #_ discards | EDN |
| Round-tripping JSON-shaped data with rich types | Transit |
See also: Cookbook for JSON and CSV recipes, and the full edn / transit / json API reference.