Robust decoding of model objects, tailored for use with JSON APIs
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.
["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.(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:
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
.
The initializer fails by returning nil
if decoder.succeeded
is false. This will happen whenever any non-optional value is not present.
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.
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
link |
Stars: 0 |
Last commit: 3 weeks ago |
Swiftpack is being maintained by Petr Pavlik | @ptrpavlik | @swiftpackco