├── .github └── workflows │ └── main.yml ├── .gitignore ├── CHANGELOG.md ├── EXPLAIN.md ├── EXPLAIN_v3.md ├── LICENSE ├── Makefile ├── README.md ├── build.sh ├── json.l ├── module.l ├── test.json ├── test.l ├── test ├── test_json.l └── test_regressions.l ├── test2.json ├── test3.json ├── test4.json ├── test5.json ├── test6.json └── test7.json /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: [push] 4 | 5 | jobs: 6 | build: 7 | 8 | runs-on: ubuntu-latest 9 | 10 | timeout-minutes: 5 11 | 12 | strategy: 13 | matrix: 14 | arch: ['src', 'src64'] 15 | version: ['17.12', '18.6', '18.12', '19.6', '19.12', '20.6', 'latest', 'pil21'] 16 | 17 | steps: 18 | - uses: actions/checkout@v1 19 | - uses: aw/picolisp-action@v2 20 | with: 21 | version: ${{matrix.version}} 22 | architecture: ${{matrix.arch}} 23 | 24 | - name: Run the tests on PicoLisp ${{matrix.arch}} v${{matrix.version}} 25 | run: make check 26 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .lib/ 2 | .modules/ 3 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## 4.1.0 (2019-10-19) 4 | 5 | * Fix issue #19 - Unable to parse empty arrays with quotes 6 | * Add regression tests for this issue 7 | 8 | ## 4.0.0 (2019-04-19) 9 | 10 | * Fix typo in EXPLAIN document 11 | * Stop testing on broken Travis-CI environment 12 | * **Breaking changes** 13 | * Remove all support for PicoLisp namespaces 14 | * Prefix all internal function names with _json-_ 15 | * Adjust tests to use new function names 16 | * Remove tests for namespace issue #9 17 | * Update "picolisp-unit" testing dependency to `v3.0.0` 18 | * Update supported PicoLisp version to 18.12 19 | 20 | ## 3.10.0 (2018-06-29) 21 | 22 | * Optionally prevent duplicate keys from being accepted with '*Json_prevent_duplicate_keys' 23 | * Remove optional 'Make' parameter for '(link-array)' 24 | 25 | ## 3.9.0 (2018-06-19) 26 | 27 | * Store JSON parsing error message in *Msg 28 | 29 | ## 3.8.0 (2018-03-29) 30 | 31 | * Fix issue #15 - Invalid parsing of certain characters with \uNNNN 32 | * Add regression tests for this issue 33 | 34 | ## 3.7.0 (2018-03-21) 35 | 36 | * Fix issue #13 - Invalid encoding of control characters 0x01-0x1F 37 | * Add regression tests for this issue 38 | 39 | ## 3.6.0 (2018-03-21) 40 | 41 | * Fix issue #12 - Invalid encoding of quote and solidus (\\ and \") characters 42 | * Add regression tests for this issue 43 | * Fix existing regression tests which provided incorrect results 44 | 45 | ## 3.5.0 (2018-03-20) 46 | 47 | * Fix issue #11 - Invalid encoding of special control characters (^J, ^M, ^I) 48 | * Ensure ^H (\b) and ^L (\f) are also encoded correctly 49 | * Add regression tests for this issue 50 | 51 | ## 3.4.0 (2018-03-19) 52 | 53 | * Fix issue #10 - Invalid parsing of strings with caret (^) 54 | * Add regression tests for this issue 55 | 56 | ## 3.3.0 (2018-02-15) 57 | 58 | * Fix issue #9 - Bug in namespaces functionality 59 | * Add regression tests for this issue 60 | * Add notice about namespaces in PicoLisp >= 17.3.4 61 | * Don't load module.l in json.l 62 | 63 | ## 3.2.0 (2018-01-10) 64 | 65 | * Fix issue #8 - Invalid encoding of true,false,null 66 | * Add regression tests for this issue 67 | * Refactor encoder 68 | 69 | ## 3.1.0 (2018-01-10) 70 | 71 | * Fix issue #6 - Invalid parsing of empty arrays and empty objects 72 | * Add regression test, and tests for additional JSON structures 73 | * Refactor, simplify, and optimize decoder 74 | 75 | ## 3.0.0 (2018-01-08) 76 | 77 | * Re-implement JSON decoding in pure PicoLisp 78 | * Remove ffi-bindings (C parson) library 79 | * [breaking] Errors are sent to STDERR instead of being suppressed 80 | 81 | ## 2.2.0 (2017-03-23) 82 | 83 | * Restore PicoLisp namespaces for backwards compatibility. Disable with PIL_NAMESPACES=false 84 | 85 | ## 2.1.0 (2017-03-14) 86 | 87 | * Update 'parson' dependency version 88 | 89 | ## 2.0.0 (2017-03-09) 90 | 91 | * Remove the use of PicoLisp namespaces (functionally equivalent to 1.1.0) 92 | * Update 'parson' dependency version 93 | 94 | ## 1.1.0 (2015-07-08) 95 | 96 | * Update install paths in Makefile 97 | * Update 'parson' dependency version 98 | 99 | ## 1.0.0 (2015-06-09) 100 | 101 | * Production release 102 | 103 | ## 0.6.3 (2015-05-26) 104 | 105 | * Update picolisp-unit to v0.6.2 106 | 107 | ## 0.6.2 (2015-05-08) 108 | 109 | * Specify explicit git ref for 'parson' library 110 | 111 | ## 0.6.1 (2015-04-28) 112 | 113 | * Fix bug in make-json-string. 114 | 115 | ## 0.6.0 (2015-04-28) 116 | 117 | * Remove the need for git submodules 118 | * Add Makefile for fetching and building dependencies 119 | * Change default path for dependencies and shared module (.modules and .lib) 120 | * Adjust README.md, tests and travis-ci unit testing config 121 | 122 | ## 0.5.2 (2015-04-10) 123 | 124 | * Run travis-ci tests in docker container 125 | * Update picolisp-unit to v0.6.1 126 | 127 | ## 0.5.1 (2015-04-08) 128 | 129 | * Ensure module.l requires the correct versions 130 | 131 | ## 0.5.0 (2015-04-05) 132 | 133 | * Rename some internal functions 134 | 135 | ## 0.4.2 (2015-04-05) 136 | 137 | * Update picolisp-unit to v0.6.0 138 | 139 | ## 0.4.1 (2015-04-05) 140 | 141 | * Refactor 'json-boolean call 142 | 143 | ## 0.4.0 (2015-04-04) 144 | 145 | * Add requires to module.l 146 | * Update README.md and EXPLAIN.md 147 | * Replace ffi-bindings with a table, to be more lispy 148 | * Rename some internal functions 149 | 150 | ## 0.3.1 (2015-03-30) 151 | 152 | * Update picolisp-unit to v0.5.2 153 | 154 | ## 0.3.0 (2015-03-24) 155 | 156 | * Prevent leaky globals 157 | * Update picolisp-unit to v0.4.0 158 | * Update EXPLAIN.md and README.md 159 | * Improve travis-ci build and test times 160 | 161 | ## 0.2.7 (2015-03-19) 162 | 163 | * Add license information to json.l 164 | 165 | ## 0.2.6 (2015-03-19) 166 | 167 | * Move MODULE_INFO into module.l 168 | 169 | ## 0.2.5 (2015-03-19) 170 | 171 | * Add unit tests using picolisp-unit 172 | * Stylistic changes to MODULE_INFO 173 | * Add update.sh 174 | * Add note about Updating and Testing 175 | * Add .travis.yml for automated testing 176 | -------------------------------------------------------------------------------- /EXPLAIN.md: -------------------------------------------------------------------------------- 1 | # Explanation: JSON Encoder/Decoder for PicoLisp 2 | 3 | This document provides a short walkthrough of the source code for the [PicoLisp-JSON](https://github.com/aw/picolisp-json.git) encoder/decoder. 4 | 5 | **Note:** This document covers the older `v2` of the JSON library. To view the newer (pure PicoLisp) version [click here](https://github.com/aw/picolisp-json/blob/master/EXPLAIN.md). 6 | 7 | It's split into a few sections for easier reading: 8 | 9 | 1. [Global variables](#global-variables): Important variables used throughout the library. 10 | 2. [Native calls (ffi-bindings)](#native-calls-ffi-bindings): The `Parson` native C library, and how it's used. 11 | * [A rule-based solution](#a-rule--based-solution) 12 | 3. [Internal functions](#internal-functions): Recursion and datatype-checking. 13 | * [decoding JSON](#decoding-json) 14 | * [encoding JSON](#encoding-json) 15 | 16 | Make sure you read the [README](README.md) to get an idea of what this library does. 17 | 18 | Also, I recommend you read my [Nanomsg Explanation](https://github.com/aw/picolisp-nanomsg/blob/master/EXPLAIN.md) for additional PicoLisp tips and ideas. 19 | 20 | # Global variables 21 | 22 | PicoLisp does not prevent variables from leaking into the global namespace. In order to prevent that, you must use [local](http://software-lab.de/doc/refL.html#local) and define exactly what should _not_ affect the global namespace. This is important to avoid un-intended side-effects. 23 | 24 | ```lisp 25 | (local MODULE_INFO *Json *JSONError *JSONNull) 26 | ``` 27 | 28 | This will ensure the variables will not affect anything outside their current scope (namespace). It's similar to `var Myvar;` in JavaScript. 29 | 30 | A few global variables have been defined at the top of the file. 31 | 32 | ```lisp 33 | (setq 34 | *Json (pack (car (file)) "lib/libparson.so") 35 | *JSONError -1 36 | *JSONNull 1 37 | *JSONString 2 38 | *JSONNumber 3 39 | *JSONObject 4 40 | *JSONArray 5 41 | *JSONBoolean 6 42 | *JSONSuccess 0 43 | *JSONFailure -1 ) 44 | ``` 45 | 46 | You'll notice I'm following the [PicoLisp Naming Conventions](http://software-lab.de/doc/ref.html#conv) this time. 47 | 48 | The variables prefixed with `*JSON` were copied directly from [Parson's source code](https://github.com/kgabis/parson/blob/81c2fd0186cafb43c6b4c190b50bb3a4fef1827e/parson.h#L39-L54): 49 | 50 | ```C 51 | .. 52 | enum json_value_type { 53 | JSONError = -1, 54 | JSONNull = 1, 55 | JSONString = 2, 56 | .. 57 | ``` 58 | 59 | When working with a native C library in PicoLisp, it's important to use the same (or very similar) symbol names to avoid confusion. 60 | 61 | # Native calls (ffi-bindings) 62 | 63 | [Parson](https://github.com/kgabis/parson/) is a very simple C library, with functions accepting zero to three arguments, and returning simple validated values and structures. 64 | 65 | Example: 66 | 67 | ```lisp 68 | (ffi 'json-type (ffi 'json-parse-string "{\"Hello\":\"World\"}")) 69 | -> 4 70 | ``` 71 | 72 | This returns `4` which is `*JSONObject` based on our variables defined earlier. 73 | 74 | As we'll see later, our `picolisp-json` library can make decisions on how to parse data based on these types of results. 75 | 76 | ### A rule-based solution 77 | 78 | Inspired by the amazing ["Paradigms of Artificial Intelligence"](http://norvig.com/paip/README.html) book (Chapter 2), I wrote the ffi functions using a rule-based approach. 79 | 80 | First we create a simple list which declares all the ffi functions and their result type: 81 | 82 | ```lisp 83 | [de ffi-table 84 | (json-parse-file 'N) 85 | (json-parse-string 'N) 86 | (json-value-init-object 'N) 87 | (json-type 'I) 88 | (json-array 'N) ] 89 | # snipped for brevity 90 | ``` 91 | 92 | We then use a function which maps the first argument `Function` to a name in the list: 93 | 94 | ```lisp 95 | [de ffi (Function . @) 96 | (let Rule (assoc Function ffi-table) 97 | (pass native `*Json (chop-ffi (car Rule)) (eval (cadr Rule) ] 98 | ``` 99 | 100 | The `(ffi)` function calls `(native)` using [pass](http://software-lab.de/doc/refP.html#pass), to append the rest of the variable-length arguments in `@` at the end of the function. 101 | 102 | You'll notice `(chop-ffi)` actually converts the `-` characters to `_`. This is necessary because the C function names have underscores instead of dashes, but in general I think the LISP world prefers dashes for function names. 103 | 104 | ```lisp 105 | [de chop-ffi (Name) 106 | (glue "_" (split (chop Name) "-") ] 107 | ``` 108 | 109 | I think this is a very lispy approach. It allows us to easily add new native functions by simply adding to the `ffi-table`. No other code modifications are necessary. 110 | 111 | # Internal functions 112 | 113 | The meat of this library is in the internal functions. The `'json-parse-string` and `'json-parse-file` functions validate the JSON string. If those calls are successful, then we can safely iterate over the result and generate our own list. 114 | 115 | ## decoding JSON 116 | 117 | We'll begin by looking at how JSON is decoded in this library. 118 | 119 | ### (iterate-object) 120 | 121 | We'll first look at the `(iterate-object)` function. This is a recursive function which loops and iterates through the results of each native C call, and quickly builds a sexy PicoLisp list. 122 | 123 | ```lisp 124 | [de iterate-object (Value) 125 | (make 126 | (let Type (ffi 'json-type Value) 127 | (case Type (`*JSONArray (link-array Value)) 128 | (`*JSONObject (link-object Value)) 129 | (`*JSONString (chain (ffi 'json-string Value))) 130 | [`*JSONBoolean (chain (case (ffi 'json-boolean Value) (1 'true) (0 'false) ] 131 | (`*JSONNumber (chain (ffi 'json-number Value))) 132 | (`*JSONNull (chain 'null)) ] 133 | ``` 134 | 135 | Lots of meat there. 136 | 137 | ### (make) 138 | 139 | We've seen [make](http://software-lab.de/doc/refM.html#make) before, but I didn't fully explain it. 140 | 141 | The `(make)` function is the instigator for building a list. You put it at the top or start of your function, and watch it build lists using [link](http://software-lab.de/doc/refL.html#link) and [chain](http://software-lab.de/doc/refC.html#chain). 142 | 143 | We use [case](http://software-lab.de/doc/refC.html#case) here as our switch statement. This concept is similar in other programming language. This `(case)` call compares the `Type` value with those defined as global variables. If a match is found, it runs the following expression. Otherwise it returns `NIL` (aka: stop looping, i'm done damnit!). 144 | 145 | JSON Arrays and Objects are a bit more tricky to parse, so we'll get to those later. In the case of `String, Boolean, Number or Null`, we add them to the list using `(chain)`. 146 | 147 | ### (link-array) 148 | 149 | When the value is an Array (`Type = 5 = *JSONArray`), we loop through it to build a list (arrays are mapped as lists). 150 | 151 | ```lisp 152 | [de link-array (Value) 153 | (let Arr (ffi 'json-array Value) 154 | (link T) 155 | (for N (ffi 'json-array-get-count Arr) 156 | (let Val (ffi 'json-array-get-value Arr (dec N)) 157 | (link (iterate-object Val)) ] 158 | ``` 159 | 160 | You'll notice we added `(link T)` before the [for loop](http://software-lab.de/doc/refF.html#f). After long discussions with Alexander Burger, it was made clear that a marker is required to differentiate Objects from Arrays (in PicoLisp). We do that by appending `T` as the first element in the list. 161 | 162 | The `(for)` loop is rather simple, but in each case we're obtaining new values by performing native C calls, and then adding to the list using `(link)`. 163 | 164 | If you've had your coffee today, you would notice the [dec](http://software-lab.de/doc/refD.html#dec) call. As it turns out, `(for)` starts with 1 and counts to the total number of items in the Array. We use `(dec N)` to start at 0. 165 | 166 | Example: 167 | 168 | ``` 169 | for N 5 170 | N = 1 171 | (ffi 'json-array-get-value Arr 0) 172 | .. 173 | N = 2 174 | (ffi 'json-array-get-value Arr 1) 175 | .. 176 | ``` 177 | 178 | Finally, the `(link)` function makes a call to `(iterate-object)`. Remember earlier? when `(link-array)` was called within `(iterate-object)`? 179 | 180 | **Note:** This is called recursion, where a function calls itself (in our case, with a different value). You can [ask Google](https://encrypted.google.com/search?hl=en&q=recursion) about it. 181 | 182 | The reason we perform this recursion is in case the value in the array is itself an array or an object. The `(iterate-object)` function will simply return a string, boolean, number or null otherwise. 183 | 184 | ### (link-object) 185 | 186 | The `(link-object)` is similar to `(link-array)` except, you guessed it, it loops over objects. 187 | 188 | ```lisp 189 | .. 190 | (link (cons Name (iterate-object Val))) 191 | .. 192 | ``` 193 | 194 | The other difference is during the `(link)` call, it appends a [cons](http://software-lab.de/doc/refC.html#cons) pair instead of a single value. We do this because a JSON Object is represented as a `(cons)` pair in PicoLisp. 195 | 196 | ```lisp 197 | {"hello":"world"} <-> '(("hello" . "world")) 198 | ``` 199 | 200 | Of course, this function also recursively calls `(iterate-object)`. 201 | 202 | ## encoding JSON 203 | 204 | Decoding was fun, because `Parson` did most of the work for us. Encoding is ugly, so I tried to make it as simple and intuitive as possible (less chance for bugs). 205 | 206 | ### (iterate-list) 207 | 208 | Since we now have a friendly JSON string represented as a PicoLisp list, we'll iterate over it and turn it back into a JSON string. 209 | 210 | ```lisp 211 | [de iterate-list (Item) 212 | (let Value (cdr Item) 213 | (or 214 | (make-null Value) 215 | (make-boolean Value) 216 | (make-json-number Value) 217 | (make-json-string Value) 218 | (make-json-array Value) 219 | (make-object Value) ] 220 | ``` 221 | 222 | This is a bit sneaky, but I :heart: it. I'm not sure how efficient it is either, but it works well, and I'd rather have _slow, but valid data_ than _fast, but invalid data_ 223 | 224 | > It's not slow, in fact it's incredibly fast based on my opinion of what fast looks like. 225 | 226 | This function uses [or](http://software-lab.de/doc/refO.html#or) as a conditional statement. The `Value` passes through each function to determine the type of value it is, as well as to convert it to a string, number, boolean, null, or whatever. 227 | 228 | ### (make-null) 229 | 230 | This function does nothing special, but I wanted to show something interesting. 231 | 232 | ```lisp 233 | [de make-null (Value) 234 | (when (== 'null Value) "null") ] 235 | ``` 236 | 237 | You'll notice we check if `Value` is `==` to `'null`. What's going on here? Using double equal signs checks for [**Pointer equality**](http://software-lab.de/doc/ref.html#cmp). This is really important, make sure you understand the difference for a happy PicoLisp life. 238 | 239 | This checks if the things we're comparing are not just equal, but also _identical_. In other words: Is `null` the exact same thing as `null` (`Value`). Not `"null"` or `NULL` or any other variation, but `null`. Yes. Got it? 240 | 241 | ### (make-json-array) 242 | 243 | You should remember earlier we discussed appending `T` as the first element in the list, in the case of an Array. 244 | 245 | ```lisp 246 | [de make-json-array (Value) 247 | (when (=T (car Value)) (make-array (cdr Value))) ] 248 | ``` 249 | 250 | What we're doing here is checking if the [car](http://software-lab.de/doc/refC.html#car) of the `Value` is `T`. If yes, then call the `(make-array)` function. 251 | 252 | ### (make-array) 253 | 254 | This function builds an Array suitable for JSON. 255 | 256 | ```lisp 257 | [de make-array (Value) 258 | (pack "[" 259 | (glue "," 260 | (mapcar 261 | '((N) (iterate-list (cons NIL N))) 262 | Value ) ) 263 | "]" ] 264 | ``` 265 | 266 | We've seen what [pack](http://software-lab.de/doc/refP.html#pack) does. We use it to build our Array with opening and closing `[]` brackets. 267 | 268 | The cool thing I discovered recently is [glue](http://software-lab.de/doc/refG.html#glue). It is similar to `Array.join()` in Ruby and JavaScript, by concatenating a list with the supplied argument. In our case, it's a comma `,`. 269 | 270 | Here we're doing something a little different. 271 | 272 | If you remember `(mapcar)`, you'll know the first argument is a function, but in this code we have this: 273 | 274 | ```lisp 275 | '((N) (iterate-list (cons NIL N))) 276 | ``` 277 | 278 | Above is an **anonymous function**. If you're familiar with Ruby, it looks something like this: 279 | 280 | ```ruby 281 | ->(N) { iterate-list [nil, N] } 282 | ``` 283 | 284 | In the case of PicoLisp, our function that we defined on the fly will be applied to the `Value`, but will first make a recursive call to `(iterate-list)` with a `(cons)` pair as its argument. 285 | 286 | ### (make-object) 287 | 288 | This function is almost identical to `(make-array)`, except it generates a JSON Object using opening and closing `{}` braces, of course iterating recursively with `(iterate-list)`. 289 | 290 | # The end 291 | 292 | That's pretty much all I have to explain about the JSON encoder/decoder FFI binding. I'm very open to providing more details about functionality I've skipped, so just file an [issue](https://github.com/aw/picolisp-json/issues/new) and I'll do my best. 293 | 294 | # License 295 | 296 | This work is licensed under a [Creative Commons Attribution-ShareAlike 4.0 International License](http://creativecommons.org/licenses/by-sa/4.0/). 297 | 298 | Copyright (c) 2015 Alexander Williams, Unscramble 299 | -------------------------------------------------------------------------------- /EXPLAIN_v3.md: -------------------------------------------------------------------------------- 1 | # Explanation: JSON Encoder/Decoder in pure PicoLisp 2 | 3 | This document provides a short walkthrough of the source code for the [PicoLisp-JSON](https://github.com/aw/picolisp-json.git) encoder/decoder. 4 | 5 | **Note:** This document covers `v3` of the JSON library. To view the older (C/ffi bindings) version [click here](https://github.com/aw/picolisp-json/blob/v2.2.0/EXPLAIN.md). 6 | 7 | It's split into a few sections for easier reading: 8 | 9 | 1. [Global variables](#global-variables): Important variables used throughout the library. 10 | 2. [Pure PicoLisp JSON decoding](#pure-picolisp-json-decoding): Decoding JSON in PicoLisp, without external libraries. 11 | * [Handling Unicode characters](#handling-unicode-characters) 12 | * [Stack-based bracket matching](#stack--based-bracket-matching) 13 | * [Object and array validation](#object-and-array-validation) 14 | 3. [Internal functions](#internal-functions): Recursion and datatype-checking. 15 | * [decoding JSON](#decoding-json) 16 | * [encoding JSON](#encoding-json) 17 | 18 | Make sure you read the [README](README.md) to get an idea of what this library does. 19 | 20 | Also, I recommend you visit my [PicoLisp Libraries Page](https://picolisp.a1w.ca/) for additional PicoLisp tips and ideas. 21 | 22 | # Global variables 23 | 24 | Prior to `version 17.3.4`, PicoLisp provided the [local](https://software-lab.de/doc/refL.html#local) function to prevent variables from leaking into the global namespace, however it was removed in the `32-bit` version, and its semantics were changed, thus introducing a breaking change for anyone using `(local)` in their code. 25 | 26 | To work around this issue, I modified the library to _disable_ namespaces by specifying the environment variable `PIL_NAMESPACES=false`. 27 | 28 | ```picolisp 29 | (unless (= "false" (sys "PIL_NAMESPACES")) 30 | (symbols 'json 'pico) 31 | 32 | (local MODULE_INFO *Msg err-throw) 33 | ``` 34 | 35 | This change allows the JSON library to be loaded correctly on all 32/64-bit systems using PicoLisp higher than `version 3.1.9` (for backwards compatibility), however if namespaces aren't required, it's probably best to _disable_ namespaces as mentioned above. 36 | 37 | # Pure PicoLisp JSON decoding 38 | 39 | In `v2`, an external [C library](https://github.com/kgabis/parson) was used to perform JSON string decoding. This version gets rid of that dependency and performs all parsing directly in PicoLisp. 40 | 41 | ### Handling Unicode characters 42 | 43 | The JSON spec requires proper handling of Unicode characters written as: `\uNNNN`, where `N` is a hexadecimal digit, as well formfeed `\f` and backspace `\b`, which are not handled by PicoLisp. However it does handle newline `\n -> ^J`, carriage return `\r -> ^M`, tab `\t -> ^I`. 44 | 45 | Similar to the `@lib/json.l` included with PicoLisp, this library calls [str](https://software-lab.de/doc/refS.html#str) to tokenize the JSON string. 46 | 47 | Unforunately, the tokenization removes the single `\` from Unicode characters, turning `\u006C` into `u006c`, rendering it impossible to safely differentiate it from a random string containg the `u006c` character sequence. 48 | 49 | In that case, it's necessary to parse the Unicode characters _before_ tokenizing the string: 50 | 51 | ```picolisp 52 | (str (json-parse-unicode (chop Value)) "_") 53 | ``` 54 | 55 | The `(json-parse-unicode)` function receives a [chop(ped)](https://software-lab.de/doc/refC.html#chop) list of characters representing the full JSON string, and returns a [pack(ed)](https://software-lab.de/doc/refP.html#pack) string with all `\uNNNN` values converted to their UTF-8 symbol: 56 | 57 | ```picolisp 58 | [de json-parse-unicode (Value) 59 | (pack 60 | (make 61 | (while Value 62 | (let R (pop 'Value) 63 | (cond 64 | [(and (= "\\" R) (= "u" (car Value))) (let U (cut 5 'Value) (link (char (hex (pack (tail 4 U) ] # \uNNNN hex 65 | [(and (= "\\" R) (= "b" (car Value))) (pop 'Value) (link (char (hex "08") ] # \b backspace 66 | [(and (= "\\" R) (= "f" (car Value))) (pop 'Value) (link (char (hex "0C") ] # \f formfeed 67 | (T (link R)) ] 68 | ``` 69 | 70 | Let's see what's going on here: 71 | 72 | 1. [make](https://software-lab.de/doc/refM.html#make) is used to initiate a new list 73 | 2. [while](https://software-lab.de/doc/refW.html#while) loops over the list stored in `Value`, until the list is empty 74 | 3. [pop](https://software-lab.de/doc/refP.html#pop) removes the first element from the list stored in `Value` 75 | 4. A conditional check since we're searching for a `\b` (backspace), `\f` (formfeed), or `\uNNNN` (Unicode) character 76 | 5. If the character following `\\` (it's escaped `\`) is `u`, then we pop the next 5 items from the list (i.e: `uNNNN`) using [cut](https://software-lab.de/doc/refC.html#cut) 77 | 6. [link](https://software-lab.de/doc/refL.html#link) is used to add a new list to the list created with `(make)` 78 | 7. Finally, we pack the last 4 items from the previously cut items (i.e: `NNNN`), and use [hex](https://software-lab.de/doc/refH.html#hex) and [char](https://software-lab.de/doc/refC.html#char) to convert `NNNN`. 79 | 80 | For Unicode characters, it ends up like this: `"\\u0065" -> "e"`. Yay! 81 | 82 | ### Stack-based bracket matching 83 | 84 | There's no point in decoding a JSON file that isn't valid, so an early detection method is to determine whether all the curly braces (`{}`) and square brackets (`[]`) are matched. 85 | 86 | We'll use a stack-based algorithm to count brackets, and only consider it a success if the stack is empty at the end. 87 | 88 | First, we provide the tokenized string to the `(json-count-brackets)` function, and map over each character. For each character, we perform the following: 89 | 90 | ```picolisp 91 | (if (or (= "{" N) (= "[" N)) 92 | (push 'Json_stack N) 93 | (case N 94 | ("]" (let R (pop 'Json_stack) (unless (= "[" R) (err-throw "Unmatched JSON brackets '['")))) 95 | ("}" (let R (pop 'Json_stack) (unless (= "{" R) (err-throw "Unmatched JSON brackets '{'")))) ) ) ) 96 | ``` 97 | 98 | 1. If the character is an opening `{` or `[`, [push](https://software-lab.de/doc/refP.html#push) it to the stack 99 | 2. If the character is a closing `}` or `]`, [pop](https://software-lab.de/doc/refP.html#pop) the next value from the stack, and if that character isn't the matching bracket (i.e: `{` for `}`, or `[` for `]`), then we have unmatched JSON brackets. Easy. 100 | 101 | Those who are paying attention will notice the `(err-throw)` function. It does two things: 102 | 103 | ```picolisp 104 | (msg Error) 105 | (throw 'invalid-json NIL) 106 | ``` 107 | 108 | The [msg](https://software-lab.de/doc/refM.html#msg) function will output a message to STDERR, because the [UNIX Philosophy](https://en.wikipedia.org/wiki/Unix_philosophy#Mike_Gancarz:_The_UNIX_Philosophy). 109 | 110 | The [throw](https://software-lab.de/doc/refT.html#throw) function will raise an error in the program, with the `'invalid-json` label and a `NIL` return value. 111 | 112 | The decoder will [catch](https://software-lab.de/doc/refC.html#catch) the raised error, as it should, but more importantly, the `NIL` return value will indicate that decoding failed. This is important for programs which embed this library, as it won't break a running program, and will behave exactly as expected when _something goes wrong_. 113 | 114 | ### Object and array validation 115 | 116 | We'll briefly cover the validation for objects, arrays, and the separator. 117 | 118 | Essentially, `(json-array-check)`, `(json-object-check)` simply validate whether the value following the `{` or `[` brackets are allowed. 119 | 120 | The `(json-object-check-separator)` is used to ensure a `:` separates the string from the value (ex: `{"string" : value}`). 121 | 122 | ```picolisp 123 | [de json-object-check (Name) 124 | (or 125 | (lst? Name) 126 | (= "}" Name) 127 | (err-throw (text "Invalid Object name '@1', must be '}' OR string", Name) ] 128 | ``` 129 | 130 | As you can see, it's quite simple, and if there's no match, `(err-throw)` will be called. 131 | 132 | # Internal functions 133 | 134 | This part of the code was completely rewritten from scratch, so we'll go through it together. 135 | 136 | ## decoding JSON 137 | 138 | We'll begin by looking at how JSON is decoded in this library. 139 | 140 | ### (iterate-object) 141 | 142 | A fully tokenized JSON string might look like this: 143 | 144 | ```picolisp 145 | ("{" ("t" "e" "s" "t") ":" "[" 1 "," 2 "," 3 "]" "}") 146 | ``` 147 | 148 | Now, look at the `(iterate-object)` function. This is a recursive function which loops and iterates through the global `*Json` variable, a list which contains the tokenized JSON string, and then quickly builds a sexy PicoLisp list. 149 | 150 | ```picolisp 151 | [de iterate-object () 152 | (let Type (pop '*Json) 153 | (cond 154 | ((= "[" Type) (make (link-array T))) 155 | ((= "{" Type) (make (link-object))) 156 | ((lst? Type) (pack Type)) 157 | ((num? Type) Type) 158 | ((= "-" Type) (if (num? (car *Json)) (format (pack "-" (pop '*Json))) (iterate-object))) 159 | ((= 'true Type) 'true) 160 | ((= 'false Type) 'false) 161 | ((= 'null Type) 'null) 162 | (T (err-throw (text "Invalid Object '@1', must be '[' OR '{' OR string OR number OR true OR false OR null", Type) ] 163 | ``` 164 | 165 | We treat the `*Json` list as a stack, and iterate through it after popping one or more elements, until there's nothing left but tears of joy. 166 | 167 | The condition for `[` will start a new list with [make](https://software-lab.de/doc/refM.html#make), and call `(link-array)` with the argument `T`. We'll see why later. 168 | 169 | The rest is quite easy to understand, but I'll focus on the case of `(= "-" Type)`. The tokenization doesn't recognize negative numbers, so `-32` would be tokenized to `'("-" 32)`. To solve this, we check for a single `"-"`, and if the next item in the list is a number, then we [pop](https://software-lab.de/doc/refP.html#pop) the `"-"`, [pack](https://software-lab.de/doc/refP.html#pack) it with the number (`(pack)` creates a string), then use [format](https://software-lab.de/doc/refF.html#format) to convert it to a number. 170 | In other words, our tokenized `'("-" 23) -> -23`. Please note, since `-23` is not a string, this could not have been done in the Unicode parsing stage. It must occur after tokenization with `(str)`. 171 | 172 | ### (link-array) and (link-object) 173 | 174 | Both the `(link-array)` and `(link-object)` function make a call to the more generic `(link-generic)` function. It accepts three arguments: the type of item, the closing bracket, and an unevaluated [quote(ed)](https://software-lab.de/doc/refQ.html#quote) function. 175 | 176 | ```picolisp 177 | (link-generic "array" 178 | "]" 179 | '(link (iterate-object)) 180 | (link-generic "object" 181 | "}" 182 | '(link-object-value Name) ] 183 | ``` 184 | 185 | They're quite similar. In both cases, the function will iterate once more over the object, depending on various conditions described in `(link-generic)`. 186 | 187 | Let's look at some of the magic going on in `(link-generic)`: 188 | 189 | ```picolisp 190 | # 1. ((any (pack "json-" Type "-check")) Name) 191 | # 2. (unless (= Bracket Name) (eval Iterator)) 192 | ``` 193 | 194 | The first looks a bit weird, but it essentially uses [any](https://software-lab.de/doc/refA.html#any) and [pack](https://software-lab.de/doc/refP.html#pack) to dynamically generate a function name, and then calls it with the `Name` argument. 195 | 196 | This gives something like: `(json-array-check "[")` - dynamically generate Lisp functions ftw! 197 | 198 | The second is a bit easier to grok, where it simply [eval(uates)](https://software-lab.de/doc/refE.html#eval) the given function passed as through the variable `Iterator`. 199 | 200 | ### T 201 | 202 | Earlier, we saw `(link-array T)` was called, but sometimes, only `(link-array)` is called, without the `T` argument. Why? 203 | 204 | To differentiate an `Array` from an `Object` in PicoLisp, we append `T` to the start of the list. When recursing, unless it's a new array, we don't provide the `T` argument: 205 | 206 | ```picolisp 207 | (when Make (link T)) 208 | ``` 209 | 210 | The previously tokenized JSON string would end up like this: 211 | 212 | ```picolisp 213 | (("test" T 1 2 3)) 214 | ``` 215 | 216 | ## encoding JSON 217 | 218 | The code for encoding JSON strings hasn't changed, so feel free to [read about it here](https://github.com/aw/picolisp-json/blob/master/EXPLAIN.md#encoding-json). 219 | 220 | # The end 221 | 222 | That's pretty much all I have to explain about the new and improved `v3` pure PicoLisp JSONencoder/decoder. I'm very open to providing more details about functionality I've skipped, so just file an [issue](https://github.com/aw/picolisp-json/issues/new) and I'll look into amending this document. 223 | 224 | # License 225 | 226 | This work is licensed under a [Creative Commons Attribution-ShareAlike 4.0 International License](http://creativecommons.org/licenses/by-sa/4.0/). 227 | 228 | Copyright (c) 2018 Alexander Williams, Unscramble 229 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015-2020 Alexander Williams, Unscramble 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of 6 | this software and associated documentation files (the "Software"), to deal in 7 | the Software without restriction, including without limitation the rights to 8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software is furnished to do so, 10 | subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 17 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 18 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 19 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 20 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | # picolisp-json Makefile 2 | 3 | PIL_MODULE_DIR ?= .modules 4 | REPO_PREFIX ?= https://github.com/aw 5 | 6 | # Unit testing 7 | TEST_REPO = $(REPO_PREFIX)/picolisp-unit.git 8 | TEST_DIR = $(PIL_MODULE_DIR)/picolisp-unit/HEAD 9 | TEST_REF = v3.0.0 10 | 11 | .PHONY: all 12 | 13 | all: check 14 | 15 | $(TEST_DIR): 16 | mkdir -p $(TEST_DIR) && \ 17 | git clone $(TEST_REPO) $(TEST_DIR) && \ 18 | cd $(TEST_DIR) && \ 19 | git checkout $(TEST_REF) 20 | 21 | check: $(TEST_DIR) run-tests 22 | 23 | run-tests: 24 | ./test.l 25 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # JSON Encoder/Decoder for PicoLisp 2 | 3 | [![GitHub release](https://img.shields.io/github/release/aw/picolisp-json.svg)](https://github.com/aw/picolisp-json) [![Dependency](https://img.shields.io/badge/[deps] picolisp--unit-v3.0.0-ff69b4.svg)](https://github.com/aw/picolisp-unit.git) ![Build status](https://github.com/aw/picolisp-json/workflows/CI/badge.svg?branch=master) 4 | 5 | This library can be used to parse and serialize (encode/decode) JSON strings in pure [PicoLisp](http://picolisp.com/). 6 | 7 | ![picolisp-json](https://cloud.githubusercontent.com/assets/153401/6571543/56e31e44-c701-11e4-99f0-c2c51fd8061b.png) 8 | 9 | **NEW:** Please read [EXPLAIN_v3.md](EXPLAIN_v3.md) to learn more about PicoLisp and this (`v3`) JSON library. 10 | 11 | Please read [EXPLAIN.md](EXPLAIN.md) to learn more about PicoLisp and the older (`v2`) JSON library. 12 | 13 | 1. [Requirements](#requirements) 14 | 2. [Getting Started](#getting-started) 15 | 3. [Usage](#usage) 16 | 4. [Examples](#examples) 17 | 5. [Testing](#testing) 18 | 6. [Alternatives](#alternatives) 19 | 7. [Contributing](#contributing) 20 | 8. [Changelog](#changelog) 21 | 9. [License](#license) 22 | 23 | # Requirements 24 | 25 | * PicoLisp 32-bit `v3.1.11` to `v20.6` (tested) 26 | * PicoLisp 64-bit `v17.12` to `v20.6` (tested) 27 | * PicoLisp 64-bit `pil21` (tested) 28 | 29 | **BREAKING CHANGE since v4.0.0:** Namespaces have been completely removed, and all function names are now prefixed with _json-_ (see [Changelog](CHANGELOG.md)). 30 | 31 | # Getting Started 32 | 33 | This library has been rewritten in pure PicoLisp and contains **no external dependencies**. 34 | 35 | ~~These FFI bindings require the [Parson C library](https://github.com/kgabis/parson), compiled as a shared library~~ 36 | 37 | 1. Include `json.l` in your project 38 | 2. Try the [examples](#examples) below 39 | 40 | # Usage 41 | 42 | Public functions: 43 | 44 | * `(json-decode arg1 arg2)` parses a JSON string or file 45 | - `arg1` _String_: the JSON string or filename you want to decode 46 | - `arg2` _Flag (optional)_: a flag (`T` or `NIL`) indicating to parse a file if set 47 | * `(json-encode arg1)` serializes a list into a JSON string 48 | - `arg1` _List_: a PicoLisp list which will be converted to a JSON string 49 | 50 | ### JSON-PicoLisp data type table 51 | 52 | | JSON | PicoLisp | Example | 53 | | ---- | -------- | ------- | 54 | | Number | Number | `25 <-> 25` | 55 | | String | String | `"hello" <-> "hello"` | 56 | | Null | Transient _null_ Symbol | `null <-> 'null` | 57 | | Boolean | Transient _true_ or _false_ Symbol | `true <-> 'true` | 58 | | Array | List with T in cdar | `{"array":[1,2,3]} <-> '(("array" T 1 2 3))` | 59 | | Object | Cons pair | `{"hello":"world"} <-> '(("hello" . "world"))` | 60 | 61 | ### Notes 62 | 63 | * To disallow duplicate Object keys: `(on *Json_prevent_duplicate_keys)`. Default allows duplicate Object keys. 64 | * A successful result will return a list. 65 | * Failures return `NIL`, store the error message in `*Msg`, and print the error message to `STDERR` (standard error). 66 | * Keys are in `car`, values are in `cdr`. 67 | * When the 2nd item in the list is `T`, the rest of the list represents a JSON array. 68 | * When the 2nd item in the list is a cons pair, it represents a JSON object. 69 | * Supports Unicode characters as `"\uNNNN"` where `N` is a hexadecimal digit. 70 | 71 | ### JSON Specification 72 | 73 | This library conforms to the [ECMA-404 The JSON Data Interchange Standard](http://www.ecma-international.org/publications/files/ECMA-ST/ECMA-404.pdf), except for the following semantic exceptions: 74 | 75 | * [Numbers] Scientific (floating point, fractional, exponential) numbers (ex: `3.7e-5`) are not accepted. They must be provided as strings (ex: `"3.7e-5"`). 76 | 77 | # Examples 78 | 79 | ### (json-decode String) 80 | 81 | ```picolisp 82 | (load "json.l") 83 | 84 | (json-decode "{\"Hello\":\"World\"}") 85 | 86 | -> (("Hello" . "World")) 87 | ``` 88 | 89 | ### (json-decode Filename T) 90 | 91 | The same function is used for parsing JSON strings and files. 92 | Simply append `T` as the last argument if you want to parse a file. 93 | 94 | ```picolisp 95 | (load "json.l") 96 | 97 | (json-decode "test.json" T) 98 | 99 | -> (("first" . "John") 100 | ("last" . "Doe") 101 | ("age" . 25) 102 | ("registered" . true) 103 | ("interests" T "Reading" "Mountain Biking") 104 | ("favorites" ("color" . "blue") ("sport" . "running")) 105 | ("utf string" . "lorem ipsum") 106 | ("utf-8 string" . "あいうえお") 107 | ("surrogate string" . "lorem�ipsum�lorem") ) 108 | ``` 109 | 110 | ### (json-encode List) 111 | 112 | ```picolisp 113 | (load "json.l") 114 | 115 | (json-encode '(("Hello" . "World"))) 116 | 117 | -> "{\"Hello\":\"World\"}" 118 | ``` 119 | 120 | ### (json-decode InvalidString) 121 | 122 | ```picolisp 123 | 124 | (json-decode "{\"Hello\":invalid}") 125 | "Invalid Object 'invalid', must be '[' OR '{' OR string OR number OR true OR false OR null" 126 | 127 | -> NIL 128 | ``` 129 | 130 | # Testing 131 | 132 | This library comes with full [unit tests](https://github.com/aw/picolisp-unit). To run the tests, type: 133 | 134 | make check 135 | 136 | # Alternatives 137 | 138 | The following are alternatives also written in pure PicoLisp. They are limited by pipe/read syscalls. 139 | 140 | * [JSON reader/writer](http://rosettacode.org/wiki/JSON#PicoLisp) by Alexander Burger. 141 | * [JSON reader/writer](https://bitbucket.org/hsarvell/ext/src/9d6e5a15c5ce7cb47033e0082ef70aee6c4c8dd7/json.l?at=default) by Henrik Sarvell. 142 | 143 | # Contributing 144 | 145 | If you find any bugs or issues, please [create an issue](https://github.com/aw/picolisp-json/issues/new). 146 | 147 | If you want to improve this library, please make a pull-request. 148 | 149 | # Changelog 150 | 151 | * [Changelog](CHANGELOG.md) 152 | 153 | # License 154 | 155 | [MIT License](LICENSE) 156 | 157 | Copyright (c) 2015-2020 Alexander Williams, Unscramble 158 | -------------------------------------------------------------------------------- /build.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # 3 | # Copyright (c) 2015 Alexander Williams, Unscramble 4 | # MIT License 5 | # 6 | # For backwards compatibility 7 | 8 | set -u 9 | set -e 10 | 11 | # cleanup artifacts 12 | rm -rf lib vendor 13 | 14 | # rebuild 15 | make 16 | -------------------------------------------------------------------------------- /json.l: -------------------------------------------------------------------------------- 1 | # json.l 2 | # 3 | # The MIT License (MIT) 4 | # 5 | # Copyright (c) 2015-2020 Alexander Williams, Unscramble 6 | 7 | (setq *Json_control_characters (extract '((N) (unless (member N '("^H" "^L" "^J" "^M" "^I")) N)) (mapcar char (range 1 31)))) 8 | 9 | # send error message to STDERR 10 | [de json-err-throw (Error) 11 | (msg Error) 12 | (setq *Msg Error) 13 | (throw 'invalid-json NIL) ] 14 | 15 | # json 16 | [de json-parse-file (File) 17 | (json-parse-string (in File (till NIL T) ] 18 | 19 | [de json-parse-string (Value) 20 | (let Str (str (json-parse-unicode (chop Value)) "_") 21 | (json-count-brackets Str) 22 | Str ] 23 | 24 | [de json-parse-unicode-special (Value) 25 | (case (lowc Value) 26 | ("0022" "\\\"") 27 | ("005c" "\\\\") 28 | ("005e" "\\\^") 29 | (T (char (hex Value) ] 30 | 31 | [de json-parse-unicode (Value) 32 | (pack 33 | (make 34 | (while Value 35 | (let R (pop 'Value) 36 | (cond 37 | [(= "\^" R) (link "\\\^") ] # ^ becomes \^ 38 | [(and (= "\\" R) (= "u" (car Value))) (let U (cut 5 'Value) (link (json-parse-unicode-special (pack (tail 4 U) ] # \uNNNN hex 39 | [(and (= "\\" R) (= "b" (car Value))) (pop 'Value) (link (char (hex "08") ] # \b backspace 40 | [(and (= "\\" R) (= "f" (car Value))) (pop 'Value) (link (char (hex "0C") ] # \f formfeed 41 | (T (link R)) ] 42 | 43 | [de json-count-brackets (Str) 44 | (let Json_stack NIL 45 | (mapc '((N) 46 | (if (or (= "{" N) (= "[" N)) 47 | (push 'Json_stack N) 48 | (case N 49 | ("]" (let R (pop 'Json_stack) (unless (= "[" R) (json-err-throw "Unmatched JSON brackets '['")))) 50 | ("}" (let R (pop 'Json_stack) (unless (= "{" R) (json-err-throw "Unmatched JSON brackets '{'")))) ) ) ) 51 | Str ) 52 | (when Json_stack (json-err-throw (text "Unmatched JSON brackets '@1'", (pop Json_stack) ] 53 | 54 | [de json-array-check (Value) 55 | (or 56 | (= "{" Value) 57 | (= "[" Value) 58 | (= "]" Value) 59 | (lst? Value) 60 | (num? Value) 61 | (= "-" Value) 62 | (= 'true Value) 63 | (= 'false Value) 64 | (= 'null Value) 65 | (json-err-throw (text "Invalid Array value '@1', must be {' OR '[' OR ']' OR string OR number OR true OR false OR null", Value) ] 66 | 67 | [de json-object-check (Name) 68 | (when (and *Json_prevent_duplicate_keys (assoc (pack Name) (made))) 69 | (json-err-throw (text "Duplicate Object key '@1'", Name)) ) 70 | (or 71 | (lst? Name) 72 | (= "}" Name) 73 | (json-err-throw (text "Invalid Object name '@1', must be '}' OR string", Name) ] 74 | 75 | [de json-object-check-separator (Separator) 76 | (unless (= ":" Separator) 77 | (json-err-throw (text "Invalid Object separator '@1', must be ':'", Separator) ] 78 | 79 | # internal 80 | [de json-link-generic (Checker Linker Bracket Iterator) 81 | (let Name (car *Json) 82 | (when Name 83 | (eval Checker) 84 | (unless (= Bracket Name) (eval Iterator)) ) 85 | 86 | (if (= Bracket (car *Json)) 87 | (pop '*Json) 88 | (when (= "," (car *Json)) 89 | (pop '*Json) 90 | (eval Linker) ) 91 | (unless (car *Json) 92 | (cut 2 '*Json) ] 93 | 94 | [de json-link-array () 95 | (json-link-generic '(json-array-check Name) 96 | '(json-link-array) 97 | "]" 98 | '(link (json-iterate-object)) ] 99 | 100 | [de json-link-object () 101 | (json-link-generic '(json-object-check Name) 102 | '(json-link-object) 103 | "}" 104 | '(json-link-object-value Name) ] 105 | 106 | [de json-link-object-value (Name) 107 | (pop '*Json) 108 | (json-object-check-separator (pop '*Json)) 109 | (link (cons (pack Name) (json-iterate-object))) ] 110 | 111 | [de json-iterate-object () 112 | (let Type (pop '*Json) 113 | (cond 114 | ((= "[" Type) (make (link T) (json-link-array))) 115 | ((= "{" Type) (make (json-link-object))) 116 | ((lst? Type) (pack Type)) 117 | ((num? Type) Type) 118 | ((= "-" Type) (if (num? (car *Json)) (format (pack "-" (pop '*Json))) (json-iterate-object))) 119 | ((= 'true Type) 'true) 120 | ((= 'false Type) 'false) 121 | ((= 'null Type) 'null) 122 | (T (json-err-throw (text "Invalid Object '@1', must be '[' OR '{' OR string OR number OR true OR false OR null", Type) ] 123 | 124 | [de json-iterate-list (Item) 125 | (let Value (cdr Item) 126 | (or 127 | (json-make-null Value) 128 | (json-make-boolean Value) 129 | (json-make-json-number Value) 130 | (json-make-json-string Value) 131 | (json-make-json-array Value) 132 | (json-make-object Value) ] 133 | 134 | [de json-make-null (Value) 135 | (when (== 'null Value) 'null) ] 136 | 137 | [de json-make-boolean (Value) 138 | (cond ((== 'true Value) 'true) 139 | ((== 'false Value) 'false) ] 140 | 141 | [de json-make-json-number (Value) 142 | (when (num? Value) Value) ] 143 | 144 | [de json-make-json-string (Value) 145 | (when (str? Value) 146 | (pack 147 | "\"" 148 | (extract '((N) 149 | (if (member N *Json_control_characters) 150 | (pack "\\u" (pad 4 (hex (char N)))) 151 | (case N 152 | ("\"" "\\\"") 153 | ("\\" "\\\\") 154 | ("^H" "\\b") 155 | ("^L" "\\f") 156 | ("^J" "\\n") 157 | ("^M" "\\r") 158 | ("^I" "\\t") 159 | ("\\\^" "\^") 160 | (T N) ) ) ) 161 | (chop Value) ) 162 | "\"" ] 163 | 164 | [de json-make-json-array (Value) 165 | (when (=T (car Value)) (json-make-array (cdr Value))) ] 166 | 167 | [de json-make-generic (Bracket_open Bracket_close Iterator) 168 | (pack Bracket_open 169 | (glue "," (mapcar '((N) (eval Iterator)) Value)) 170 | Bracket_close ] 171 | 172 | [de json-make-object (Value) 173 | (json-make-generic "{" 174 | "}" 175 | '(pack "\"" (car N) "\":" (json-iterate-list N)) ] 176 | 177 | [de json-make-array (Value) 178 | (json-make-generic "[" 179 | "]" 180 | '(json-iterate-list (cons NIL N)) ] 181 | 182 | # public 183 | [de json-decode (Value Type) 184 | (catch 'invalid-json 185 | (use *Json 186 | (setq *Json 187 | (if Type 188 | (json-parse-file Value) 189 | (json-parse-string Value) ) ) 190 | (when *Json 191 | (json-iterate-object) ) ] 192 | 193 | [de json-encode (Value) 194 | (if (=T (car Value)) 195 | (json-make-array (cdr Value)) 196 | (json-make-object Value) ] 197 | -------------------------------------------------------------------------------- /module.l: -------------------------------------------------------------------------------- 1 | [de MODULE_INFO 2 | ("name" "json") 3 | ("version" "4.1.0") 4 | ("summary" "JSON encoder/decoder for PicoLisp") 5 | ("source" "https://github.com/aw/picolisp-json.git") 6 | ("author" "Alexander Williams") 7 | ("license" "MIT") 8 | ("copyright" "(c) 2015-2020 Alexander Williams, Unscramble ") 9 | ("install" "make") 10 | ("requires" 11 | ("picolisp-unit" "v3.0.0" "https://github.com/aw/picolisp-unit.git") ] 12 | -------------------------------------------------------------------------------- /test.json: -------------------------------------------------------------------------------- 1 | { 2 | "first": "John", 3 | "last": "Doe", 4 | "age": 25, 5 | "registered": true, 6 | "interests": [ "Reading", "Mountain Biking" ], 7 | "favorites": { 8 | "color": "blue", 9 | "sport": "running" 10 | }, 11 | "utf string" : "\u006corem\u0020ipsum", 12 | "utf-8 string": "あいうえお", 13 | "surrogate string": "lorem ipsum lorem" 14 | } -------------------------------------------------------------------------------- /test.l: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env pil 2 | 3 | (load (pack (car (file)) ".modules/picolisp-unit/HEAD/unit.l")) 4 | 5 | (chdir "test/" 6 | (mapcar load (filter '((N) (sub? "test_" N)) (dir "."))) ) 7 | 8 | (report) 9 | -------------------------------------------------------------------------------- /test/test_json.l: -------------------------------------------------------------------------------- 1 | (load (pack (car (file)) "../json.l")) 2 | 3 | (prinl "^J Testing JSON encoder/decoder for PicoLisp^J") 4 | 5 | (setq 6 | *My_tests_are_order_dependent NIL 7 | *Json_output '(("first" . "John") 8 | ("last" . "Doe") 9 | ("age" . 25) 10 | ("registered" . true) 11 | ("interests" T "Reading" "Mountain Biking") 12 | ("favorites" ("color" . "blue") ("sport" . "running")) 13 | ("utf string" . "lorem ipsum") 14 | ("utf-8 string" . "あいうえお") 15 | ("surrogate string" . "lorem ipsum lorem") ) ) 16 | 17 | (setq 18 | *Json_output2 '(("string" . "lorem ipsum") ("utf string" . "lorem ipsum") ("utf-8 string" . "あいうえお") ("surrogate string" . "lorem𝄞ipsum𝍧lorem") ("positive one" . 1) ("negative one" . -1) ("pi" . 3) ("hard to parse number" . 0) ("big int" . 2147483647) ("big uint" . 4294967295) ("boolean true" . true) ("boolean false" . false) ("null" . null) ("string array" T "lorem" "ipsum") ("x\^2 array" T 0 1 4 9 16 25 36 49 64 81 100) ("/*" . null) ("object" ("nested string" . "str") ("nested true" . true) ("nested false" . false) ("nested null" . null) ("nested number" . 123) ("nested array" T "lorem" "ipsum")) ("*/" . null) ("/**/" . "comment") ("//" . "comment") ("url" . "https://www.example.com/search?q=12345") ("escaped chars" . "\" \\ /") ("empty object") ("empty array" T)) 19 | *Json_output3 '(T "JSON Test Pattern pass1" (("object with 1 member" T "array with 1 element")) NIL (T) -42 true false null (("integer" . 1234567890) ("real" . -9877) (" " . "t") ("zero" . 0) ("one" . 1) ("space" . " ") ("quote" . "\"") ("backslash" . "\\") ("controls" . "^H^L^J^M^I") ("slash" . "/ & /") ("alpha" . "abcdefghijklmnopqrstuvwyz") ("ALPHA" . "ABCDEFGHIJKLMNOPQRSTUVWYZ") ("digit" . "0123456789") ("0123456789" . "digit") ("special" . "`1~!@#$%\^&*()_+-={:[,]}|;.?") ("hex" . "ģ䕧覫췯ꯍ") ("true" . true) ("false" . false) ("null" . null) ("array" T) ("object") ("address" . "50 St. James Street") ("url" . "http://www.JSON.org/") ("comment" . "// /* */" . " "))) ) 20 | 21 | [de test-decode-string () 22 | (assert-equal '(("Hello" . "World")) 23 | (json-decode "{\"Hello\":\"World\"}") 24 | "Decode JSON string into a list" ] 25 | 26 | [de test-decode-file () 27 | (assert-equal *Json_output 28 | (json-decode "../test.json" T) 29 | "Decode JSON file into a list" ] 30 | 31 | [de test-decode-file2 () 32 | (assert-equal *Json_output2 33 | (json-decode "../test2.json" T) 34 | "Decode JSON file2 into a list" ] 35 | 36 | [de test-decode-file3 () 37 | (assert-equal *Json_output3 38 | (json-decode "../test3.json" T) 39 | "Decode JSON file3 into a list" ] 40 | 41 | [de test-decode-invalid () 42 | (assert-nil (json-decode "{\"I am not JASON\"}") 43 | "Fail to decode an invalid JSON string" ] 44 | 45 | [de test-encode-string () 46 | (assert-equal "{\"Hello\":\"World\"}" 47 | (json-encode '(("Hello" . "World"))) 48 | "Encode list into JSON string" ] 49 | 50 | [de test-decode-unicode () 51 | (assert-equal '(("name" . "^H")) (json-decode "{\"name\":\"\\b\"}") "Ensure '\\b' backspace is decoded") 52 | (assert-equal '(("name" . "^L")) (json-decode "{\"name\":\"\\f\"}") "Ensure '\\f' formfeed is decoded") 53 | (assert-equal '(("name" . "^J")) (json-decode "{\"name\":\"\\n\"}") "Ensure '\\n' newline is decoded") 54 | (assert-equal '(("name" . "^M")) (json-decode "{\"name\":\"\\r\"}") "Ensure '\\r' carriage return is decoded") 55 | (assert-equal '(("name" . "^I")) (json-decode "{\"name\":\"\\t\"}") "Ensure '\\t' horizontal tab is decoded") ] 56 | 57 | [de test-decode-002f () 58 | (assert-equal '(("name" . "/")) (json-decode "{\"name\":\"\\u002F\"}") "Ensure '\\u002F' produces the same result: /") 59 | (assert-equal '(("name" . "/")) (json-decode "{\"name\":\"\\u002f\"}") "Ensure '\\u002f' produces the same result: /") 60 | (assert-equal '(("name" . "/")) (json-decode "{\"name\":\"\\/\"}") "Ensure '\\/' produces the same result: /") 61 | (assert-equal '(("name" . "/")) (json-decode "{\"name\":\"/\"}") "Ensure '/' produces the same result: /") ] 62 | 63 | [de test-duplicate-keys () 64 | (on *Json_prevent_duplicate_keys) 65 | (assert-nil (json-decode "{\"test\":true,\"test\":false}") "Duplicate keys are not allowed") 66 | (off *Json_prevent_duplicate_keys) 67 | (assert-equal '(("test" . true) ("test" . false)) (json-decode "{\"test\":true,\"test\":false}") "Duplicate keys are allowed") ] 68 | 69 | [execute 70 | '(test-decode-string) 71 | '(test-decode-file) 72 | '(test-decode-file2) 73 | '(test-decode-file3) 74 | '(test-decode-invalid) 75 | '(test-encode-string) 76 | 77 | # validations since v3.0 78 | '(assert-nil (json-decode "{\"Name\":invalid}") "Invalid value returns NIL") 79 | '(assert-nil (json-decode "{invalid:\"Value\"}") "Invalid name returns NIL") 80 | '(assert-nil (json-decode "{\"Name\":[invalid]}") "Invalid array value returns NIL") 81 | '(assert-nil (json-decode "{\"Name\":[1,]") "Invalid array separator returns NIL") 82 | '(assert-nil (json-decode "{\"Name\":[1]") "Unmatched JSON brackets returns NIL") 83 | '(assert-nil (json-decode "{\"Name\":[1,2,[]}") "Unmatched JSON brackets returns NIL") 84 | '(assert-nil (json-decode "{\"Name\":[1}") "Unmatched JSON brackets returns NIL") 85 | '(assert-nil (json-decode "{\"Name\":[1,[}") "Unmatched JSON brackets returns NIL") 86 | '(assert-nil (json-decode "{}") "Empty object returns NIL") 87 | '(assert-equal '(T) (json-decode "[]") "Empty array returns T list") 88 | '(assert-equal '(("name")) (json-decode "{\"name\":{}}") "Empty object value returns object only") 89 | '(assert-equal '(("name" T 1 2)) (json-decode "{\"name\":[1,2]}") "Object and array") 90 | '(assert-equal '(("name" . -23)) (json-decode "{\"name\":-23}") "Negative numbers are valid") 91 | '(assert-nil (json-decode "{\"name\":-garbage}") "Negative garbage returns NIL") 92 | '(assert-nil (json-decode "{\"name\"::23}") "Double colons returns NIL") 93 | '(assert-nil (json-decode "{\"name\":[1,2,,3]}") "Double commas in Array returns NIL") 94 | '(assert-nil (json-decode "{\"name\":true,,\"name2\":false}") "Double commas in Object returns NIL") 95 | '(assert-equal '(("name" T 1 2 -3)) (json-decode "{\"name\":[1,2,-3]}") "Array values can be negative numbers") 96 | '(assert-equal "{\"name\":[1,2,-23]}" (json-encode (json-decode (json-encode (json-decode "{\"name\":[1,2,-23]}")))) "Yo Dawg, (json-encode (json-decode (json-encode (json-decode...") 97 | '(test-decode-unicode) 98 | '(test-decode-002f) 99 | '(assert-equal (or (json-decode "{\"name\":invalid}") *Msg) "Invalid Object 'invalid', must be '[' OR '{' OR string OR number OR true OR false OR null" "Error message is stored in *Msg") 100 | '(test-duplicate-keys) ] 101 | -------------------------------------------------------------------------------- /test/test_regressions.l: -------------------------------------------------------------------------------- 1 | (load (pack (car (file)) "../json.l")) 2 | 3 | # Regression tests 4 | 5 | # Invalid result from double-quoted strings - https://github.com/aw/picolisp-json/issues/4 6 | [de test-gh-issue-4 () 7 | (assert-equal "{\"etag\":\"\\\"12345\\\"\"}" 8 | (json-encode '(("etag" . "\"12345\""))) 9 | "Regression test GH issue #4 - Invalid result from double-quoted strings" ] 10 | 11 | # Null and False values are not encoded correctly - https://github.com/aw/picolisp-json/issues/5 12 | [de test-gh-issue-5 () 13 | (assert-equal "{\"anullvalue\":null}" 14 | (json-encode (list (cons "anullvalue" 'null))) 15 | "Regression test GH issue #5 - Null values are not encoded correctly" ) 16 | (assert-equal "{\"afalsevalue\":false}" 17 | (json-encode (list (cons "afalsevalue" 'false))) 18 | "Regression test GH issue #5 - False values are not encoded correctly" ] 19 | 20 | # Invalid parsing of empty arrays and empty objects - https://github.com/aw/picolisp-json/issues/6 21 | [de test-gh-issue-6 () 22 | (assert-equal '(("test" T 1 2 NIL 4 NIL NIL (T) (T)) ("empty") ("true" . true)) 23 | (json-decode "{\"test\":[1,2,{},4,{},{},[],[]],\"empty\":{},\"true\":true}") 24 | "Regression test GH issue #6 - Invalid parsing of empty arrays and empty objects" ] 25 | 26 | # Invalid encoding of true,false,null - https://github.com/aw/picolisp-json/issues/8 27 | [de test-gh-issue-8 () 28 | (assert-equal "{\"test\":\"true\"}" 29 | (json-encode '(("test" . "true"))) 30 | "Regression test GH issue #8 - Invalid encoding of true" ) 31 | (assert-equal "{\"test\":\"false\"}" 32 | (json-encode '(("test" . "false"))) 33 | "Regression test GH issue #8 - Invalid encoding of false" ) 34 | (assert-equal "{\"test\":\"null\"}" 35 | (json-encode '(("test" . "null"))) 36 | "Regression test GH issue #8 - Invalid encoding of null" ) 37 | (assert-equal "{\"test\":true}" 38 | (json-encode '(("test" . true))) 39 | "Regression test GH issue #8 - Invalid encoding of true" ) 40 | (assert-equal "{\"test\":false}" 41 | (json-encode '(("test" . false))) 42 | "Regression test GH issue #8 - Invalid encoding of false" ) 43 | (assert-equal "{\"test\":null}" 44 | (json-encode '(("test" . null))) 45 | "Regression test GH issue #8 - Invalid encoding of null" ) ] 46 | 47 | ###### 48 | 49 | # Invalid parsing of caret (^) characters - https://github.com/aw/picolisp-json/issues/10 50 | [de test-gh-issue-10 () 51 | (assert-equal '(("test" . "x\^2")) 52 | (json-decode "../test4.json" T) 53 | "Regression test GH issue #10 - Invalid parsing of caret (\^) characters (1)" ) 54 | (assert-equal "{\"test\":\"x\^2\"}" 55 | (json-encode (json-decode "../test4.json" T)) 56 | "Regression test GH issue #10 - Invalid parsing of caret (\^) characters (2)" ) 57 | (assert-equal '(("test" . "x\^2")) 58 | (json-decode (json-encode (json-decode "../test4.json" T))) 59 | "Regression test GH issue #10 - Invalid parsing of caret (\^) characters (3)" ) 60 | (assert-equal "{\"test\":\"x\^2\\\\u1234\"}" 61 | (json-encode '(("test" . "x\^2\\u1234"))) 62 | "Regression test GH issue #10 - Invalid parsing of caret (\^) characters (4)" ] 63 | 64 | # Invalid encoding of special control (^J^M^I) characters - https://github.com/aw/picolisp-json/issues/11 65 | [de test-gh-issue-11 () 66 | (assert-equal "{\"test\":\"\\n\\r\\t\\b\\f\"}" 67 | (json-encode (json-decode "../test5.json" T)) 68 | "Regression test GH issue #11 - Invalid encoding of special control (\^J\^M\^I) characters (1)" ) 69 | (assert-equal "{\"test\":\"Hello\\n\\r\\t\\b\\fWorld\"}" 70 | (json-encode (list (cons "test" "Hello^J^M^I^H^LWorld"))) 71 | "Regression test GH issue #11 - Invalid encoding of special control (\^J\^M\^I) characters (2)" ) ] 72 | 73 | # Invalid encoding of quote and solidus (\ and ") characters - https://github.com/aw/picolisp-json/issues/12 74 | [de test-gh-issue-12 () 75 | (assert-equal "{\"test\":\"hello\\\"\\\\\"}" 76 | (json-encode (json-decode "../test6.json" T)) 77 | "Regression test GH issue #12 - Invalid encoding of quote and solidus (\\ and \") characters (1)" ) 78 | (assert-equal "{\"test\":\"Hello\\\\\\\"\\\\\\\\World\"}" 79 | (json-encode (list (cons "test" "Hello\\\"\\\\World"))) 80 | "Regression test GH issue #12 - Invalid encoding of quote and solidus (\\ and \") characters (2)" ) 81 | (assert-equal "{\"test\":\"/\"}" 82 | (json-encode (list (cons "test" "/"))) 83 | "Regression test GH issue #12 - Invalid encoding of quote and solidus (\\ and \") characters (3)" ) 84 | (assert-equal "{\"test\":\"/\"}" 85 | (json-encode (list (cons "test" "\/"))) 86 | "Regression test GH issue #12 - Invalid encoding of quote and solidus (\\ and \") characters (4)" ) 87 | (assert-equal '(("test" . "\/")) 88 | (json-decode "{\"test\":\"/\"}") 89 | "Regression test GH issue #12 - Invalid encoding of quote and solidus (\\ and \") characters (5)" ) 90 | (assert-equal '(("test" . "/")) 91 | (json-decode "{\"test\":\"/\"}") 92 | "Regression test GH issue #12 - Invalid encoding of quote and solidus (\\ and \") characters (6)" ) ] 93 | 94 | # Invalid encoding of control characters 01-0x1F - https://github.com/aw/picolisp-json/issues/13 95 | [de test-gh-issue-13 () 96 | (assert-equal "{\"test\":\"\\u0001\\u001E\\u001F \"}" 97 | (json-encode (json-decode "../test7.json" T)) 98 | "Regression test GH issue #13 - Invalid encoding of control characters 0x01-0x1F (1)" ) 99 | (assert-equal "{\"test\":\"\\u0001\\u001E\\u001F \"}" 100 | (json-encode (list (cons "test" (pack "^A^^^_" (char 32))))) 101 | "Regression test GH issue #13 - Invalid encoding of control characters 0x01-0x1F (2)" ) ] 102 | 103 | }# Invalid parsing of certain characters with \uNNNN - https://github.com/aw/picolisp-json/issues/15 104 | [de test-gh-issue-15 () 105 | (assert-equal '(("test" . "\"")) 106 | (json-decode "{\"test\":\"\\u0022\"}") 107 | "Regression test GH issue #15 - Invalid parsing of certain characters with \uNNNN (1)" ) 108 | (assert-equal '(("test" . "\\")) 109 | (json-decode "{\"test\":\"\\u005c\"}") 110 | "Regression test GH issue #15 - Invalid parsing of certain characters with \uNNNN (2)" ) 111 | (assert-equal '(("test" . "\^")) 112 | (json-decode "{\"test\":\"\\u005e\"}") 113 | "Regression test GH issue #15 - Invalid parsing of certain characters with \uNNNN (3)" ) ] 114 | 115 | # Unable to parse empty arrays with quotes https://github.com/aw/picolisp-json/issues/19 116 | [de test-gh-issue-19 () 117 | (assert-equal '(("myarray" T) ("test" . 12345)) 118 | (json-decode "{\"myarray\":[\"\"],\"test\":12345}") 119 | "Regression test GH issue #19 - Unable to parse empty arrays with quotes (1)" ) 120 | (assert-equal '(("myarray" T) ("test" . 12345)) 121 | (json-decode "{\"myarray\":[],\"test\":12345}") 122 | "Regression test GH issue #19 - Unable to parse empty arrays with quotes (2)" ) ] 123 | 124 | [execute 125 | '(test-gh-issue-4) 126 | '(test-gh-issue-5) 127 | '(test-gh-issue-6) 128 | '(test-gh-issue-8) 129 | '(test-gh-issue-10) 130 | '(test-gh-issue-11) 131 | '(test-gh-issue-12) 132 | '(test-gh-issue-13) 133 | '(test-gh-issue-15) 134 | '(test-gh-issue-19) ] 135 | -------------------------------------------------------------------------------- /test2.json: -------------------------------------------------------------------------------- 1 | { 2 | "string": "lorem ipsum", 3 | "utf string": "lorem ipsum", 4 | "utf-8 string": "あいうえお", 5 | "surrogate string": "lorem𝄞ipsum𝍧lorem", 6 | "positive one": 1, 7 | "negative one": -1, 8 | "pi": 3.1400000000000001, 9 | "hard to parse number": -0.00031399999999999999, 10 | "big int": 2147483647, 11 | "big uint": 4294967295, 12 | "boolean true": true, 13 | "boolean false": false, 14 | "null": null, 15 | "string array": [ 16 | "lorem", 17 | "ipsum" 18 | ], 19 | "x^2 array": [ 20 | 0, 21 | 1, 22 | 4, 23 | 9, 24 | 16, 25 | 25, 26 | 36, 27 | 49, 28 | 64, 29 | 81, 30 | 100 31 | ], 32 | "\/*": null, 33 | "object": { 34 | "nested string": "str", 35 | "nested true": true, 36 | "nested false": false, 37 | "nested null": null, 38 | "nested number": 123, 39 | "nested array": [ 40 | "lorem", 41 | "ipsum" 42 | ] 43 | }, 44 | "*\/": null, 45 | "\/**\/": "comment", 46 | "\/\/": "comment", 47 | "url": "https:\/\/www.example.com\/search?q=12345", 48 | "escaped chars": "\" \\ \/", 49 | "empty object": {}, 50 | "empty array": [] 51 | } -------------------------------------------------------------------------------- /test3.json: -------------------------------------------------------------------------------- 1 | [ 2 | "JSON Test Pattern pass1", 3 | { 4 | "object with 1 member": ["array with 1 element"] 5 | }, 6 | {}, 7 | [], -42, 8 | true, 9 | false, 10 | null, 11 | { 12 | "integer": 1234567890, 13 | "real": -9876.543210, 14 | " ":"t", 15 | "zero": 0, 16 | "one": 1, 17 | "space": " ", 18 | "quote": "\"", 19 | "backslash": "\\", 20 | "controls": "\b\f\n\r\t", 21 | "slash": "/ & \/", 22 | "alpha": "abcdefghijklmnopqrstuvwyz", 23 | "ALPHA": "ABCDEFGHIJKLMNOPQRSTUVWYZ", 24 | "digit": "0123456789", 25 | "0123456789": "digit", 26 | "special": "`1~!@#$%^&*()_+-={:[,]}|;.?", 27 | "hex": "\u0123\u4567\u89AB\uCDEF\uabcd\uef4A", 28 | "true": true, 29 | "false": false, 30 | "null": null, 31 | "array": [], 32 | "object": {}, 33 | "address": "50 St. James Street", 34 | "url": "http://www.JSON.org/", 35 | "comment": "// /* */": " " 37 | } 38 | ] 39 | -------------------------------------------------------------------------------- /test4.json: -------------------------------------------------------------------------------- 1 | {"test":"x^2"} 2 | -------------------------------------------------------------------------------- /test5.json: -------------------------------------------------------------------------------- 1 | {"test":"\n\r\t\b\f"} 2 | -------------------------------------------------------------------------------- /test6.json: -------------------------------------------------------------------------------- 1 | {"test":"hello\"\\"} 2 | -------------------------------------------------------------------------------- /test7.json: -------------------------------------------------------------------------------- 1 | {"test":"\u0001\u001E\u001F "} 2 | --------------------------------------------------------------------------------