Subject: bug#24252: 25.1; json.el doesn't distinguish null and empty object

From: Damien Cassou <[email-masked]>
Date: Sat, 19 May 2018 08:52:42 +0200

Yoichi Nakayama <[email-masked]> writes:
> When json-pretty-print applied to "{}", it is
> unexpectedly converted to "null".
> This is caused by internal representations of null
> and empty object are the same:
>   (json-read-from-string "{}") ; => nil
>   (json-read-from-string "null") ; => nil

please find a patch attached. This patch uses :json-null to distinguish,
during pretty-print, between the value null and an empty object.

All tests pass and many new tests are added.

Best
>From 78eb7b659809444f7c9690db73e1f65843d5658c Mon Sep 17 00:00:00 2001
From: Damien Cassou <[email-masked]>
Date: Sat, 19 May 2018 08:36:32 +0200
Subject: [PATCH] Fix pretty-printing empty objects as null

* lisp/json.el (json-pretty-print): Force distinction between empty
  objects and null.
(json-encode-list): Remove responsibility to print "null" as this
value is not a list.
(json-encode): Give higher precedence to lists so that an empty list
is printed as an empty object, not as "null".

* test/lisp/json-tests.el (test-json-encode): Add many tests to check
  the behavior of pretty-printing.
---
 lisp/json.el            |  7 +++--
 test/lisp/json-tests.el | 67 +++++++++++++++++++++++++++++++++++++++++
 2 files changed, 71 insertions(+), 3 deletions(-)

diff --git a/lisp/json.el b/lisp/json.el
index d374f452e6..cd95ec2832 100644
--- a/lisp/json.el
+++ b/lisp/json.el
@@ -609,8 +609,7 @@ json-encode-list
   "Return a JSON representation of LIST.
 Tries to DWIM: simple lists become JSON arrays, while alists and plists
 become JSON objects."
-  (cond ((null list)         "null")
-        ((json-alist-p list) (json-encode-alist list))
+  (cond ((json-alist-p list) (json-encode-alist list))
         ((json-plist-p list) (json-encode-plist list))
         ((listp list)        (json-encode-array list))
         (t
@@ -723,12 +722,12 @@ json-encode
         ((stringp object)      (json-encode-string object))
         ((keywordp object)     (json-encode-string
                                 (substring (symbol-name object) 1)))
+        ((listp object)        (json-encode-list object))
         ((symbolp object)      (json-encode-string
                                 (symbol-name object)))
         ((numberp object)      (json-encode-number object))
         ((arrayp object)       (json-encode-array object))
         ((hash-table-p object) (json-encode-hash-table object))
-        ((listp object)        (json-encode-list object))
         (t                     (signal 'json-error (list object)))))
 
 ;; Pretty printing
@@ -743,6 +742,8 @@ json-pretty-print
   (interactive "r")
   (atomic-change-group
     (let ((json-encoding-pretty-print t)
+          ;; Distinguish an empty objects from 'null'
+          (json-null :json-null)
           ;; Ensure that ordering is maintained
           (json-object-type 'alist)
           (txt (delete-and-extract-region begin end)))
diff --git a/test/lisp/json-tests.el b/test/lisp/json-tests.el
index ea562e8b13..84039c09ce 100644
--- a/test/lisp/json-tests.el
+++ b/test/lisp/json-tests.el
@@ -325,5 +325,72 @@ json-tests--with-temp-buffer
   (with-temp-buffer
     (should-error (json-encode (current-buffer)) :type 'json-error)))
 
+;;; Pretty-print
+
+(defun json-tests-equal-pretty-print (original &optional expected)
+  "Abort current test if pretty-printing ORIGINAL does not yield EXPECTED.
+
+Both ORIGINAL and EXPECTED should be strings.  If EXPECTED is
+nil, ORIGINAL should stay unchanged by pretty-printing."
+  (with-temp-buffer
+    (insert original)
+    (json-pretty-print-buffer)
+    (should (equal (buffer-string) (or expected original)))))
+
+(ert-deftest test-json-pretty-print-string ()
+  (json-tests-equal-pretty-print "\"\"")
+  (json-tests-equal-pretty-print "\"foo\""))
+
+(ert-deftest test-json-pretty-print-atom ()
+  (json-tests-equal-pretty-print "true")
+  (json-tests-equal-pretty-print "false")
+  (json-tests-equal-pretty-print "null"))
+
+(ert-deftest test-json-pretty-print-number ()
+  (json-tests-equal-pretty-print "123")
+  (json-tests-equal-pretty-print "0.123"))
+
+(ert-deftest test-json-pretty-print-object ()
+  ;; empty (regression test for bug#24252)
+  (json-tests-equal-pretty-print
+   "{}"
+   "{\n}")
+  ;; one pair
+  (json-tests-equal-pretty-print
+   "{\"key\":1}"
+   "{\n  \"key\": 1\n}")
+  ;; two pairs
+  (json-tests-equal-pretty-print
+   "{\"key1\":1,\"key2\":2}"
+   "{\n  \"key1\": 1,\n  \"key2\": 2\n}")
+  ;; embedded object
+  (json-tests-equal-pretty-print
+   "{\"foo\":{\"key\":1}}"
+   "{\n  \"foo\": {\n    \"key\": 1\n  }\n}")
+  ;; embedded array
+  (json-tests-equal-pretty-print
+   "{\"key\":[1,2]}"
+   "{\n  \"key\": [\n    1,\n    2\n  ]\n}"))
+
+(ert-deftest test-json-pretty-print-array ()
+  ;; empty
+  (json-tests-equal-pretty-print "[]")
+  ;; one item
+  (json-tests-equal-pretty-print
+   "[1]"
+   "[\n  1\n]")
+  ;; two items
+  (json-tests-equal-pretty-print
+   "[1,2]"
+   "[\n  1,\n  2\n]")
+  ;; embedded object
+  (json-tests-equal-pretty-print
+   "[{\"key\":1}]"
+   "[\n  {\n    \"key\": 1\n  }\n]")
+  ;; embedded array
+  (json-tests-equal-pretty-print
+   "[[1,2]]"
+   "[\n  [\n    1,\n    2\n  ]\n]"))
+
 (provide 'json-tests)
 ;;; json-tests.el ends here
-- 
2.17.0

-- 
Damien Cassou
http://damiencassou.seasidehosting.st

"Success is the ability to go from one failure to another without
losing enthusiasm." --Winston Churchill