Search code examples
jsoncommon-lispclos

How to convert json-string into CLOS object using cl-json library?


If there is a class and a json:

(defclass foo ()
    ((bar :initarg :bar)))

(defvar *input* "\{ \"bar\" : 3 }")

How to convert *input* into an instance of foo using cl-json library?

I guess it should be something like:

(with-decoder-simple-clos-semantics
    (let ((*prototype-name* 'foo))
      (decode-json-from-string *input*)))

But it produces:

Invalid SB-MOP:SLOT-DEFINITION initialization: the
initialization argument :NAME was constant: :BAR.
   [Condition of type SB-PCL::SLOTD-INITIALIZATION-ERROR]

What am I doing wrong?


Solution

  • The cause of the error is that cl-json:*json-symbols-package* is bound to the KEYWORD package: when JSON keys are turned into symbols, they become keywords which apparently are not valid as slot names.

    Fluid objects

    The following works:

    (let ((json:*json-symbols-package* (find-package :cl-user)))
      (json:with-decoder-simple-clos-semantics
        (json:decode-json-from-string "{ \"bar\" : 3 }")))
    

    (note: you only need backslashes before double-quote characters)

    You obtain a FLUID-OBJECT.

    Prototype key in JSON data

    Now, you can also define your own class:

    (in-package :cl-user)
    (defclass foo ()
      ((bar :initarg :bar)))
    

    And then, the JSON needs to have a "prototype" key:

     (let ((json:*json-symbols-package* (find-package :cl-user)))
       (json:with-decoder-simple-clos-semantics
         (json:decode-json-from-string
          "{ \"bar\" : 3 , 
             \"prototype\" : { \"lispClass\" : \"foo\", 
                               \"lispPackage\" : \"cl-user\"  }}")))
    

    The above returns an instance of FOO.

    You can use a different key than "prototype" by rebinding *prototype-name*.

    Force a default prototype (hack)

    Without changing the existing library code, you can hack around it to change the behavior of the decoding step. The code is organized around special variables that are used as callbacks at various point of the parsing, so it is a matter of wrapping the expected function with your own:

    (defun wrap-for-class (class &optional (fn json::*end-of-object-handler*))
      (let ((prototype (make-instance 'json::prototype :lisp-class class)))
        (lambda ()
          ;; dynamically rebind *prototype* right around calling fn
          (let ((json::*prototype* prototype))
            (funcall fn)))))
    

    The above creates a prototype object for the given class (symbol), capture the current binding of *end-of-object-handler*, and returns a closure that, when called, bind *prototype* to the closed-over prototype instance.

    Then, you call it as follows:

    (let ((json:*json-symbols-package* *package*))
      (json:with-decoder-simple-clos-semantics
        (let ((json::*end-of-object-handler* (wrap-for-class 'foo)))
          (json:decode-json-from-string
           "{ \"bar\" : 3 }"))))
    

    And you have an instance of FOO.

    Recursion

    Note that if you define foo as follows:

    (defclass foo ()
      ((bar :initarg :bar :accessor bar)
       (foo :initarg :foo :accessor foo)))
    

    Then the hack also reads nested JSON objects as FOO:

    (let ((json:*json-symbols-package* *package*))
      (json:with-decoder-simple-clos-semantics
        (let ((json::*end-of-object-handler* (wrap-for-class 'foo)))
          (json:decode-json-from-string
           "{ \"bar\" : 3, \"foo\" : { \"bar\" : 10} }"))))
    
    => #<FOO {1007A70E23}>
    
    > (describe *)
    #<FOO {1007A70E23}>
      [standard-object]
    
    Slots with :INSTANCE allocation:
      BAR                            = 3
      FOO                            = #<FOO {1007A70D53}>
    
    > (describe (foo **))
    #<FOO {1007A70D53}>
      [standard-object]
    
     Slots with :INSTANCE allocation:
      BAR                            = 10
      FOO                            = #<unbound slot>