SkillAgentSearch skills...

JSONShootout

Compare several Swift JSON mappers.

Install / Use

/learn @bwhiteley/JSONShootout
About this skill

Quality Score

0/100

Supported Platforms

Universal

README

* Note about JSONDecoder in Swift 4 *

To experiment with JSONDecoder in Swift 4, use the swift4 branch. Some of the other implementations were removed from the swift4 branch to get it to compile and run. Check the performance graph near end of this file to see the performance of JSONDecoder

Swift + JSON

Since the first days of Swift, developers have been exploring strategies for dealing with JSON. While some call this "JSON Parsing", with few exceptions most people rely on NSJSONSerialization for the actual parsing. Most of the effort has gone into finding the best way to map JSON objects (dictionaries and arrays) into model objects (structs, classes, enums).

A Convergence of Ideas

<blockquote class="twitter-tweet" data-lang="en"><p lang="en" dir="ltr">&quot;I believe there&#39;s a JSON platonic ideal and all Swift implementations converge towards it&quot;</p>&mdash; (quoted on Twitter, original author unknown) </blockquote>

Many projects have emerged to take on this challenge, employing various approaches and philosophies. It's interesting that several of these projects have taken a very similar approach, ostensibly independent of each other. Here are some examples:

The power of the approach taken by these projects lies in the ability to easily map not only primitive JSON types, but also custom types and objects in a type safe manner.

For example, the goal is to be able to do something like this:

let title:String = try json.value(for: "header.title")
let users:[User] = try json.value(for: "users")

All of the frameworks listed above leverage Swift's powerful type system to handle all of the details.

A Detailed Look

I put together a small project to compare these Swift JSON mappers. Let's look at some objects from a Digital Video Recorder feed:

The Objects

struct Recording {
    let startTs:NSDate?
    let endTs:NSDate?
    let startTsStr:String
    let status:Status // enum
    let recordId:String
    let recGroup:RecGroup // enum
}

struct Program {
    let title:String
    let chanId:String
    let startTime:NSDate
    let endTime:NSDate
    let description:String?
    let subtitle:String?
    let recording:Recording // nested object
    let season:Int?
    let episode:Int?
}

Now let's look at how these objects would be extracted with each of the JSON mappers. We'll ignore proper error handling for this exercise.

Marshal

extension Recording: Unmarshaling {
    public init(object json:MarshaledObject) throws {
        startTs = try? json.value(for: "StartTs")
        endTs = try? json.value(for: "EndTs")
        startTsStr = try json.value(for: "StartTs")
        recordId = try json.value(for: "RecordId")
        status = (try? json.value(for: "Status")) ?? .Unknown
        recGroup = (try? json.value(for: "RecGroup")) ?? .Unknown
    }
}

extension Program: Unmarshaling {
    public init(object json: MarshaledObject) throws {
        title = try json.value(for: "Title")
        chanId = try json.value(for: "Channel.ChanId")
        startTime = try json.value(for: "StartTime")
        endTime = try json.value(for: "EndTime")
        description = try json.value(for: "Description")
        subtitle = try json.value(for: "SubTitle")
        recording = try json.value(for: "Recording")
        season = (try json.value(for: "Season") as String?).flatMap({Int($0)})
        episode = (try json.value(for: "Episode") as String?).flatMap({Int($0)})
    }
}

// Extract an array of Programs
let json = try! NSJSONSerialization.JSONObjectWithData(data, options: []) as! NSDictionary
let programs:[Program] = try json.valueForKey("ProgramList.Programs")

Mapper

extension Recording: Mappable {
    public init(map: Mapper) throws {
        startTs =  map.optionalFrom("StartTs")
        endTs =  map.optionalFrom("EndTs")
        startTsStr = try map.from("StartTs")
        recordId = try map.from("RecordId")
        status = map.optionalFrom("Status") ?? .Unknown
        recGroup = map.optionalFrom("RecGroup") ?? .Unknown
    }
}

extension Program: Mappable {
    public init(map: Mapper) throws {
        title = try map.from("Title")
        chanId = try map.from("Channel.ChanId")
        startTime = try map.from("StartTime")
        endTime = try map.from("EndTime")
        description = try map.from("Description")
        subtitle = try map.from("SubTitle")
        recording = try map.from("Recording")
        season = (try map.from("Season") as String?).flatMap({Int($0)})
        episode = (try map.from("Episode") as String?).flatMap({Int($0)})
    }
}

// Extract an array of Programs
let dict = try! NSJSONSerialization.JSONObjectWithData(data, options: []) as! NSDictionary
let mapper = Mapper(JSON: dict)
let programs:[Program] = try! mapper.from("ProgramList.Programs")

Unbox

extension Recording: Unboxable {
    public init(unboxer: Unboxer) throws {
        startTs = unboxer.unbox(key: "StartTs", formatter:NSDate.ISO8601SecondFormatter)
        endTs = unboxer.unbox(key: "EndTs", formatter:NSDate.ISO8601SecondFormatter)
        startTsStr = try unboxer.unbox(key: "StartTs")
        recordId = try unboxer.unbox(key: "RecordId")
        status = unboxer.unbox(key: "Status") ?? .Unknown
        recGroup = unboxer.unbox(key: "RecGroup") ?? .Unknown
    }
}

extension Program: Unboxable {
    public init(unboxer: Unboxer) throws {
        title = try unboxer.unbox(key: "Title")
        chanId = try unboxer.unbox(keyPath: "Channel.ChanId")
        startTime = try unboxer.unbox(key: "StartTime", formatter:NSDate.ISO8601SecondFormatter)
        endTime = try unboxer.unbox(key: "EndTime", formatter:NSDate.ISO8601SecondFormatter)
        description = unboxer.unbox(key: "Description")
        subtitle = unboxer.unbox(key: "SubTitle")
        recording = try unboxer.unbox(key: "Recording")
        season = unboxer.unbox(key: "Season")
        episode = unboxer.unbox(key: "Episode")
    }
}

// Extract an array of Programs
let dict = try! NSJSONSerialization.JSONObjectWithData(data, options: []) as! UnboxableDictionary
let programs:[Program] = try! unbox(dictionary: dict, atKeyPath: "ProgramList.Programs")

Decodable

extension Recording : Decodable {
    public static func decode(_ json: Any) throws -> Recording {
        return try Recording(
            startTsStr: json => "StartTs",
            status: Status(rawValue: json => "Status") ?? .Unknown,
            recordId: json => "RecordId",
            recGroup: RecGroup(rawValue: json => "RecGroup") ?? .Unknown
        )
    }
}

extension Program: Decodable {
    public static func decode(_ json: Any) throws -> Program {
        return try Program(
            title: json => "Title",
            chanId: json => "Channel" => "ChanId",
            startTime = json => "StartTime",
            endTime = json => "EndTime",
            description: json => "Description",
            subtitle: json => "SubTitle",
            recording: json => "Recording",
            season: Int(json => "Season" as String),
            episode: Int(json => "Episode" as String)
        )
    }
}

// Extract an array of Programs
let dict = try! NSJSONSerialization.JSONObjectWithData(data, options: []) as! NSDictionary
let programs:[Program] = try! dict => "ProgramList" => "Programs"

Gloss

extension Recording: Decodable {
  public init?(json: JSON) {
    self.startTsStr = "StartTs" <~~ json ?? ""
    self.recordId   = "RecordId" <~~ json ?? ""
    self.status     = "Status" <~~ json ?? Status.Unknown
    self.recGroup   = "RecGroup" <~~ json ?? RecGroup.Unknown
  }
}

extension Program: Decodable {
    public init?(json: JSON) {
        title       = "Title" <~~ json ?? ""
        chanId      = "channel.ChanId" <~~ json ?? ""
        description = "Description" <~~ json
        subtitle    = "SubTitle" <~~ json
        season      = "Season" <~~ json
        episode     = "Episode" <~~ json
        recording   = ("Recording" <~~ json)!
    }
}

// Extract an array of Programs
let dict: [String: Any] = try! JSONSerialization.jsonObject(with: data, options: []) as! [String: Any]
let programs: [Program] = "ProgramList.Programs" <~~ dict ?? []

Genome

extension Recording: MappableObject {
    public init(map: Map) throws {
        startTsStr = try map.extract("StartTs")
        status = try map.extract("Status") { Status(rawValue: $0) ?? .Unknown }
        recordId = try map.extract("RecordId")
        recGroup = try map.extract("RecGroup") { RecGroup(rawValue: $0) ?? .Unknown }
    }
    public func sequence(_ map: Map) throws { }
}

extension Program: MappableObject {
    public init(map: Map) throws {
        title = try map.extract("Title") { $0 ?? "" }
        chanId = try map.extract("channel", "ChanId") { $0 ?? "" }
        description = try map.extract("Description")
        subtitle = try map.extract("SubTitle")
        season = try map.extract("Season")
        episode = try map.extract("Episode")
        recording = try Recording(node: try map.extract("Recording"))
    }
    public func sequence(_ map: Map) throws { }
}

// Extract an array of Programs
let json = try! data.makeNode()
let programs: [Program] = try! [Program](node: json["ProgramList", "Programs"]!)

Analysis

You can immediately see the similarities between the three projects. I won't get into the details of how they work here. You can read more about each project, or read Jason Larsen's [three](http://jasonlarsen.me/2015/06/23/no-magic-json.h

View on GitHub
GitHub Stars595
CategoryDevelopment
Updated26d ago
Forks36

Languages

Swift

Security Score

80/100

Audited on Mar 12, 2026

No findings