Robust decoding of model objects, tailored for use with JSON APIs
LastMile aims to tackle these important problems in decoding API JSON responses, which are weaknesses of decoding with Swift Codable:
Continue to use Codable for internal serialization/deserialization, the role at which it is best suited, and use LastMile to create models from JSON received over the API.
LastMile decoding is different from Swift Codable decoding in these important ways:
Things LastMile gives you that Swift Codable doesn't:
response["items"][0]["values"][0]["name"]
, for instance, just say so. Swift Decodable isn't suited to pick values out of JSON that doesn't match the structure of your internal models.Add it to your Podfile
:
pod 'LastMile'
Add it to your Package.swift
or to your Xcode project's Package Dependencies:
.package("LastMile", url: "https://github.com/jbelkins/LastMile-iOS", version: "1.0.0")
Note that this code is also demo'd in the Swift Playground included in this project.
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",
"contact_info": {
"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["contact_info"]["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 decoder 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 since decoder.succeeded
was true.
(The static var idKey: String?
declaration helps to identify the specific data record that caused a decoding error.)
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. The error is stored in the decoder
and may be accessed after decoding completes.
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)
To see the errors generated while decoding:
decodeResult.errors.forEach { print($0) }
// prints:
// root(Person person_id=8675309) > "height"(Double) : Unexpectedly found a string
link |
Stars: 0 |
Last commit: 27 weeks ago |
Swiftpack is being maintained by Petr Pavlik | @ptrpavlik | @swiftpackco | API | Analytics