RBBJSON

Swift Version: 5.2

Swift Package Manager

Swift Package Manager

Twitter: @DLX

RBBJSON enables flexible JSON traversal at runtime and JSONPath-like querying for rapid prototyping.

Use JSONDecoder to create an RBBJSON struct, then traverse it using dynamic member lookup:

let json = try JSONDecoder().decode(RBBJSON.self, from: data)

json.firstName         // RBBJSON.string("John")
json.lastName          // RBBJSON.string("Appleseed")
json.age               // RBBJSON.number(26)
json.invalidKey        // RBBJSON.null
json.phoneNumbers[0]   // RBBJSON.string("+14086065775")

If you want to access a value that coincides with a Swift-defined property, use a String subscript instead:

json.office.map     // Error: Maps to Sequence.map
json.office["map"]  // RBBJSON.string("https://maps.apple.com/?q=IL1")

To unbox a JSON value, use one of the failable initializers:

String(json.firstName) // "John"
String(json.lastName)  // "Appleseed"
String(json.age)       // nil

Int(json.age)          // 26
Double(json.age)       // 26.0

You can also make use of a JSONPath-inspired Query syntax to find nested data inside a JSON structure.

For example, given:

{ 
  "store": {
    "book": [ 
      { 
        "category": "reference",
        "author": "Nigel Rees",
        "title": "Sayings of the Century",
        "price": 8.95
      },
      { 
        "category": "fiction",
        "author": "Evelyn Waugh",
        "title": "Sword of Honour",
        "price": 12.99
      },
      { 
        "category": "fiction",
        "author": "Herman Melville",
        "title": "Moby Dick",
        "isbn": "0-553-21311-3",
        "price": 8.99
      },
      { 
        "category": "fiction",
        "author": "J. R. R. Tolkien",
        "title": "The Lord of the Rings",
        "isbn": "0-395-19395-8",
        "price": 22.99
      }
    ],
    "bicycle": {
      "color": "red",
      "price": 19.95
    }
  }
}
JSONPath RBBJSON Result
$.store.book[*].author json.store.book[any: .child].author The authors of all books in the store.
$..author json[any: .descendantOrSelf].author All authors.
$.store.* json.store[any: .child] All things in the store, a list of books an a red bycicle.
$.store..price json.store[any: .descendantOrSelf].price All prices in the store.
$..book[2] json[any: .descendantOrSelf].book[2] The second book.
$..book[-2] json[any: .descendantOrSelf].book[-2] The second-to-last book.
$..book[0,1], $..book[:2] json[any: .descendantOrSelf].book[0, 1]), json[any: .descendantOrSelf].book[0...1]), json[any: .descendantOrSelf].book[0..<2]) The first two books.
$..book[?(@.isbn)] json[any: .descendantOrSelf].book[has: \.isbn] All books with an ISBN number.
$..book[?(@.price<10)] json.store.book[matches: { $0.price <= 10 }] All books cheaper than 10.
$.store["book", "bicycle"]..["price", "author"] json.store["book", "bicycle"][any: .descendantOrSelf]["price", "author"] The author (where available) and price of every book or bicycle.

Once you query a JSON value using one of the higher order selectors, the resulting type of the expression will be a lazy RBBJSONQuery:

json.store.book[0]["title"]     // RBBJSON.string("Sayings of the Century")
json.store.book[0, 1]["title"]  // some RBBJSONQuery

Because RBBJSONQuery conforms to Sequence, you can initialize an Array with it to obtain the results or use e.g. compactMap:

String(json.store.book[0].title)                    // "Sayings of the Century"
json.store.book[0, 1].title.compactMap(String.init) // ["Sayings of the Century", "Sword of Honour"]

String(json.store.book[0]["invalid Property"])                    // nil
json.store.book[0, 1]["invalid Property"].compactMap(String.init) // []

GitHub

View Github