JSON encoder/decoder

1 view
Skip to first unread message

Michael Harrison (goodmike)

unread,
May 14, 2009, 5:23:56 PM5/14/09
to Clojure Study Group Washington DC
I found a Clojure JSON encoder/decoder: Dan Larkin's clojure-json at
http://github.com/danlarkin/clojure-json/tree/master

It's pretty easy to hook up to the word-count code I've already posted
here:

(use 'org.danlarkin.json)

; code that builds sorted-word-counts ...

(def word-count-json (encode-to-str sorted-word-counts))

user> word-count-json
"{\"000\":1,\"100\":3,\"11\":2,\"120\":2,\"13\":1,\"150\":2,\"2002\":
2,\"2009\":1,\"30\":1,\"401\":2,\"50\":1,\"500\":1,\"789\":2,\"about\":
7,\"abuse\":2,\"accident\":1,\"according\":1,\"accused\":1,\"accuses\":
2,\"across\":1,\"actress\":1,\"administration\":1,\"admits\":
1,\"affecting\":1, ...}

Nothing fancy. Just JSON.

There are some things I really like about the clojure-json library,
and I think they're worth mentioning here.

First, it's nice and small, so it's easy to grok in a close reading.
It also serves as a simple example for library construction.

The code is in a filepath that maps to the org.danlarkin.json
namespace:
src > org > danlarkin

Inside the danlarkin folder is a main file: json.clj. This is where
the public functions that do the work reside, functions like encode-to-
str. Also inside danlarkin is a 'json' folder with library files:
decoder.clj and encoder.clj. The functions in the main file make use
of the functions and definitions in these files.

This is a standard way of laying out libraries: a main file named
xxx.clj in a directory like zzz > yyy will use a namespace
zzz.yyy.xxx. Then an xxx folder will contain one or more auxiliary clj
files.

The file encoder.clj has some interesting code inside it:

(def
#^{:private true}
separator-symbol ;separator-symbol will be used for encoding
(symbol ",")) ;commas in arrays and objects: [a,b,c] and
{a:b,c:d}

(declare encode-helper) ;encode-helper is used before it's defined
;so we have to pre-define it

(defn- map-entry?
"Returns true if x is a MapEntry"
[x]
(instance? clojure.lang.IMapEntry x))

The #^{:private true} inside the definition of separator-symbol is
metadata, but it does more than just document. It makes this value
private in the sense of the Java keyword.

Stu writes about declare. Its use in the encoder code is pretty
straightforward: encode-helper is going to be used in several function
defintions before it's defined. Why? Well, because encode-helper is
itself going to reference these functions.

Then, how about (defn- map-entry? ...)
The defn- macro defines a function, but makes it "non-public".
Visibility control is easy in Clojure. Here's what happens if I try to
refer to map-entry?:

user> org.danlarkin.json.encoder/map-entry?
java.lang.IllegalStateException: var: org.danlarkin.json.encoder/map-
entry? is not public


One other cool thing about clojure-json: it's actually skeletal, but
the means to extend it is provided.

For example, can encode-to-str handle a Clojure datastructure with a
Java date object in it?

user> (encode-to-str {:time (java.util.Date.)})
java.lang.RuntimeException: java.lang.Exception: Unknown
Datastructure: Thu May 14 17:13:00 EDT 2009

Nope. It doesn't know what to do with it. But there's a macro provided
to generate a custom encoder for the class. It's called add-encoder.
You send it a type-dispatcher, which can be a class name or a
function, and a function that takes an instance of the target class
(es) and encodes it. Here's the simple example from the json.clj file:

(add-encoder java.util.Date
(fn [#^java.util.Date date #^Writer writer
#^String pad #^String current-indent #^String start-
token-indent #^Integer indent-size]
(.append writer (str start-token-indent \" date \"))))

As you might imagine from the first argument here being a type-
dispatcher, this macro resolves to a defmethod that adds a method to a
multimethod, in this case encode-custom.

If you try this yourself from the REPL, you'll have to specify the
Writer class you mean:

user> (add-encoder java.util.Date
(fn [#^java.util.Date date #^java.io.Writer writer
#^String pad #^String current-indent #^String start-
token-indent #^Integer indent-size]
(.append writer (str start-token-indent \" date \"))))
#<MultiFn clojure.lang.MultiFn@155f16>
user> (encode-to-str {:time (java.util.Date.)})
"{\"time\":\"Thu May 14 17:23:12 EDT 2009\"}"

That's it for now. I hope to see you on Saturday.

Cheers,
Michael
Reply all
Reply to author
Forward
0 new messages