awwx.ws

Creating a parser combinator library to parse JSON

Prev: JSON arraysContentsNext: The finished result

JSON objects


Both JSON arrays and objects have a list of things interspersed by commas; in the case of json-object, it’s key-value pairs. In json-array, it was JSON values that were separated by commas:

(optional (cons-seq forward.json-value
                    (many (seq2 (skipwhite:match-is #\,)
                                (must "a comma must be followed by a value"
                                      forward.json-value)))))

I can extract that pattern:

(def parse-intersperse (separator parser must-message)
  (optional (cons-seq parser
                      (many (seq2 separator
                                  (must must-message parser))))))

The “must-message” is the error message to give if we see a separator, but it isn’t followed by another thing that the parser matches.

A comma separated list is a specific case:

(def comma-separated (parser must-message)
  (parse-intersperse (skipwhite:match-is #\,) parser must-message))

Now json-array is:

(= json-array
  (seq2 (match-is #\[)
        (comma-separated forward.json-value
                         "a comma must be followed by a value")
        (must "a JSON array must be terminated with a closing ]"
              (skipwhite:match-is #\]))))

In a json-object, key-value pairs are separated by a colon, and the key is always a JSON string:

(= json-object-kv
  (with-seq (key   skipwhite.json-string
             colon (skipwhite:match-is #\:)
             value forward.json-value)
    (list key value)))

This matches a single key-value pair, and returns them as a two element list:

arc> (show-parse json-object-kv "\"abc\":[1,2,3]")
returning: ("abc" (1 2 3)) remaining: 
nil

Arc’s listtab will convert a list of those key-value pairs into a table for us:

(= json-object
   (on-result listtab
     (seq2 (match-is #\{)
           (comma-separated json-object-kv "comma must be followed by a key")
           (skipwhite:match-is #\}))))
(= json-value
  (skipwhite:alt json-true
                 json-false
                 json-null
                 json-number
                 json-string
                 json-array
                 json-object))
arc> (fromjson «{"a": [1, 2, {"b": 3}]}»)
#hash(("a" . (1 2 #hash(("b" . 3)) . nil)))

As usual, we can report errors better...

arc> (fromjson «{»)
not a JSON value: {
arc> (fromjson «{"a"}»)
not a JSON value: {"a"}
arc> (fromjson «{"a":}»)
not a JSON value: {"a":}

toss in a few must’s:

(= json-object-kv
  (with-seq (key   skipwhite.json-string
             colon (must "a JSON object key string must be followed by a :"
                         (skipwhite:match-is #\:))
             value (must "a colon in a JSON object must be followed by a value"
                         forward.json-value))
    (list key value)))
(= json-object
   (on-result listtab
     (seq2 (match-is #\{)
           (comma-separated json-object-kv "comma must be followed by a key")
           (must "a JSON object must be terminated with a closing }"
                 (skipwhite:match-is #\})))))

and now we get:

arc> (fromjson «{»)
a JSON object must be terminated with a closing }
arc> (fromjson «{"a"}»)
a JSON object key string must be followed by a :
arc> (fromjson «{"a":}»)
a colon in a JSON object must be followed by a value

Prev: JSON arraysContentsNext: The finished result


Questions? Comments? Email me andrew.wilcox [at] gmail.com