Now, as to what Hans has proposed regarding the encoder. I completely side with his idea of a streaming API. Currently, the only possible means of customization is to add methods to the ENCODE-JSON generic function. This is, obviously, not very good, because (a) the GF eventually gets cluttered by multitude obscure heterogenous methods; (b) it is not possible to have different encoding strategies for one and the same data type; (c) the library really adds no value, as the technicalities of JSON syntax still have to be dealt with inside the new method.
I have to admit I don't quite understand Henrik's objections against the proposal. As far as I see, there is no conflict at all between that and the current simple API, as the latter can be absolutely gracefully reimplemented on top of the former. E.g., instead of
(defmethod encode-json ((s sequence) stream) (with-sequence-iterator (generator s) (write-json-array generator stream)))
we could write something like
(defmethod encode-json ((s sequence) stream) (with-json-array (:stream stream) (map nil #'encode-array-element s)))
The user of the library is, of course, free to write his own encoder specialized for his own classes, as nothing prevents him from calling encode-json in his default method, e.g.:
(defmethod my-own-encode-json ((obj my-class) stream) (with-json-object (:stream stream) (with-object-element ("initial") (my-own-encode-json (get-value-for-initial-field obj) stream)) (with-object-element ("subsequent") (my-own-encode-json (get-value-for-subsequent-field obj) stream))))
(defmethod my-own-encode-json ((value t) stream) (encode-json value stream))
CL-JSON can very well provide some syntactic sugar for objects, along the lines of:
(defmacro with-json-object-for-fields ((field (&rest fields) &key stream) &body body) `(with-json-object (:stream stream) ,@(loop for (name getter) in fields collect `(with-object-element (,name) (let ((,field ,getter)) ,@body)))))
Which allows us to rewrite the method above as
(defmethod my-own-encode-json ((obj my-class) stream) (with-json-object-for-slots (slot (("initial" (get-value-for-initial-field obj)) ("subsequent" (get-value-for-subsequent-field obj))) :stream stream) (my-own-encode-json slot stream)))
This kind of encoder transparency shall make for a natural and useful counterpart to decoder customizability.
Sincerely, - B. Smilga.
P.S. I was thinking about a customizable encoder myself, but only came up with a very clumsy idea of exporting WRITE-JSON-OBJECT and WRITE-JSON-ARRAY, and having their GENERATOR-FN arguments return callbacks where customized encoding is needed. We would have
(defmethod encode-json ((fn function) stream) (funcall fn stream))
and then use that in the following manner:
(defmethod my-own-encode-json ((obj my-class) stream) (write-json-object (make-my-class-iterator obj) stream))
(defun make-my-class-iterator (obj) (let ((state :initial)) (lambda () (ecase state (:initial (setq state :subsequent) (values t :initial-field (lambda (stream) ; a callback (my-own-encode-json (get-value-for-initial-field obj) stream)))) (:subsequent (setq state :final) (values t :subsequent-field (lambda (stream) ; another one (my-own-encode-json (get-value-for-subsequent-field obj) stream)))) (:final nil)))))
Compared to Hans's approach, mine is just an insult to good taste and common sense.