Library: Cheshire

Library: Cheshire

Grinning like a Cheshire Cat

Anytime I work with JSON and Clojure my mind goes immediately to a library that puts a grin on my face: Cheshire. This is not only due to the great name, but it is truly a library that betters the Clojure landscape - fast, effective, and easy to use. To show my (hopefully well-received) appreciation to the creator and maintainer, Lee Hinman, I have put together a small example that makes use of the library. The aim is to complement Cheshire's already good documentation with a working example.

Cheshire

Cheshire is a JSON encoding and decoding library for Clojure that comes with nice bonus features such as Smile support and custom encoding/decoding. Smile is a JSON-compatible binary data format. To simplify matters, I will stick to JSON.

Encoding produces JSON from standard Clojure data structures and even useful Java objects, such as Date. Decoding produces data structures consumable by Clojure, which can be used directly (e.g., {:foo "bar" :taco 3}).

Alternatives

Some alternatives mentioned on Cheshire's README are clojure-json and clj-json. There is also data.json. I will leave it to you to consider the others, but comparing the number of favorites and forks on Github would suggest Cheshire to be the most popular.

Unfortunately, I won't be able to cover all Cheshire has to offer, or compare it to other libraries, but this should let anyone hit the ground running when it comes to using encoding and decoding JSON.

Down the rabbit hole

Let's suppose you have a library in Java you need to work with. Let's further suppose it is an imaginary library for tracing your pet cat's family tree. Unfortunately, all that stands between you and the family tree is that the library only accepts and returns a String containing JSON. "How unfortunate!" you proclaim. But fret not, for you have Cheshire and Clojure.

Which way you ought to go depends on where you want to get to...

As it is almost tea time, we will have to ignore what the imaginary cat family tree library could return and instead focus on what to feed it. The information is on the matriarch of the cat family tree and how much she cost to purchase. We've already assembled the information in a map and assigned it to the var cat-map.

(def cat-map
 {:id 1 
 :name "Fajita" 
 :type "Cat" 
 :price 70.50 
 :colors ["white" "orange" "black" "brown"]
 :born (java.util.Date. 971059433000)})

As the library in question does not support the superior data format that is edn, the simplest thing we can do is parse the above into JSON with (generate-string cat-map). Of course, we can only do so in the REPL if we first require Cheshire with (:require '[cheshire.core :refer [generate-string parse-string]]). Binding to the cat-json var and encoding JSON returns:

(def cat-json (generate-string cat-map))
=> #'user/cat-json

cat-json
=> {\"id\":1,
    \"name\":\"Fajita\",
    \"type\":\"Cat\",
    \"price\":70.5,
    \"colors\":[\"white\",\"orange\",\"black\",\"brown\"],
    \"born\":\"2000-10-09T02:43:53Z\"}

Certainly not the prettiest of things. One of the annoyances with JSON is the need to surround a string with escaped quotation marks. If you were to try running the result from cat-json without the \" everywhere (using just ") then you would have a bad time. We should confirm it is valid JSON by decoding this with parse-string:

(parse-string cat-json)
=> {"id" 1, 
    "name" "Fajita", 
    "type" "Cat", 
    "price" 70.5, 
    "colors" ["white" "orange" "black" "brown"], 
    "born" "2000-10-09T02:43:53Z"}

Should the JSON be malformed (e.g., missing a \" or :), we would most likely get a JsonParseException. Fortunately, ours parses just fine but looks a little different than what went in. It seems the original cat-map had keywords for its keys and this does not. And what about the commas... What gives?

The Mad Mapper

Well, as you might already know, what was returned is still a valid Clojure map. Commas are treated as whitespace and may not print in your REPL, depending on the setup. As for the keys, it turns out you have quite a bit of freedom in what can be used as a key for Clojure. Let's try to retrieve some different values using get:

;; Keywords are not the only acceptable keys in Clojure.

;; Here we have integers. Looking for the key '3' we return the value '6'
(get {3 6 0 1} 3)
=> 6

;; If nothing is found, nil is returned.
(get {3 6} 7)
=> nil

;; Vectors are also acceptable.
(get {[1 2] true} [1 2])
=> true

;; Specifying a fallback value if nil is an option.
;; Could also be written (:name {:id 3} "No name")
(get {:id 3} :name "No name") 
=> "No name"

;; Strings are fine too.
(get {"name" "Fajita"} "name")
=> "Fajita"

;; A sister function to get is get-in, which can retrieve values from nested maps.
(get-in {:index {:running "3" :jump "page 44"}} [:index :jumping])
=> "44"

Now that we have expanded our minds with retrieving values from a map using get and get-in, we can focus the discrepancy between the original cat-map and (parse-string (generate-string cat-map)). What should have been done initially, if we wanted keywords, is to use a true value after the map for parse-string:

(parse-string cat-json true)
=> {:name "Fajita", 
    :type "Cat", 
    :colors ["white" "orange" "black" "brown"], 
    :id 1, 
    :price 70.5}

As mentioned earlier, the commas are considered whitespace by Clojure, so this is equivalent to what we initially put in. Not impressed? How about this - if we wanted to do something fancier, like munging the keywords, you can simply replace true with a function:

;; Same thing as using true.
(parse-string (generate-string {:fish "Bass" :veggie "Carrot"}) (fn [k] (keyword k)))
=>  {:veggie "Carrot", :fish "Bass"}

;; Append "tail" to the key then keywordize.
(parse-string (generate-string {:fish "Bass" :veggie "Carrot"}) (fn [k] (keyword (str k "tail"))))
=> {:veggietail "Carrot", :fishtail "Bass"}

Java Jabberwocky

This is not all that Cheshire can do. For instance, while it supports Clojure data structures and some popular Java objects such as Date and UUID, what should you do if you need to encode or decode other Java objects? Why, implement your own custom encoding and decoding of course! Suppose there was a need for a Point object. One way to encode it could be:

;; Don't forget to require cheshire.generate.
(:require '[cheshire.generate :refer [add-encoder encode-str remove-encoder]])

;; This encoder will only write the Java object to string. Nothing fancy.
(add-encoder java.awt.Point 
             (fn [c jsonGenerator] 
               (.writeString jsonGenerator (str c))))
=> nil

(generate-string (java.awt.Point. 10 20))
=> "\"java.awt.Point[x=10,y=20]\""

;; However, the following encoder grabs the fields of Point and does something a bit fancier.
;; Note: You might want to remove the old encoder before trying to add this one. My brief
;;         testing suggested the new encoder just overwrites, but better safe than sorry.
(remove-encoder java.awt.Point)
=> nil

(add-encoder java.awt.Point 
             (fn [c jsonGenerator] 
               (.writeString jsonGenerator (str (.getX c) ", " (.getY c)))))
=> nil

(generate-string (java.awt.Point. 10 20))
=> "\"10.0, 20.0\""

Unfortunately, there does not appear to be any custom decoding... simply the normal decoding from parse-string. While unfortunate, it is fairly simple to work around. By now, you should be on your way to becoming an expert Cheshire user.

Tea time

One last tip: If you find generate-string and parse-string too cumbersome to use (read: too long to type) then give encode and decode a try. They are aliases of the former and latter.

With that I must depart for tea as I am late. Hopefully this has encouraged you to consider using Cheshire whenever the need to convert JSON arises. Don't forget - it has quite helpful documentation that won't leave you mad as a hatter.