Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with
or
.
Download ZIP
Fetching contributors…

Cannot retrieve contributors at this time

338 lines (276 sloc) 11.732 kB
;;; json/read.clj: JavaScript Object Notation (JSON) parser
;; by Stuart Sierra, http://stuartsierra.com/
;; February 13, 2009
;; Copyright (c) Stuart Sierra, 2009. All rights reserved. The use
;; and distribution terms for this software are covered by the Eclipse
;; Public License 1.0 (http://opensource.org/licenses/eclipse-1.0.php)
;; which can be found in the file epl-v10.html at the root of this
;; distribution. By using this software in any fashion, you are
;; agreeing to be bound by the terms of this license. You must not
;; remove this notice, or any other, from this software.
;; Change Log
;;
;; February 13, 2009: added custom handler for quoted strings, to
;; allow escaped forward backslash characters ("\/") in strings.
;;
;; January 26, 2009: initial version
;; For more information on JSON, see http://www.json.org/
;;
;; This library parses data in JSON format. This is a fairly strict
;; implementation of JSON as described at json.org, not a full-fledged
;; JavaScript parser. JavaScript functions and object constructors
;; are not supported. Object field names must be quoted strings; they
;; may not be bare symbols.
(ns
#^{:author "Stuart Sierra",
:doc "JavaScript Object Notation (JSON) parser
For more information on JSON, see http://www.json.org/
This library parses data in JSON format. This is a fairly strict
implementation of JSON as described at json.org, not a full-fledged
JavaScript parser. JavaScript functions and object constructors
are not supported. Object field names must be quoted strings; they
may not be bare symbols.
If you want to convert map keys from strings to keywords, use
clojure.contrib.walk/keywordize-keys
",
:see-also [["http://www.json.org", "JSON Home Page"]]}
clojure.contrib.json.read
(:import (java.io PushbackReader StringReader EOFException))
(:use [clojure.contrib.test-is :only (deftest- is)]))
(declare read-json)
(def #^{:doc "If true, JSON object keys will be converted to keywords
instead of strings. Defaults to false. There are no checks that
the strings form valid keywords."} *json-keyword-keys* false)
(defn- read-json-array [#^PushbackReader stream]
;; Expects to be called with the head of the stream AFTER the
;; opening bracket.
(loop [i (.read stream), result []]
(let [c (char i)]
(cond
(= i -1) (throw (EOFException. "JSON error (end-of-file inside array)"))
(Character/isWhitespace c) (recur (.read stream) result)
(= c \,) (recur (.read stream) result)
(= c \]) result
:else (do (.unread stream (int c))
(let [element (read-json stream)]
(recur (.read stream) (conj result element))))))))
(defn- read-json-object [#^PushbackReader stream]
;; Expects to be called with the head of the stream AFTER the
;; opening bracket.
(loop [i (.read stream), key nil, result {}]
(let [c (char i)]
(cond
(= i -1) (throw (EOFException. "JSON error (end-of-file inside object)"))
(Character/isWhitespace c) (recur (.read stream) key result)
(= c \,) (recur (.read stream) nil result)
(= c \:) (recur (.read stream) key result)
(= c \}) (if (nil? key)
result
(throw (Exception. "JSON error (key missing value in object)")))
:else (do (.unread stream i)
(let [element (read-json stream)]
(if (nil? key)
(if (string? element)
(recur (.read stream) element result)
(throw (Exception. "JSON error (non-string key in object)")))
(recur (.read stream) nil
(assoc result (if *json-keyword-keys* (keyword key) key)
element)))))))))
(defn- read-json-hex-character [#^PushbackReader stream]
;; Expects to be called with the head of the stream AFTER the
;; initial "\u". Reads the next four characters from the stream.
(let [digits [(.read stream)
(.read stream)
(.read stream)
(.read stream)]]
(when (some neg? digits)
(throw (EOFException. "JSON error (end-of-file inside Unicode character escape)")))
(let [chars (map char digits)]
(when-not (every? #{\0 \1 \2 \3 \4 \5 \6 \7 \8 \9 \a \b \c \d \e \f \A \B \C \D \E \F}
chars)
(throw (Exception. "JSON error (invalid hex character in Unicode character escape)")))
(char (Integer/parseInt (apply str chars) 16)))))
(defn- read-json-escaped-character [#^PushbackReader stream]
;; Expects to be called with the head of the stream AFTER the
;; initial backslash.
(let [c (char (.read stream))]
(cond
(#{\" \\ \/} c) c
(= c \b) \backspace
(= c \f) \formfeed
(= c \n) \newline
(= c \r) \return
(= c \t) \tab
(= c \u) (read-json-hex-character stream))))
(defn- read-json-quoted-string [#^PushbackReader stream]
;; Expects to be called with the head of the stream AFTER the
;; opening quotation mark.
(let [buffer (StringBuilder.)]
(loop [i (.read stream)]
(let [c (char i)]
(cond
(= i -1) (throw (EOFException. "JSON error (end-of-file inside string)"))
(= c \") (str buffer)
(= c \\) (do (.append buffer (read-json-escaped-character stream))
(recur (.read stream)))
:else (do (.append buffer c)
(recur (.read stream))))))))
(defn read-json
"Read one JSON record from s, which may be a String or a
java.io.PushbackReader."
([] (read-json *in* true nil))
([s] (if (string? s)
(read-json (PushbackReader. (StringReader. s)) true nil)
(read-json s true nil)))
([#^PushbackReader stream eof-error? eof-value]
(loop [i (.read stream)]
(let [c (char i)]
(cond
;; Handle end-of-stream
(= i -1) (if eof-error?
(throw (EOFException. "JSON error (end-of-file)"))
eof-value)
;; Ignore whitespace
(Character/isWhitespace c) (recur (.read stream))
;; Read numbers, true, and false with Clojure reader
(#{\- \0 \1 \2 \3 \4 \5 \6 \7 \8 \9} c)
(do (.unread stream i)
(read stream true nil))
;; Read strings
(= c \") (read-json-quoted-string stream)
;; Read null as nil
(= c \n) (let [ull [(char (.read stream))
(char (.read stream))
(char (.read stream))]]
(if (= ull [\u \l \l])
nil
(throw (Exception. (str "JSON error (expected null): " c ull)))))
;; Read true
(= c \t) (let [rue [(char (.read stream))
(char (.read stream))
(char (.read stream))]]
(if (= rue [\r \u \e])
true
(throw (Exception. (str "JSON error (expected true): " c rue)))))
;; Read false
(= c \f) (let [alse [(char (.read stream))
(char (.read stream))
(char (.read stream))
(char (.read stream))]]
(if (= alse [\a \l \s \e])
false
(throw (Exception. (str "JSON error (expected false): " c alse)))))
;; Read JSON objects
(= c \{) (read-json-object stream)
;; Read JSON arrays
(= c \[) (read-json-array stream)
:else (throw (Exception. (str "JSON error (unexpected character): " c))))))))
(defn read-json-string [string]
(read-json (PushbackReader. (StringReader. string))))
;;; TESTS
(deftest- can-read-numbers
(is (= 42 (read-json "42")))
(is (= -3 (read-json "-3")))
(is (= 3.14159 (read-json "3.14159")))
(is (= 6.022e23 (read-json "6.022e23"))))
(deftest- can-read-null
(is (= nil (read-json "null"))))
(deftest- can-read-strings
(is (= "Hello, World!" (read-json "\"Hello, World!\""))))
(deftest- handles-escaped-slashes-in-strings
(is (= "/foo/bar" (read-json "\"\\/foo\\/bar\""))))
(deftest- handles-unicode-escapes
(is (= " \u0beb " (read-json "\" \\u0bEb \""))))
(deftest- handles-escaped-whitespace
(is (= "foo\nbar" (read-json "\"foo\\nbar\"")))
(is (= "foo\rbar" (read-json "\"foo\\rbar\"")))
(is (= "foo\tbar" (read-json "\"foo\\tbar\""))))
(deftest- can-read-booleans
(is (= true (read-json "true")))
(is (= false (read-json "false"))))
(deftest- can-ignore-whitespace
(is (= nil (read-json "\r\n null"))))
(deftest- can-read-arrays
(is (= [1 2 3] (read-json "[1,2,3]")))
(is (= ["Ole" "Lena"] (read-json "[\"Ole\", \r\n \"Lena\"]"))))
(deftest- can-read-objects
(is (= {"a" 1, "b" 2} (read-json "{\"a\": 1, \"b\": 2}"))))
(deftest- can-read-nested-structures
(is (= {"a" [1 2 {"b" [3 "four"]} 5.5]}
(read-json "{\"a\":[1,2,{\"b\":[3,\"four\"]},5.5]}"))))
(deftest- disallows-non-string-keys
(is (thrown? Exception (read-json "{26:\"z\""))))
(deftest- disallows-barewords
(is (thrown? Exception (read-json " foo "))))
(deftest- disallows-unclosed-arrays
(is (thrown? Exception (read-json "[1, 2, "))))
(deftest- disallows-unclosed-objects
(is (thrown? Exception (read-json "{\"a\":1, "))))
(deftest- can-get-keyword-keys
(is (= {:a [1 2 {:b [3 "four"]} 5.5]}
(binding [*json-keyword-keys* true]
(read-json "{\"a\":[1,2,{\"b\":[3,\"four\"]},5.5]}")))))
(declare *pass1-string*)
(deftest- pass1-test
(let [input (read-json *pass1-string*)]
(is (= "JSON Test Pattern pass1" (first input)))
(is (= "array with 1 element" (get-in input [1 "object with 1 member" 0])))
(is (= 1234567890 (get-in input [8 "integer"])))
(is (= "rosebud" (last input)))))
; from http://www.json.org/JSON_checker/test/pass1.json
(def *pass1-string*
"[
\"JSON Test Pattern pass1\",
{\"object with 1 member\":[\"array with 1 element\"]},
{},
[],
-42,
true,
false,
null,
{
\"integer\": 1234567890,
\"real\": -9876.543210,
\"e\": 0.123456789e-12,
\"E\": 1.234567890E+34,
\"\": 23456789012E66,
\"zero\": 0,
\"one\": 1,
\"space\": \" \",
\"quote\": \"\\\"\",
\"backslash\": \"\\\\\",
\"controls\": \"\\b\\f\\n\\r\\t\",
\"slash\": \"/ & \\/\",
\"alpha\": \"abcdefghijklmnopqrstuvwyz\",
\"ALPHA\": \"ABCDEFGHIJKLMNOPQRSTUVWYZ\",
\"digit\": \"0123456789\",
\"0123456789\": \"digit\",
\"special\": \"`1~!@#$%^&*()_+-={':[,]}|;.</>?\",
\"hex\": \"\\u0123\\u4567\\u89AB\\uCDEF\\uabcd\\uef4A\",
\"true\": true,
\"false\": false,
\"null\": null,
\"array\":[ ],
\"object\":{ },
\"address\": \"50 St. James Street\",
\"url\": \"http://www.JSON.org/\",
\"comment\": \"// /* <!-- --\",
\"# -- --> */\": \" \",
\" s p a c e d \" :[1,2 , 3
,
4 , 5 , 6 ,7 ],\"compact\":[1,2,3,4,5,6,7],
\"jsontext\": \"{\\\"object with 1 member\\\":[\\\"array with 1 element\\\"]}\",
\"quotes\": \"&#34; \\u0022 %22 0x22 034 &#x22;\",
\"\\/\\\\\\\"\\uCAFE\\uBABE\\uAB98\\uFCDE\\ubcda\\uef4A\\b\\f\\n\\r\\t`1~!@#$%^&*()_+-=[]{}|;:',./<>?\"
: \"A key can be any string\"
},
0.5 ,98.6
,
99.44
,
1066,
1e1,
0.1e1,
1e-1,
1e00,2e+00,2e-00
,\"rosebud\"]")
Jump to Line
Something went wrong with that request. Please try again.