Skip to main content

Data Formats

On this page

Phel ships two eval-free interchange modules for exchanging data with other Clojure-aligned runtimes, or with itself across process boundaries:

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#

FunctionPurpose
(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#

KeyValueBehaviour
: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.
:eofanyReturned 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-data

phel.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#

FunctionPurpose
(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#

KeyValueBehaviour
: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#

PhelTransit JSON-Verbose
nilnull
booltrue / false
int, floatJSON number
stringJSON string (with ~/^/` escape)
keyword"~:name"
symbol"~$name"
Phel\Lang\UUID"~uXXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX"
DateTimeImmutable"~tISO-8601"
vectorJSON 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 :handlers map on write-string yet. Encode custom types yourself before handing them to write-string.

Choosing between EDN and Transit#

NeedUse
Phel-to-Phel config files, source-like dataEDN
Talking to Clojure/Java/JS over HTTP, MessagePack-ready wire formatTransit
Human-edited input with comments and #_ discardsEDN
Round-tripping JSON-shaped data with rich typesTransit

See also: Cookbook for JSON and CSV recipes, and the full edn / transit / json API reference.