Most EOFs produce helpful error messages like JSON error (end-of-file inside string). But the object and array readers don't handle EOF and throw unhelpful exceptions.
(clojure.data.json/read-str "{") throws Value out of range for char: -1 due to this line in read-key:
        (throw (Exception. (str "JSON error (non-string key in object), found " (char c) ", expected \"")))
The (char c) throws because c is -1.
This is fixed by handling the -1 case in read-key:
(defn- read-key [^PushbackReader stream]
  (let [c (int (next-token stream))]
    (if (= c (codepoint \"))
      (let [key (read-quoted-string stream)]
        (if (= (codepoint \:) (int (next-token stream)))
          key
          (throw (Exception. "JSON error (missing `:` in object)"))))
      (codepoint-case c
        \} nil
        -1 (throw (Exception. "JSON error (end-of-file inside object)"))
        (throw (Exception. (str "JSON error (non-string key in object), found `" (char c) "`, expected `\"`")))))))
(clojure.data.json/read-str "{\"\":\"\"") gives an incorrect error message: JSON error (missing entry in object)
This is fixed by handling EOF in read-object:
  (codepoint-case (int (next-token stream))
    \, (recur r)
    \} (persistent! r)
    -1 (throw (Exception. "JSON error (end-of-file inside object)"))
    (throw (Exception. "JSON error (missing entry in object)"))))
(clojure.data.json/read-str "[") throws JSON error (unexpected character):  (the unexpected character is (char 65535)). This is because read-array pushes -1 back onto the stream, and -1 gets pushed as 65535.
The fix is to handle EOF inside read-array:
(defn- read-array [^PushbackReader stream options]
  ;; Expects to be called with the head of the stream AFTER the
  ;; opening bracket.
  ;; Only handles array value.
  (let [c (int (next-token stream))]
    (codepoint-case c
      \] []
      \, (throw (invalid-array-exception))
      -1 (throw (Exception. "JSON error (end-of-file inside array)"))
      (do (.unread stream c)
          (read-array* stream options)))))