问题
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?
回答1:
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>
来源:https://stackoverflow.com/questions/55015871/how-to-convert-json-string-into-clos-object-using-cl-json-library