A parser for JSON data as specified by RFC 7159 which produces a stream of Event to be handled by the caller. The parser produces events as it reads the source input, so it's possible to start parsing JSON while it's still being received.

This parser does not enforce uniqueness of keys within JSON objects. It is usually not onerous for the caller to do so if they require such enforcement.

By default ParseExceptions will propagate out of calls to next() when a error is detected. You can use errorReporting to report errors as Exceptions within the stream.

Example

Suppose we have the domain model:

class Order(address, items) {
    shared String address;
    shared Item[] items;
}
class Item(sku, quantity) {
    shared String sku;
    shared Integer quantity;
}

And we want to parse JSON that looks like this:

    {
      "address":"",
      "items":[
        {
          "sku":"123-456-789",
          "quantity":4
        },
        {
          "sku":"456-789",
          "quantity":20
        }
      ]
    }

Then we might write a parser like this:

 class OrderParser() {

     late variable LookAhead<Event> stream;

     String missingKey(String container, String key) {
         return "``container``: '``key``' missing at line ``stream.line``'";
     }
     String duplicateKey(String container, String key) {
         return "``container``: '``key``' occurs more than once at line ``stream.line``'";
     }
     String keyType(String container, String key, String expectedType) {
         return "``container``: '``key``' key is supposed to be of ``expectedType`` type at line ``stream.line``";
     }
     String unexpectedKey(String container, String key) {
         return "``container``: '``key``' key not supported at line ``stream.line``";
     }
     String unexpectedEvent(String container, Event|Finished event) {
         return "``container``: unexpected event ``event else "null"`` at line ``stream.line``";
     }

     "Parses an item from events read from the given parser.
      Returns the item or an error explanation."
     Item|String parseItem() {
         if (!(stream.next() is ObjectStartEvent)) {
             return "Item: should be a JSON object";
         }
         variable String? sku = null;
         variable Integer? quantity = null;
         while(true) {
             switch(event=stream.next())
             case (is KeyEvent) {
                 switch(key=event.key) 
                 case ("sku") {
                     if (is String s = stream.next()) {
                         if (sku exists) {
                             return duplicateKey("Item", "sku");
                         }
                         sku = s;
                     } else {
                         return keyType("Item", "sku", "String");
                     }
                 }
                 case ("quantity") {
                     if (is Integer s = stream.next()) {
                         if (quantity exists) {
                             return duplicateKey("Item", "quantity");
                         }
                         quantity = s;
                     } else {
                         return keyType("Item", "sku", "Integer");
                     }
                 }
                 else {
                     return unexpectedKey("Item", key);
                 }
             }
             case (is ObjectEndEvent) {
                 if (exists s=sku) {
                     if (exists q=quantity) {
                         return Item(s, q);
                     }
                     return missingKey("Item", "quantity");
                 }
                 return missingKey("Item", "sku");
             }
             else {
                 return unexpectedEvent("Item", event);
             }
         }
     }

     "Parses an order from events read from the given parser.
      Returns the order or an error explanation."
     Order|String parseOrder() {
         if (!(stream.next() is ObjectStartEvent)) {
             return "Order: should be a JSON object";
         }
         variable String? address = null;
         value items = ArrayList<Item>();
         while(true) {
             switch(event=stream.next())
             case (is KeyEvent) {
                 switch(key=event.key) 
                 case ("address") {
                     if (is String s = stream.next()) {
                         if (address exists) {
                             return duplicateKey("Order", "address");
                         }
                         address = s;
                     } else {
                         return keyType("Order", "address", "String");
                     }
                 }
                 case ("items") {
                     if (!items.empty) {
                         return duplicateKey("Order", "items");
                     }
                     if (!stream.next() is ArrayStartEvent) {
                         return keyType("Order", "items", "Array");
                     }
                     while (stream.peek() is ObjectStartEvent) {
                         switch (item=parseItem())
                         case (is String) {
                             return item;
                         }
                         case (is Item) {
                             items.add(item);
                         }
                     }
                     assert(stream.next() is ArrayEndEvent);
                 }
                 else {
                     return unexpectedKey("Order", key);
                 }
             }
             case (is ObjectEndEvent) {
                 if (exists a=address) {
                     return Order(a, items.sequence());
                 }
                 return missingKey("Order", "address");
             }
             else {
                 return unexpectedEvent("Item", event);
             }
         }
     }

     shared Order|String parse(String json) {
         stream = LookAhead(StreamParser(StringTokenizer(json)));
         return parseOrder();
     }
 }

While this is certainly verbose it's extremely readable and regular.

no subtypes hierarchy

Initializer
StreamParser(Tokenizer input, Boolean trackKeys = false)
Parameters:
  • input

    The tokenizer to read input from

  • trackKeys = false

    Whether to validate the uniqueness of keys

Attributes
columnSource Codeshared actual Integer column

The column number within the current line.

lineSource Codeshared actual Integer line

The line number within the input.

positionSource Codeshared actual Integer position

The position (in characters) within the input.

stringSource Codeshared actual String string

A developer-friendly string representing the instance. Concatenates the name of the concrete class of the instance with the hash of the instance. Subclasses are encouraged to refine this implementation to produce a more meaningful representation.

Inherited Attributes
Attributes inherited from: Object
Attributes inherited from: Positioned
Methods
nextSource Codeshared actual Event|Finished next()

Return the next event from the stream, or finished

Inherited Methods
Methods inherited from: Object
Methods inherited from: Iterator<Element>