Latest 0.0.5
Homepage https://github.com/jbelkins/LastMile-iOS
License MIT
Platforms ios 8.0, osx 10.9
Frameworks Foundation
Authors

LastMile Build Status

Robust decoding of model objects, tailored for use with JSON APIs

  • Type-safe access to JSON from Swift
  • Simple, clean syntax for decoding model objects
  • Collects errors detailing abnormalities in the received JSON
  • Built-in decoding for common Swift types

LastMile vs. Swift Codable

LastMile aims exclusively to tackle the problem of building model objects from your API’s JSON easily and consistently. LastMile’s not a substitute for Swift Codable, nor is Swift Codable a substitute for LastMile. Rather, the two can be used side-by-side on the same model objects but for different purposes.

  • Harmonious coexistence. You can use LastMile and Swift Codable independently on the same type. Using LastMile for decoding API responses allows you to use Codable exclusively for internal serialization needs, resulting in cleaner, simpler, more focused code.
  • Flexibility. LastMile reaches through multiple levels of JSON effortlessly. If you need to reach deep into nested JSON to get ["items"][0]["values"][0]["name"], just say so. Swift Decodable excels at decoding data that Swift encoded with Encodable on the same type, but it’s not suited to pick values out of JSON that doesn’t match the structure of your internal models.
  • Resilience. Swift Decodable throws an error and quits decoding when it hits an unexpected value, because such errors are unlikely when decoding data that was encoded internally. LastMile keeps on decoding past the error, returning whatever it can salvage from JSON that has missing or mistyped fields. If you decode an array of 100 values and one is malformed, LastMile will give you an array with the other 99 plus error objects describing why that bad element failed. Codable will throw an error and return no part of the response.
  • Accountability. LastMile makes a note of everything that is unexpectedly missing or in a different type than expected in your API response, and returns a list of everything wrong in the form of a collection of error objects, whether or not a response is returned. The info from error objects can cut hours off of debugging and production downtime by leading you to the problem faster. By contrast, Swift Decodable will give you one error per API response: the one that made it quit decoding.

Using LastMile

(See test file PersonTests.swift in the project to see this demo code in operation.)

Here is a sample model object:

struct Person {
    let id: Int
    let firstName: String
    let lastName: String
    let phoneNumber: String?
    let height: Double?
}

id, firstName, and lastName are required fields. phoneNumber and height are optional.

Here is the JSON we will decode this object from:

{
    "person_id": 8675309,
    "first_name": "Mary",
    "last_name": "Smith",
    "phone_number": "(312) 555-1212",
    "height": "really tall"
}

(note that the value for "height" is unexpectedly a String instead of a number, as expected.)

And here is an APIDecodable extension for Person that will create a new instance:

extension Person: APIDecodable {
    static var idKey: String? { return "person_id" }

    init?(from decoder: APIDecoder) {
        // 1a
        let id =          decoder["person_id"]    --> Int.self
        let firstName =   decoder["first_name"]   --> String.self
        let lastName =    decoder["last_name"]    --> String.self

        // 1b
        let phoneNumber = decoder["phone_number"] --> String?.self
        let height =      decoder["height"]       --> Double?.self

        // 2
        guard decoder.succeeded else { return nil }

        // 3
        self.init(id: id!, firstName: firstName!, lastName: lastName!, phoneNumber: phoneNumber, height: height)
    }
}

There are three steps to this initializer:

1) The decoder is used to create all of the values required to initialize a DemoStruct. Using one or more subscripts on the parser lets you safely access JSON subelements, then you use the --> operator with either a type that is (1a) APIDecodable or (1b) an optional APIDecodable.

2) The initializer fails by returning nil if decoder.succeeded is false. This will happen whenever any non-optional value is not present.

3) Finally, the structure is initialized by calling the memberwise initializer. Required values id, firstName, and lastName may be force-unwrapped safely if decoder.succeeded was true.

Since the value for "height" in the JSON above was unexpectedly a String instead of a number, an error object will be generated describing the problem and its location.

Decoding

To decode a Person from a data object containing JSON received by HTTP:

let decodeResult = APIDataDecoder().decode(data: data, to: Person.self)
print(decodeResult.value ?? "nil")
// prints:
// Person(id: 8675309, firstName: "Mary", lastName: "Smith", phoneNumber: Optional("(312) 555-1212"), height: nil)

decodeResult.errors.forEach { print($0) }
// prints:
// root(Person person_id=8675309) > "height"(Double) : Unexpectedly found a string

Latest podspec

{
    "name": "LastMile",
    "version": "0.0.5",
    "summary": "Decodes data encoded in JSON to Swift objects",
    "description": "Decodes JSON into objects, with simple, elegant syntax, flexible and resilient parsing, and insightful error reporting.",
    "homepage": "https://github.com/jbelkins/LastMile-iOS",
    "license": "MIT",
    "authors": {
        "Josh Elkins": "[email protected]"
    },
    "platforms": {
        "ios": "8.0",
        "osx": "10.9"
    },
    "swift_version": "5.0",
    "source": {
        "git": "https://github.com/jbelkins/LastMile-iOS.git",
        "tag": "v0.0.5"
    },
    "source_files": [
        "LastMile",
        "LastMile/**/*.swift"
    ],
    "frameworks": "Foundation"
}

Pin It on Pinterest

Share This