Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with
or
.
Download ZIP
Newer
Older
100644 338 lines (276 sloc) 11.721 kb
15b478f @stuartsierra Moved print_json.clj to json/write.clj; added json/read.clj.
stuartsierra authored
1 ;;; json/read.clj: JavaScript Object Notation (JSON) parser
2
3 ;; by Stuart Sierra, http://stuartsierra.com/
aec4371 @stuartsierra json/read.clj: added custom string parser to support \/ escapes
stuartsierra authored
4 ;; February 13, 2009
15b478f @stuartsierra Moved print_json.clj to json/write.clj; added json/read.clj.
stuartsierra authored
5
6 ;; Copyright (c) Stuart Sierra, 2009. All rights reserved. The use
7 ;; and distribution terms for this software are covered by the Eclipse
8 ;; Public License 1.0 (http://opensource.org/licenses/eclipse-1.0.php)
9 ;; which can be found in the file epl-v10.html at the root of this
10 ;; distribution. By using this software in any fashion, you are
11 ;; agreeing to be bound by the terms of this license. You must not
12 ;; remove this notice, or any other, from this software.
13
14
aec4371 @stuartsierra json/read.clj: added custom string parser to support \/ escapes
stuartsierra authored
15 ;; Change Log
16 ;;
17 ;; February 13, 2009: added custom handler for quoted strings, to
18 ;; allow escaped forward backslash characters ("\/") in strings.
19 ;;
20 ;; January 26, 2009: initial version
21
15b478f @stuartsierra Moved print_json.clj to json/write.clj; added json/read.clj.
stuartsierra authored
22
23 ;; For more information on JSON, see http://www.json.org/
aec4371 @stuartsierra json/read.clj: added custom string parser to support \/ escapes
stuartsierra authored
24 ;;
25 ;; This library parses data in JSON format. This is a fairly strict
26 ;; implementation of JSON as described at json.org, not a full-fledged
27 ;; JavaScript parser. JavaScript functions and object constructors
28 ;; are not supported. Object field names must be quoted strings; they
29 ;; may not be bare symbols.
15b478f @stuartsierra Moved print_json.clj to json/write.clj; added json/read.clj.
stuartsierra authored
30
31
32
86e9001 @tomfaulhaber Lots 'o doc strings
tomfaulhaber authored
33 (ns
34 #^{:author "Stuart Sierra",
35 :doc "JavaScript Object Notation (JSON) parser
36
37 For more information on JSON, see http://www.json.org/
38
39 This library parses data in JSON format. This is a fairly strict
40 implementation of JSON as described at json.org, not a full-fledged
41 JavaScript parser. JavaScript functions and object constructors
42 are not supported. Object field names must be quoted strings; they
68dae64 @stuartsierra json/read.clj: doc string
stuartsierra authored
43 may not be bare symbols.
44
45 If you want to convert map keys from strings to keywords, use
46 clojure.contrib.walk/keywordize-keys
47 ",
dd497fb @tomfaulhaber Fixed a bunch of little doc errors
tomfaulhaber authored
48 :see-also [["http://www.json.org", "JSON Home Page"]]}
86e9001 @tomfaulhaber Lots 'o doc strings
tomfaulhaber authored
49 clojure.contrib.json.read
15b478f @stuartsierra Moved print_json.clj to json/write.clj; added json/read.clj.
stuartsierra authored
50 (:import (java.io PushbackReader StringReader EOFException))
4f2c7bb @stuarthalloway gtic: move clojure tests and supporting libraries into clojure repos
stuarthalloway authored
51 (:use [clojure.test :only (deftest- is)]))
15b478f @stuartsierra Moved print_json.clj to json/write.clj; added json/read.clj.
stuartsierra authored
52
53 (declare read-json)
54
f79532a @stuartsierra json/read.clj: added *json-keyword-keys* to get keywords in maps
stuartsierra authored
55 (def #^{:doc "If true, JSON object keys will be converted to keywords
e087b14 @stuartsierra jsov/read.clj: doc string
stuartsierra authored
56 instead of strings. Defaults to false. There are no checks that
57 the strings form valid keywords."} *json-keyword-keys* false)
f79532a @stuartsierra json/read.clj: added *json-keyword-keys* to get keywords in maps
stuartsierra authored
58
1707eae @stuartsierra json/read.clj: eliminated reflection, fixed int/char typecast bug
stuartsierra authored
59 (defn- read-json-array [#^PushbackReader stream]
15b478f @stuartsierra Moved print_json.clj to json/write.clj; added json/read.clj.
stuartsierra authored
60 ;; Expects to be called with the head of the stream AFTER the
61 ;; opening bracket.
62 (loop [i (.read stream), result []]
63 (let [c (char i)]
64 (cond
65 (= i -1) (throw (EOFException. "JSON error (end-of-file inside array)"))
66 (Character/isWhitespace c) (recur (.read stream) result)
1707eae @stuartsierra json/read.clj: eliminated reflection, fixed int/char typecast bug
stuartsierra authored
67 (= c \,) (recur (.read stream) result)
15b478f @stuartsierra Moved print_json.clj to json/write.clj; added json/read.clj.
stuartsierra authored
68 (= c \]) result
69 :else (do (.unread stream (int c))
70 (let [element (read-json stream)]
71 (recur (.read stream) (conj result element))))))))
72
1707eae @stuartsierra json/read.clj: eliminated reflection, fixed int/char typecast bug
stuartsierra authored
73 (defn- read-json-object [#^PushbackReader stream]
15b478f @stuartsierra Moved print_json.clj to json/write.clj; added json/read.clj.
stuartsierra authored
74 ;; Expects to be called with the head of the stream AFTER the
75 ;; opening bracket.
76 (loop [i (.read stream), key nil, result {}]
77 (let [c (char i)]
78 (cond
79 (= i -1) (throw (EOFException. "JSON error (end-of-file inside object)"))
80
81 (Character/isWhitespace c) (recur (.read stream) key result)
82
83 (= c \,) (recur (.read stream) nil result)
84
85 (= c \:) (recur (.read stream) key result)
86
87 (= c \}) (if (nil? key)
88 result
89 (throw (Exception. "JSON error (key missing value in object)")))
90
91 :else (do (.unread stream i)
92 (let [element (read-json stream)]
93 (if (nil? key)
94 (if (string? element)
95 (recur (.read stream) element result)
96 (throw (Exception. "JSON error (non-string key in object)")))
f79532a @stuartsierra json/read.clj: added *json-keyword-keys* to get keywords in maps
stuartsierra authored
97 (recur (.read stream) nil
98 (assoc result (if *json-keyword-keys* (keyword key) key)
99 element)))))))))
15b478f @stuartsierra Moved print_json.clj to json/write.clj; added json/read.clj.
stuartsierra authored
100
188e6b0 @stuartsierra json/read.clj: added type hints to avoid reflection
stuartsierra authored
101 (defn- read-json-hex-character [#^PushbackReader stream]
aec4371 @stuartsierra json/read.clj: added custom string parser to support \/ escapes
stuartsierra authored
102 ;; Expects to be called with the head of the stream AFTER the
103 ;; initial "\u". Reads the next four characters from the stream.
104 (let [digits [(.read stream)
105 (.read stream)
106 (.read stream)
107 (.read stream)]]
108 (when (some neg? digits)
109 (throw (EOFException. "JSON error (end-of-file inside Unicode character escape)")))
110 (let [chars (map char digits)]
111 (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}
112 chars)
113 (throw (Exception. "JSON error (invalid hex character in Unicode character escape)")))
114 (char (Integer/parseInt (apply str chars) 16)))))
115
188e6b0 @stuartsierra json/read.clj: added type hints to avoid reflection
stuartsierra authored
116 (defn- read-json-escaped-character [#^PushbackReader stream]
aec4371 @stuartsierra json/read.clj: added custom string parser to support \/ escapes
stuartsierra authored
117 ;; Expects to be called with the head of the stream AFTER the
118 ;; initial backslash.
119 (let [c (char (.read stream))]
120 (cond
121 (#{\" \\ \/} c) c
122 (= c \b) \backspace
123 (= c \f) \formfeed
124 (= c \n) \newline
125 (= c \r) \return
126 (= c \t) \tab
127 (= c \u) (read-json-hex-character stream))))
128
129 (defn- read-json-quoted-string [#^PushbackReader stream]
130 ;; Expects to be called with the head of the stream AFTER the
131 ;; opening quotation mark.
132 (let [buffer (StringBuilder.)]
133 (loop [i (.read stream)]
134 (let [c (char i)]
135 (cond
136 (= i -1) (throw (EOFException. "JSON error (end-of-file inside string)"))
137 (= c \") (str buffer)
138 (= c \\) (do (.append buffer (read-json-escaped-character stream))
139 (recur (.read stream)))
140 :else (do (.append buffer c)
141 (recur (.read stream))))))))
142
15b478f @stuartsierra Moved print_json.clj to json/write.clj; added json/read.clj.
stuartsierra authored
143 (defn read-json
585ef55 @stuartsierra json/read.clj: read-json now takes a string or a reader
stuartsierra authored
144 "Read one JSON record from s, which may be a String or a
15b478f @stuartsierra Moved print_json.clj to json/write.clj; added json/read.clj.
stuartsierra authored
145 java.io.PushbackReader."
146 ([] (read-json *in* true nil))
585ef55 @stuartsierra json/read.clj: read-json now takes a string or a reader
stuartsierra authored
147 ([s] (if (string? s)
148 (read-json (PushbackReader. (StringReader. s)) true nil)
149 (read-json s true nil)))
1707eae @stuartsierra json/read.clj: eliminated reflection, fixed int/char typecast bug
stuartsierra authored
150 ([#^PushbackReader stream eof-error? eof-value]
15b478f @stuartsierra Moved print_json.clj to json/write.clj; added json/read.clj.
stuartsierra authored
151 (loop [i (.read stream)]
152 (let [c (char i)]
153 (cond
154 ;; Handle end-of-stream
155 (= i -1) (if eof-error?
156 (throw (EOFException. "JSON error (end-of-file)"))
157 eof-value)
158
159 ;; Ignore whitespace
160 (Character/isWhitespace c) (recur (.read stream))
161
aec4371 @stuartsierra json/read.clj: added custom string parser to support \/ escapes
stuartsierra authored
162 ;; Read numbers, true, and false with Clojure reader
163 (#{\- \0 \1 \2 \3 \4 \5 \6 \7 \8 \9} c)
15b478f @stuartsierra Moved print_json.clj to json/write.clj; added json/read.clj.
stuartsierra authored
164 (do (.unread stream i)
165 (read stream true nil))
166
aec4371 @stuartsierra json/read.clj: added custom string parser to support \/ escapes
stuartsierra authored
167 ;; Read strings
168 (= c \") (read-json-quoted-string stream)
169
15b478f @stuartsierra Moved print_json.clj to json/write.clj; added json/read.clj.
stuartsierra authored
170 ;; Read null as nil
171 (= c \n) (let [ull [(char (.read stream))
172 (char (.read stream))
173 (char (.read stream))]]
174 (if (= ull [\u \l \l])
175 nil
176 (throw (Exception. (str "JSON error (expected null): " c ull)))))
177
178 ;; Read true
179 (= c \t) (let [rue [(char (.read stream))
180 (char (.read stream))
181 (char (.read stream))]]
182 (if (= rue [\r \u \e])
183 true
184 (throw (Exception. (str "JSON error (expected true): " c rue)))))
185
186 ;; Read false
187 (= c \f) (let [alse [(char (.read stream))
188 (char (.read stream))
189 (char (.read stream))
190 (char (.read stream))]]
191 (if (= alse [\a \l \s \e])
192 false
193 (throw (Exception. (str "JSON error (expected false): " c alse)))))
194
195
196
197 ;; Read JSON objects
198 (= c \{) (read-json-object stream)
199
200 ;; Read JSON arrays
201 (= c \[) (read-json-array stream)
202
203 :else (throw (Exception. (str "JSON error (unexpected character): " c))))))))
204
205
206 (defn read-json-string [string]
207 (read-json (PushbackReader. (StringReader. string))))
208
209
210 ;;; TESTS
211
7d64565 @stuartsierra json/write.clj & json/read.clj: made tests namespace-private
stuartsierra authored
212 (deftest- can-read-numbers
585ef55 @stuartsierra json/read.clj: read-json now takes a string or a reader
stuartsierra authored
213 (is (= 42 (read-json "42")))
214 (is (= -3 (read-json "-3")))
215 (is (= 3.14159 (read-json "3.14159")))
216 (is (= 6.022e23 (read-json "6.022e23"))))
15b478f @stuartsierra Moved print_json.clj to json/write.clj; added json/read.clj.
stuartsierra authored
217
7d64565 @stuartsierra json/write.clj & json/read.clj: made tests namespace-private
stuartsierra authored
218 (deftest- can-read-null
585ef55 @stuartsierra json/read.clj: read-json now takes a string or a reader
stuartsierra authored
219 (is (= nil (read-json "null"))))
15b478f @stuartsierra Moved print_json.clj to json/write.clj; added json/read.clj.
stuartsierra authored
220
7d64565 @stuartsierra json/write.clj & json/read.clj: made tests namespace-private
stuartsierra authored
221 (deftest- can-read-strings
585ef55 @stuartsierra json/read.clj: read-json now takes a string or a reader
stuartsierra authored
222 (is (= "Hello, World!" (read-json "\"Hello, World!\""))))
15b478f @stuartsierra Moved print_json.clj to json/write.clj; added json/read.clj.
stuartsierra authored
223
aec4371 @stuartsierra json/read.clj: added custom string parser to support \/ escapes
stuartsierra authored
224 (deftest- handles-escaped-slashes-in-strings
585ef55 @stuartsierra json/read.clj: read-json now takes a string or a reader
stuartsierra authored
225 (is (= "/foo/bar" (read-json "\"\\/foo\\/bar\""))))
aec4371 @stuartsierra json/read.clj: added custom string parser to support \/ escapes
stuartsierra authored
226
227 (deftest- handles-unicode-escapes
585ef55 @stuartsierra json/read.clj: read-json now takes a string or a reader
stuartsierra authored
228 (is (= " \u0beb " (read-json "\" \\u0bEb \""))))
aec4371 @stuartsierra json/read.clj: added custom string parser to support \/ escapes
stuartsierra authored
229
230 (deftest- handles-escaped-whitespace
585ef55 @stuartsierra json/read.clj: read-json now takes a string or a reader
stuartsierra authored
231 (is (= "foo\nbar" (read-json "\"foo\\nbar\"")))
232 (is (= "foo\rbar" (read-json "\"foo\\rbar\"")))
233 (is (= "foo\tbar" (read-json "\"foo\\tbar\""))))
aec4371 @stuartsierra json/read.clj: added custom string parser to support \/ escapes
stuartsierra authored
234
7d64565 @stuartsierra json/write.clj & json/read.clj: made tests namespace-private
stuartsierra authored
235 (deftest- can-read-booleans
585ef55 @stuartsierra json/read.clj: read-json now takes a string or a reader
stuartsierra authored
236 (is (= true (read-json "true")))
237 (is (= false (read-json "false"))))
15b478f @stuartsierra Moved print_json.clj to json/write.clj; added json/read.clj.
stuartsierra authored
238
7d64565 @stuartsierra json/write.clj & json/read.clj: made tests namespace-private
stuartsierra authored
239 (deftest- can-ignore-whitespace
585ef55 @stuartsierra json/read.clj: read-json now takes a string or a reader
stuartsierra authored
240 (is (= nil (read-json "\r\n null"))))
15b478f @stuartsierra Moved print_json.clj to json/write.clj; added json/read.clj.
stuartsierra authored
241
7d64565 @stuartsierra json/write.clj & json/read.clj: made tests namespace-private
stuartsierra authored
242 (deftest- can-read-arrays
585ef55 @stuartsierra json/read.clj: read-json now takes a string or a reader
stuartsierra authored
243 (is (= [1 2 3] (read-json "[1,2,3]")))
244 (is (= ["Ole" "Lena"] (read-json "[\"Ole\", \r\n \"Lena\"]"))))
15b478f @stuartsierra Moved print_json.clj to json/write.clj; added json/read.clj.
stuartsierra authored
245
7d64565 @stuartsierra json/write.clj & json/read.clj: made tests namespace-private
stuartsierra authored
246 (deftest- can-read-objects
585ef55 @stuartsierra json/read.clj: read-json now takes a string or a reader
stuartsierra authored
247 (is (= {"a" 1, "b" 2} (read-json "{\"a\": 1, \"b\": 2}"))))
15b478f @stuartsierra Moved print_json.clj to json/write.clj; added json/read.clj.
stuartsierra authored
248
7d64565 @stuartsierra json/write.clj & json/read.clj: made tests namespace-private
stuartsierra authored
249 (deftest- can-read-nested-structures
15b478f @stuartsierra Moved print_json.clj to json/write.clj; added json/read.clj.
stuartsierra authored
250 (is (= {"a" [1 2 {"b" [3 "four"]} 5.5]}
585ef55 @stuartsierra json/read.clj: read-json now takes a string or a reader
stuartsierra authored
251 (read-json "{\"a\":[1,2,{\"b\":[3,\"four\"]},5.5]}"))))
15b478f @stuartsierra Moved print_json.clj to json/write.clj; added json/read.clj.
stuartsierra authored
252
7d64565 @stuartsierra json/write.clj & json/read.clj: made tests namespace-private
stuartsierra authored
253 (deftest- disallows-non-string-keys
585ef55 @stuartsierra json/read.clj: read-json now takes a string or a reader
stuartsierra authored
254 (is (thrown? Exception (read-json "{26:\"z\""))))
15b478f @stuartsierra Moved print_json.clj to json/write.clj; added json/read.clj.
stuartsierra authored
255
7d64565 @stuartsierra json/write.clj & json/read.clj: made tests namespace-private
stuartsierra authored
256 (deftest- disallows-barewords
585ef55 @stuartsierra json/read.clj: read-json now takes a string or a reader
stuartsierra authored
257 (is (thrown? Exception (read-json " foo "))))
15b478f @stuartsierra Moved print_json.clj to json/write.clj; added json/read.clj.
stuartsierra authored
258
7d64565 @stuartsierra json/write.clj & json/read.clj: made tests namespace-private
stuartsierra authored
259 (deftest- disallows-unclosed-arrays
585ef55 @stuartsierra json/read.clj: read-json now takes a string or a reader
stuartsierra authored
260 (is (thrown? Exception (read-json "[1, 2, "))))
15b478f @stuartsierra Moved print_json.clj to json/write.clj; added json/read.clj.
stuartsierra authored
261
7d64565 @stuartsierra json/write.clj & json/read.clj: made tests namespace-private
stuartsierra authored
262 (deftest- disallows-unclosed-objects
585ef55 @stuartsierra json/read.clj: read-json now takes a string or a reader
stuartsierra authored
263 (is (thrown? Exception (read-json "{\"a\":1, "))))
aec4371 @stuartsierra json/read.clj: added custom string parser to support \/ escapes
stuartsierra authored
264
f79532a @stuartsierra json/read.clj: added *json-keyword-keys* to get keywords in maps
stuartsierra authored
265 (deftest- can-get-keyword-keys
266 (is (= {:a [1 2 {:b [3 "four"]} 5.5]}
267 (binding [*json-keyword-keys* true]
585ef55 @stuartsierra json/read.clj: read-json now takes a string or a reader
stuartsierra authored
268 (read-json "{\"a\":[1,2,{\"b\":[3,\"four\"]},5.5]}")))))
f79532a @stuartsierra json/read.clj: added *json-keyword-keys* to get keywords in maps
stuartsierra authored
269
2d8ddaa @stuartsierra json/read.clj: added tests from pass1.json
stuartsierra authored
270 (declare *pass1-string*)
271
272 (deftest- pass1-test
585ef55 @stuartsierra json/read.clj: read-json now takes a string or a reader
stuartsierra authored
273 (let [input (read-json *pass1-string*)]
2d8ddaa @stuartsierra json/read.clj: added tests from pass1.json
stuartsierra authored
274 (is (= "JSON Test Pattern pass1" (first input)))
275 (is (= "array with 1 element" (get-in input [1 "object with 1 member" 0])))
276 (is (= 1234567890 (get-in input [8 "integer"])))
277 (is (= "rosebud" (last input)))))
278
279 ; from http://www.json.org/JSON_checker/test/pass1.json
280 (def *pass1-string*
281 "[
282 \"JSON Test Pattern pass1\",
283 {\"object with 1 member\":[\"array with 1 element\"]},
284 {},
285 [],
286 -42,
287 true,
288 false,
289 null,
290 {
291 \"integer\": 1234567890,
292 \"real\": -9876.543210,
293 \"e\": 0.123456789e-12,
294 \"E\": 1.234567890E+34,
295 \"\": 23456789012E66,
296 \"zero\": 0,
297 \"one\": 1,
298 \"space\": \" \",
299 \"quote\": \"\\\"\",
300 \"backslash\": \"\\\\\",
301 \"controls\": \"\\b\\f\\n\\r\\t\",
302 \"slash\": \"/ & \\/\",
303 \"alpha\": \"abcdefghijklmnopqrstuvwyz\",
304 \"ALPHA\": \"ABCDEFGHIJKLMNOPQRSTUVWYZ\",
305 \"digit\": \"0123456789\",
306 \"0123456789\": \"digit\",
307 \"special\": \"`1~!@#$%^&*()_+-={':[,]}|;.</>?\",
308 \"hex\": \"\\u0123\\u4567\\u89AB\\uCDEF\\uabcd\\uef4A\",
309 \"true\": true,
310 \"false\": false,
311 \"null\": null,
312 \"array\":[ ],
313 \"object\":{ },
314 \"address\": \"50 St. James Street\",
315 \"url\": \"http://www.JSON.org/\",
316 \"comment\": \"// /* <!-- --\",
317 \"# -- --> */\": \" \",
318 \" s p a c e d \" :[1,2 , 3
319
320 ,
321
322 4 , 5 , 6 ,7 ],\"compact\":[1,2,3,4,5,6,7],
323 \"jsontext\": \"{\\\"object with 1 member\\\":[\\\"array with 1 element\\\"]}\",
324 \"quotes\": \"&#34; \\u0022 %22 0x22 034 &#x22;\",
325 \"\\/\\\\\\\"\\uCAFE\\uBABE\\uAB98\\uFCDE\\ubcda\\uef4A\\b\\f\\n\\r\\t`1~!@#$%^&*()_+-=[]{}|;:',./<>?\"
326 : \"A key can be any string\"
327 },
328 0.5 ,98.6
329 ,
330 99.44
331 ,
332
333 1066,
334 1e1,
335 0.1e1,
336 1e-1,
337 1e00,2e+00,2e-00
338 ,\"rosebud\"]")
Something went wrong with that request. Please try again.