Introducing JSONParsing: A library for writing bidirectional JSON parsers

Hi :wave:

I'd like to introduce JSONParsing - a library for decoding and encoding JSON, built on top of Parsing.

This library provides an alternative approach for dealing with JSON to the default way in Swift, which is to use Codable. Where Codable is based on conforming types to protocols, JSONParsing lets you write parsers capable of converting between your types and JSON. This means, among other things, that you are not bound to having a single JSON representation for any given type. I write more about the benefits compared to Codable in the Motivation section of the project's README.

If you are already familiar with Parsing, you know that the parsers you write can be made invertible, or bidirectional. In the context of JSONParsing, this means that you can write parsers capable of both decoding and encoding JSON, in the same expression.

From the Quick start guide:

Let's see what it looks like to decode and encode json data using this library. Imagine, for example, you have json describing a movie:

let json = """
{
  "title": "Interstellar",
  "release_year": 2014,
  "director": "Christopher Nolan",
  "stars": [
    "Matthew McConaughey",
    "Anne Hathaway",
    "Jessica Chastain"
  ],
  "poster_url": "https://www.themoviedb.org/t/p/w1280/gEU2QniE6E77NI6lCU6MxlNBvIx.jpg",
  "added_to_favorites": true
}
""".data(using: .utf8)!

First, we define a corresponding Movie type:

struct Movie {
  let title: String
  let releaseYear: Int
  let director: String
  let stars: [String]
  let posterUrl: URL?
  let addedToFavorites: Bool
}

Then, we can create a JSON parser, to handle the decoding of the json into this new data type:

extension Movie {
  static var jsonParser: some JSONParserPrinter<Self> {
    ParsePrint(.memberwise(Self.init)) {
      Field("title") { String.jsonParser() }
      Field("release_year") { Int.jsonParser() }
      Field("director") { String.jsonParser() }
      Field("stars") {
        JSONArray { String.jsonParser() }
      }
      OptionalField("poster_url") { URL.jsonParser() }
      Field("added_to_favorites") { Bool.jsonParser() }
    }
  }
}

Now, the Movie.jsonParser can be used to decode json data into Movie instances:

let decodedMovie = try Movie.jsonParser.decode(json)
print(decodedMovie)
// Movie(title: "Interstellar", releaseYear: 2014, director: "Christopher Nolan", stars: ["Matthew McConaughey", "Anne Hathaway", "Jessica Chastain"], posterUrl: Optional(https://www.themoviedb.org/t/p/w1280/gEU2QniE6E77NI6lCU6MxlNBvIx.jpg), addedToFavorites: true)

But what's even cooler is that the very same parser, without any extra work, can also be used to encode movie values into json:

let jokerMovie = Movie(
  title: "Joker",
  releaseYear: 2019,
  director: "Todd Phillips",
  stars: ["Joaquin Phoenix", "Robert De Niro"],
  posterUrl: URL(string: "https://www.themoviedb.org/t/p/w1280/udDclJoHjfjb8Ekgsd4FDteOkCU.jpg")!,
  addedToFavorites: true
)

let jokerJson = try Movie.jsonParser.encode(jokerMovie)
print(String(data: jokerJson, encoding: .utf8)!)
// {"added_to_favorites":true,"director":"Todd Phillips","poster_url":"https://www.themoviedb.org/t/p/w1280/udDclJoHjfjb8Ekgsd4FDteOkCU.jpg","release_year":2019,"stars":["Joaquin Phoenix","Robert De Niro"],"title":"Joker"}

Check it out if you are interested! :slightly_smiling_face:

10 Likes