How to serialize moment.js values with Transit

January 6, 2016, revised July 3, 2016 ClojureScript Moment.js Transit

I had to serialize some Moment.js values using the Transit library for ClojureScript.

At first the code seems really straightforward and by-the-book. We serialize moment objects into ISO datetime strings using .format(), tag them with "moment", and we unserialize back from the strings using the moment() constructor.

(require '[cognitect.transit :as t])

(deftype MomentHandler []
  Object
  (tag [this value] "moment")
  (rep [this value] (.format value)))

(let [writer (t/writer :json
                       {:handlers {Moment (MomentHandler.)}})
      reader (t/reader :json
                       {:handlers {"moment" #(js/moment %)}})]

  ; round trip serialization test
  (t/read reader (t/write writer (js/moment))))

But this code will fail, throwing “Cannot write Moment”. What does that even mean?

I’ve naively expected that “Moment” in the code snippet and the error message is the class of the datetime objects. But, there are no classes in JavaScript. Instead, what you pass into the writer :handlers map is the constructor function. When confronted with an non-standard value, Transit will match its constructor against the defined handlers.

Finding the Moment constructor requires digging around the source code and the documentation for moment.js, but finally we find it to be:

(defonce Moment (.fn js/moment))

When you prepend this declaration the code works. The same approach will work with any custom Javascript class - just pass its constructor into the :handlers map.

(Another thing - why do I wrap js/moment in the read handler rather than passing it directly? It’s not because of interop - unlike Java methods in Clojure, JavaScript functions can be used in place of any ClojureScript function. But, the reader function receives some extra parameters, and they must not leak into the moment() factory lest it would mistreat them.)

Buy Me a Coffee at ko-fi.com