Cyrax
Cyrax

Reputation: 115

Clojure defrecord serialization ClassNotFoundException

I was trying to serialise one of my records to a human readable format. While serialising using Java serialiser worked fine I am trying to use print-dup. The problem I am facing is that while writing the record goes fine reading the record results in clojure.lang.LispReader$ReaderException: java.lang.ClassNotFoundException: common.dummy.Doodh. Am I messing up the namespaces or something? Please note that this is not a problem with Java serialisation. Code below in the simplest form

(ns common.dummy)

   (defrecord Doodh [id name])

   (defn output [filename obj]
    (def trr(map->Doodh {:id "moooh" :name "Cows"}))
    (def my-string (binding [*print-dup* true] (pr-str trr)))
    (spit filename my-string)
   )

   (defn pull [filename]
     (def my-data (with-in-str (slurp filename) (read)))
     (println my-data)
   )

text file contents:

#common.dummy.Doodh["moooh", "Cows"]

Upvotes: 3

Views: 935

Answers (1)

Leon Grapenthin
Leon Grapenthin

Reputation: 9266

  • Don't use def inside function definitions. When you are using def, you create a var in your namespace and possibly manipulate it as a side-effect with every function call. Use let-blocks.

  • If you want to save Clojure data structures in a file, use clojure.edn. It is safe (e. g. without your knowledge, no functions defined in the file will be invoked) but it allows to enable custom readers (see more below).

  • A type defined with defrecord can be printed in a (Clojure-reader-)readable way using pr-str (thanks to @A. Webb for noting that). In your example, I don't see why you would not stick to a hash-map in the first place, but if you really need a defrecord here, you may convert it into a readable string before your write it to the file.

    (defrecord Doodh [id name])
    
    (defn output [filename obj]
      (spit filename (pr-str obj))
    
    (defn pull [filename]
      (with-in-str (slurp filename)
                   (read)))
    
    • This way of doing it has several disadvantages.
      • Using read makes your code vulnerable to function calls in the slurped files (like #=(java.lang.System/exit 0)).
      • An exception will be thrown when the file at filename is empty.
      • Finally your saved file will become incompatible to your code when you move your defrecord declaration to another namespace.
      • All three disadvantages can be avoided by using the edn-reader.

Using a custom reader with EDN

  1. We extend our type Doodh by implementing the toString method of the java.lang.Object interface:

    (defrecord Doodh [id name]
      Object
      (toString [this] (str "#Doodh" (into {} this))))
    
  2. Because spit uses str, we can now omit the output function and simply invoke spit from e. g. the REPL:

    (spit "Doodh.edn" (map->Doodh {:id "134" :name "Berta"}))
    

    Doodh.edn: #Doodh{:id 134, :name "Berta"}

  3. Now to make sure that the Doodh will be read back, we invoke clojure.edn/read-string with a custom reader function:

    (defn pull [filename]
      (->> (slurp filename)
           (clojure.edn/read-string {:readers {'Doodh map->Doodh}})))
    
  4. If you read back "Doodh.edn" using the new pull, you should receive a valid Doodh. At the REPL:

    (pull "Doodh.edn")
    => #user.Doodh{:id 134, :name "Berta"}
    

Upvotes: 6

Related Questions