Skip to content
Alex Miller edited this page Jan 30, 2016 · 14 revisions

Reading

In order to read Transit encoded JSON you need to construct a reader:

(ns try-transit
  (:require [cognitect.transit :as t]))

(def r (t/reader :json))

Currently :json is the only type of reader available. Given a reader you can invoke cognitect.transit/read.

For example reading a vector:

(def v (t/read r "[1,2,3]")) ;; [1 2 3]

And reading a map:

(def m (t/read r "{\"~:foo\":\"bar\"}")) ;; {:foo "bar"}

Writing

Like reading, writing is fairly straightforward. Constructing a writer looks very much like constructing a reader:

(ns try-transit
  (:require [cognitect.transit :as t]))

(def w (t/writer :json))

Once you've constructed a writer you can invoke cognitect.transit/write to encode values.

For example writing a vector:

(t/write w [1 2 3]) ;; "[1,2,3]"

And writing a map:

(t/write w {:foo "bar"}) ;; "[\"^ \",\"~:foo\",\"bar\"]"

Maps get written out as JSON arrays as this form is more efficient for decoding. For debugging purposes it's useful to construct a verbose writer:

(def wv (t/writer :json-verbose))

And now the result of writing map-like values is easier to read:

(t/write wv {:foo "bar"}) ;; "{\"~:foo\":\"bar\"}"

Writing Custom Values

Being able to easily write out graphs of ClojureScript values is one the big benefits of transit-cljs. transit-cljs will recursively encode graphs of values and Transit ground values like integers and dates need no special treatment.

To demonstrate this lets define some simple geometry primitives:

(defrecord Rect [origin size])
(defrecord Point [x y])
(defrecord Size [width height])

(def arect (Rect. (Point. 0 0) (Size. 150 150)))

In order to write out aRect we need write handlers for all of the types involved. First let's write a handler for Point:

(deftype PointHandler []
  Object
  (tag [this v] "point")
  (rep [this v] #js [(.-x v) (.-y v)])
  (stringRep [this v] nil))

Write handlers are simplest to write with deftype. Custom types always become tagged values on the wire and the handler methods specify how your instance will become a Transit tagged value. Write handlers must supply at least the first two of the three methods: tag, rep, and stringRep. Each handler method receives the value v to be written.

tag should be a method that will take the value and return a string based tag. You can of course use the value argument v to write out different tags if you like but we're going to keep it simple here.

rep is the representation to use for the tagged value. In this case we simply return an array containing the x and y properties. These properties are numbers, a ground type, so there's nothing more for us to do. It's important that the result of rep be something that transit-cljs already knows how to encode either via a built-in or provided custom handler.

stringRep is for tagged values that have a sensible representation as JSON object keys (strings). For the most part you can omit this method but we've left it here for completeness.

Now we can construct the following verbose writer and write Point instances:

(def w 
  (t/writer :json-verbose
    {:handlers {Point (PointHandler.)}}))

(t/write w (Point. 1.5 2.5)) ;; => "{\"~#point\":[1.5,2.5]}"

Now let's write the handlers for Size and Rect:

(deftype SizeHandler []
  Object
  (tag [this v] "size")
  (rep [this v] #js [(.-width v) (.-height v)])
  (stringRep [this v] nil))

(deftype RectHandler []
  Object
  (tag [this v] "rect")
  (rep [this v] #js [(.-origin v) (.-size v)])
  (stringRep [this v] nil))

That's it, we can now write out Rect instances!

(def w
  (t/writer :json-verbose
    {:handlers 
      {Point (PointHandler.)
       Size  (SizeHandler.)
       Rect  (RectHandler.)}}))

(t/write w arect) 
;; => "{\"~#rect\":[{\"~#point\":[0,0]},{\"~#size\":[150,150]}]}"

Reading Custom Types

Now that we can write custom types we will want to be able read them.

(def r
  (t/reader :json
    {:handlers 
      {"point" (fn [[x y]] (Point. x y))
       "size"  (fn [[width height]] (Size. width height))
       "rect"  (fn [[origin size]] (Rect. origin size))}}))

(t/read r "{\"~#rect\":[{\"~#point\":[0,0]},{\"~#size\":[150,150]}]}")
;; => #try-transit.Rect{:origin #try-transit.Point{:x 0, :y 0}, 
;;                      :size #try-transit.Size{:width 150, :height 150}}

Reading is considerably simpler. When a tagged value is encountered the corresponding handler is invoked with the representation that was written on the wire - in our case we just used arrays (we could have used maps).

Notice that the Rect handler doesn't need to instantiate Point or Size. Again transit-cljs is recursive and these will have already been instantiated for you.

Clone this wiki locally