Flight School

Benchmarking Codable

JSONDecoder vs. JSONSerialization... FIGHT!

Swift Codable can automatically synthesize initializers that decode models from JSON. But how does this generated code compare to what it replaces?

To find out, let’s benchmark the performance of JSONDecoder against equivalent hand-written code that uses JSONSerialization instead.

Defining a Sample Model

In order to establish a baseline, we’ll create a model that reasonably approximates something that you’d expect to find in a typical app.

For this example, we’ll use the following Airport model:

struct Airport: Codable {
  let name: String
  let iata: String
  let icao: String
  let coordinates: [Double]

  struct Runway: Codable {
    enum Surface: String, Codable {
      case rigid, flexible, gravel, sealed, unpaved, other
    }

    let direction: String
    let distance: Int
    let surface: Surface
  }

  let runways: [Runway]
}

The Airport structure has String properties for its name, along with three-letter IATA and four-letter ICAO airport codes. The Runway type specifies a direction and distance, as well as a surface defined by a nested Surface enumeration. A set of coordinates are stored in a [Double] array, though we could also define a custom type for that, too.

By conforming to Codable in its declaration and having stored properties with types that conform to Codable, Swift automatically synthesizes the implementation for the init(from:) initializer required by the Decodable protocol (the same goes for the Encodable protocol and its encode(to:) method).

Airport may not check all the boxes in terms of Codable functionality, but it has sufficient complexity to extrapolate from our findings. And for whatever deficiencies this model has, it more than makes up for it by having real-world data to test with. As the saying goes, “Quantity has a quality all its own”.

By scraping Wikipedia’s “List of Airports” article, we were able to create a list of 7361 airports, from which we generated JSON files with 1, 10, 100, 1000, and 10000 objects (some records were duplicated to fill in the gaps for that last one).

{
  "name": "Portland International Airport",
  "iata": "PDX",
  "icao": "KPDX",
  "coordinates": [-122.5975, 45.5886111111111],
  "runways": [
    {
      "distance": 1829,
      "direction": "3/21",
      "surface": "flexible"
    }
    // ...
  ]
}

Taking a look at the relative sizes of these data sets:

Count Size gzip Compressed Size
1 271 Bytes 193 Bytes
10 2.8 KB 703 Bytes
100 33.0 KB 4.7 KB
1000 328.0 KB 44.3 KB
10000 3.2 MB 477.4 KB

Most apps don’t process more than tens of thousands of records at once, so our benchmark should be fairly representative as far as sample sizes go.

Manually Implementing a JSON Initializer

The conventional way to decode models from JSON without Codable is to implement an initializer that takes a [String: Any] type. A hand-rolled implementation of this approach for Airport weighs in at ~30 lines of code (maybe 5 to 10 minutes to write from scratch):

extension Airport {
  public init(json: [String: Any]) {
    guard let name = json["name"] as? String,
      let iata = json["iata"] as? String,
      let icao = json["icao"] as? String,
      let coordinates = json["coordinates"] as? [Double],
      let runways = json["runways"] as? [[String: Any]]
    else {
      fatalError("Cannot initialize Airport from JSON")
    }

    self.name = name
    self.iata = iata
    self.icao = icao
    self.coordinates = coordinates
    self.runways = runways.map { Runway(json: $0) }
  }
}

extension Airport.Runway {
  public init(json: [String: Any]) {
    guard let direction = json["direction"] as? String,
      let distance = json["distance"] as? Int,
      let surfaceRawValue = json["surface"] as? String,
      let surface = Surface(rawValue: surfaceRawValue)
      else {
        fatalError("Cannot initialize Runway from JSON")
    }

    self.direction = direction
    self.distance = distance
    self.surface = surface
  }
}

With effective use of guard statements and the map(_:) method, this isn’t a particularly unappetizing chunk of boilerplate. But it’s hard to compete with the zero additional lines of code required of Codable.

Creating Performance Tests

We use Xcode’s built-in testing framework, XCTest to measure the performance of each implementation.

In the setup for both tests (not shown here), a count is specified and the corresponding data set is loaded. Each test then decodes that data within a closure passed to the measure(_:)

class PerformanceTests: XCTestCase {
  var data: Data
  var count: Int

  func testPerformanceCodable() {
    self.measure {
      let decoder = JSONDecoder()
      let airports = try! decoder.decode([Airport].self, from: data)
      XCTAssertEqual(airports.count, count)
    }
  }

  func testPerformanceJSONSerialization() {
    self.measure {
      let json = try! JSONSerialization.jsonObject(with: data, options: []) as! [[String: Any]]
      let airports = json.map{ Airport(json: $0) }
      XCTAssertEqual(airports.count, count)
    }
  }
}

Benchmarking Execution Time

You can download the Xcode project used to produce these results on GitHub.


Because JSONDecoder uses JSONSerialization under the hood, we should expect the performance characteristics to be similar. And indeed that’s what we see here:

Wall Clock Time (Smaller is Better)
Count JSONSerialization Codable Δ
1 0.5 ms 0.8 ms +0.3 ms
10 1 ms 4 ms +3 ms
100 3 ms 8 ms +5 ms
1000 30 ms 51 ms +21 ms
10000 382 ms 603 ms +221 ms
Swift 4.1, Xcode 9.3 (9E145), iPhone X Simulator
2017 MacBook Pro, 2.9 GHz Intel Core i7, 16 GB 2133 MHz LPDDR3

On average, Codable with JSONDecoder is about half as fast as the equivalent implementation with JSONSerialization.

But does this mean that we shouldn’t use Codable? Probably not.

A 2x speedup factor may seem significant, but measured in absolute time difference, the savings are unlikely to be appreciable under most circumstances — and besides, performance is only one consideration in making a successful app.


If you have a codebase that uses JSONSerialization — whether directly or through a third-party framework — you might add benchmarks to see how Codable performs against your existing implementations. If performance is acceptable, you could then proceed to build new functionality with Codable before eventually transitioning existing code over.

Ultimately, every project is different, and it’s up to you to determine what’s right for you.

Codable isn’t a silver bullet, but it’s good enough that we should consider it to be our new default. Unless you have a specific reason to use JSONSerialization, Codable is an excellent choice for working with data representations.

If you’re interested in learning more, check out The Flight School Guide to Swift Codable.