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?
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.
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
.
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*
.
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
.
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>