APIs and Json serialization an example with Swift
Before we delve into the article you can check my github: chrisb5a and the two repos:
PokeApi
PokeApi_NoJsonDumps_Beta
The results are similar to this (the app are different in term of memory usage, number of network calls, “swiftness”, bugs etc)
In this article we will make a network request to the Pokemon API. But what is an API?
It stands for Application Programming Interface. The API will set ways (protocols, definitions…) for two or more programs, possibly on different computers, to communicate with each other. For example, it will explain how program 1 can access data that are available on program 2. A more concrete example could be an online business that holds a customer payment info. The business would need to share this information with a bank, following sets of protocols as far as providing the customer’s name, pay amount, date, identifier of the business etc so the bank would process a payment. The business does not have a direct access to the bank payment software, and ideally the bank does not “own” (or have access to) the customer information available with the business book. Instead the business shares the info to an interface that will “trigger” the bank’s software to process the payment. The business owner will need to be explained how to share his information. He would not know what is going on within bank’s software.
Let us look at the Pokemon API from the URL: “https://pokeapi.co/”
As you can see, the API claims to serve over 250,000,000 calls each month! I would believe that for it to be, it would have to be complex and broad in order to serve every need. The user would need to read over extensive documentation to understand the API. That in itself can be a lot of work and can even require writing codes to test what is going, See what happens with the data. My github repo: PokeApi_NoJsonDumps_Beta is one that use the Pokemon API more than once to retrieve info, images etc and display them on the screen. It has an MVC architecture. Here we work on a simple case.
In what format is the information or data transmitted? We use JSON, that stands for JavaScript Object Notation. It is a language independent standard text based data format that was initially derived from javascript and later extended to most programming languages. It is easy to read and write by a human. In most cases, machines parse the data easily. The syntax rules for creating JSON data are relatively simple:
- Data is in name/value pairs
- Data is separated by commas
- Curly braces hold objects
- Square brackets hold arrays
What is serialization?
In our context, in order to be stored or transmitted, data (data structures or object states) will need to be translated into a storable or transmittable structure. Data objects will be converted to byte strings.
By serialization we convert an object (a variable for example) into a string and when we deserialize we make the string an object.
Our Example.
- Figure out the urls
The main page of the API information shows the resources for the pokemon ditto. The direct link is:“https://pokeapi.co/api/v2/pokemon/ditto”
For each pokemon info, we would need the prefix “https://pokeapi.co/api/v2/pokemon/” and the suffix would include the name of the pokemon… Having the suffix as the name of the pokemon would make it deliberately complicated to retrieve all the pokemon info. It would even be complicated for the writer to make the API. After a bit of reading and searches, one can figure out the prefix would stay the same but the suffix would be something simpler, a number.
2. Network calls
If we want the info of a pokemon other than ditto we would need a url of this sort:
- “https://pokeapi.co/api/v2/pokemon/" for prefix
- number + “/” for suffix
Our custom url is:
“https://pokeapi.co/api/v2/pokemon/" + “\(val+1)” + “/”
that way by changing val or incrementing val we obtain a new pokemon info.
We just create a project with a file containing the network manager class with the following codes:
class NetworkManager {
let session: URLSession
init(session: URLSession = URLSession.shared) {
self.session = session
}
func fetchPageData_int(val: Int, completion: @escaping (Result<Pokemon, Error>) -> Void) {
let urlStr = "https://pokeapi.co/api/v2/pokemon/" + "\(val+1)" + "/"
guard let url = URL(string: urlStr) else {
// completion error
return
}
self.session.dataTask(with: url) { data, response, error in
// Do error handling
guard let data = data else {
// return error
return
}
do {
let page = try JSONDecoder().decode(Pokemon.self, from: data)
completion(.success(page))
} catch {
completion(.failure(error))
}
}.resume()
}
}
What are the codes for?
- We have a URLsession, to make it simple it is a SWIFT class that provides an API for downloading data from the internet.
- We have the function fetchPageData_int to fetch data based on the custom url
- Once we download our data, they are decoded with the JSONDecoder class which will do the serialization and deserialization when necessary.
JSONDecoder().decode(Pokemon.self, from: data)
We also have few completion blocks in fetchPageData_int to ensure everything executes in the order we want. We also create a NetworkError file with the following codes:
enum NetworkError: Error {
case badData
case badURL
case other(Error)
}
That simple ?…
Not so. The JSONDecoder itself will work only if it know what to decode. We will instruct it. The JSONDecoder needs to know how the data will look like, otherwise it will not understand.
The Model
We make a model file that will tell us the structure of the data. We can make our own, however there are a few websites that will make the model for you. One of them is: https://app.quicktype.io/ . It will generate the structs, classes and codes we need. We can just paste this as the model file script.
And we have the following:
struct Pokemon: Codable {
let abilities: [Ability]
let baseExperience: Int
let forms: [Species]
let gameIndices: [GameIndex]
let height: Int
let heldItems: [HeldItem]
let id: Int
let isDefault: Bool
let locationAreaEncounters: String
let moves: [Move]
let name: String
let order: Int
let pastTypes: [JSONAny]
let species: Species
let sprites: Sprites
let stats: [Stat]
let types: [TypeElement]
let weight: Int
enum CodingKeys: String, CodingKey {
case abilities
case baseExperience = "base_experience"
case forms
case gameIndices = "game_indices"
case height
case heldItems = "held_items"
case id
case isDefault = "is_default"
case locationAreaEncounters = "location_area_encounters"
case moves, name, order
case pastTypes = "past_types"
case species, sprites, stats, types, weight
}
}
// MARK: - Ability
struct Ability: Codable {
let ability: Species
let isHidden: Bool
let slot: Int
enum CodingKeys: String, CodingKey {
case ability
case isHidden = "is_hidden"
case slot
}
}
// MARK: - Species
struct Species: Codable {
let name: String
let url: String
}
// MARK: - GameIndex
struct GameIndex: Codable {
let gameIndex: Int
let version: Species
enum CodingKeys: String, CodingKey {
case gameIndex = "game_index"
case version
}
}
// MARK: - HeldItem
struct HeldItem: Codable {
let item: Species
let versionDetails: [VersionDetail]
enum CodingKeys: String, CodingKey {
case item
case versionDetails = "version_details"
}
}
// MARK: - VersionDetail
struct VersionDetail: Codable {
let rarity: Int
let version: Species
}
// MARK: - Move
struct Move: Codable {
let move: Species
let versionGroupDetails: [VersionGroupDetail]
enum CodingKeys: String, CodingKey {
case move
case versionGroupDetails = "version_group_details"
}
}
// MARK: - VersionGroupDetail
struct VersionGroupDetail: Codable {
let levelLearnedAt: Int
let moveLearnMethod, versionGroup: Species
enum CodingKeys: String, CodingKey {
case levelLearnedAt = "level_learned_at"
case moveLearnMethod = "move_learn_method"
case versionGroup = "version_group"
}
}
// MARK: - GenerationV
struct GenerationV: Codable {
let blackWhite: Sprites
enum CodingKeys: String, CodingKey {
case blackWhite = "black-white"
}
}
// MARK: - GenerationIv
struct GenerationIv: Codable {
let diamondPearl, heartgoldSoulsilver, platinum: Sprites
enum CodingKeys: String, CodingKey {
case diamondPearl = "diamond-pearl"
case heartgoldSoulsilver = "heartgold-soulsilver"
case platinum
}
}
// MARK: - Versions
struct Versions: Codable {
let generationI: GenerationI
let generationIi: GenerationIi
let generationIii: GenerationIii
let generationIv: GenerationIv
let generationV: GenerationV
let generationVi: [String: Home]
let generationVii: GenerationVii
let generationViii: GenerationViii
enum CodingKeys: String, CodingKey {
case generationI = "generation-i"
case generationIi = "generation-ii"
case generationIii = "generation-iii"
case generationIv = "generation-iv"
case generationV = "generation-v"
case generationVi = "generation-vi"
case generationVii = "generation-vii"
case generationViii = "generation-viii"
}
}
// MARK: - Sprites
class Sprites: Codable {
let backDefault: String
let backFemale: JSONNull?
let backShiny: String
let backShinyFemale: JSONNull?
let frontDefault: String
let frontFemale: JSONNull?
let frontShiny: String
let frontShinyFemale: JSONNull?
let other: Other?
let versions: Versions?
let animated: Sprites?
enum CodingKeys: String, CodingKey {
case backDefault = "back_default"
case backFemale = "back_female"
case backShiny = "back_shiny"
case backShinyFemale = "back_shiny_female"
case frontDefault = "front_default"
case frontFemale = "front_female"
case frontShiny = "front_shiny"
case frontShinyFemale = "front_shiny_female"
case other, versions, animated
}
init(backDefault: String, backFemale: JSONNull?, backShiny: String, backShinyFemale: JSONNull?, frontDefault: String, frontFemale: JSONNull?, frontShiny: String, frontShinyFemale: JSONNull?, other: Other?, versions: Versions?, animated: Sprites?) {
self.backDefault = backDefault
self.backFemale = backFemale
self.backShiny = backShiny
self.backShinyFemale = backShinyFemale
self.frontDefault = frontDefault
self.frontFemale = frontFemale
self.frontShiny = frontShiny
self.frontShinyFemale = frontShinyFemale
self.other = other
self.versions = versions
self.animated = animated
}
}
// MARK: - GenerationI
struct GenerationI: Codable {
let redBlue, yellow: RedBlue
enum CodingKeys: String, CodingKey {
case redBlue = "red-blue"
case yellow
}
}
// MARK: - RedBlue
struct RedBlue: Codable {
let backDefault, backGray, backTransparent, frontDefault: String
let frontGray, frontTransparent: String
enum CodingKeys: String, CodingKey {
case backDefault = "back_default"
case backGray = "back_gray"
case backTransparent = "back_transparent"
case frontDefault = "front_default"
case frontGray = "front_gray"
case frontTransparent = "front_transparent"
}
}
// MARK: - GenerationIi
struct GenerationIi: Codable {
let crystal: Crystal
let gold, silver: Gold
}
// MARK: - Crystal
struct Crystal: Codable {
let backDefault, backShiny, backShinyTransparent, backTransparent: String
let frontDefault, frontShiny, frontShinyTransparent, frontTransparent: String
enum CodingKeys: String, CodingKey {
case backDefault = "back_default"
case backShiny = "back_shiny"
case backShinyTransparent = "back_shiny_transparent"
case backTransparent = "back_transparent"
case frontDefault = "front_default"
case frontShiny = "front_shiny"
case frontShinyTransparent = "front_shiny_transparent"
case frontTransparent = "front_transparent"
}
}
// MARK: - Gold
struct Gold: Codable {
let backDefault, backShiny, frontDefault, frontShiny: String
let frontTransparent: String?
enum CodingKeys: String, CodingKey {
case backDefault = "back_default"
case backShiny = "back_shiny"
case frontDefault = "front_default"
case frontShiny = "front_shiny"
case frontTransparent = "front_transparent"
}
}
// MARK: - GenerationIii
struct GenerationIii: Codable {
let emerald: Emerald
let fireredLeafgreen, rubySapphire: Gold
enum CodingKeys: String, CodingKey {
case emerald
case fireredLeafgreen = "firered-leafgreen"
case rubySapphire = "ruby-sapphire"
}
}
// MARK: - Emerald
struct Emerald: Codable {
let frontDefault, frontShiny: String
enum CodingKeys: String, CodingKey {
case frontDefault = "front_default"
case frontShiny = "front_shiny"
}
}
// MARK: - Home
struct Home: Codable {
let frontDefault: String
let frontFemale: JSONNull?
let frontShiny: String
let frontShinyFemale: JSONNull?
enum CodingKeys: String, CodingKey {
case frontDefault = "front_default"
case frontFemale = "front_female"
case frontShiny = "front_shiny"
case frontShinyFemale = "front_shiny_female"
}
}
// MARK: - GenerationVii
struct GenerationVii: Codable {
let icons: DreamWorld
let ultraSunUltraMoon: Home
enum CodingKeys: String, CodingKey {
case icons
case ultraSunUltraMoon = "ultra-sun-ultra-moon"
}
}
// MARK: - DreamWorld
struct DreamWorld: Codable {
let frontDefault: String
let frontFemale: JSONNull?
enum CodingKeys: String, CodingKey {
case frontDefault = "front_default"
case frontFemale = "front_female"
}
}
// MARK: - GenerationViii
struct GenerationViii: Codable {
let icons: DreamWorld
}
// MARK: - Other
struct Other: Codable {
let dreamWorld: DreamWorld
let home: Home
let officialArtwork: OfficialArtwork
enum CodingKeys: String, CodingKey {
case dreamWorld = "dream_world"
case home
case officialArtwork = "official-artwork"
}
}
// MARK: - OfficialArtwork
struct OfficialArtwork: Codable {
let frontDefault: String
enum CodingKeys: String, CodingKey {
case frontDefault = "front_default"
}
}
// MARK: - Stat
struct Stat: Codable {
let baseStat, effort: Int
let stat: Species
enum CodingKeys: String, CodingKey {
case baseStat = "base_stat"
case effort, stat
}
}
// MARK: - TypeElement
struct TypeElement: Codable {
let slot: Int
let type: Species
}
// MARK: - Encode/decode helpers
class JSONNull: Codable, Hashable {
public static func == (lhs: JSONNull, rhs: JSONNull) -> Bool {
return true
}
public var hashValue: Int {
return 0
}
public init() {}
public required init(from decoder: Decoder) throws {
let container = try decoder.singleValueContainer()
if !container.decodeNil() {
throw DecodingError.typeMismatch(JSONNull.self, DecodingError.Context(codingPath: decoder.codingPath, debugDescription: "Wrong type for JSONNull"))
}
}
public func encode(to encoder: Encoder) throws {
var container = encoder.singleValueContainer()
try container.encodeNil()
}
}
class JSONCodingKey: CodingKey {
let key: String
required init?(intValue: Int) {
return nil
}
required init?(stringValue: String) {
key = stringValue
}
var intValue: Int? {
return nil
}
var stringValue: String {
return key
}
}
class JSONAny: Codable {
let value: Any
static func decodingError(forCodingPath codingPath: [CodingKey]) -> DecodingError {
let context = DecodingError.Context(codingPath: codingPath, debugDescription: "Cannot decode JSONAny")
return DecodingError.typeMismatch(JSONAny.self, context)
}
static func encodingError(forValue value: Any, codingPath: [CodingKey]) -> EncodingError {
let context = EncodingError.Context(codingPath: codingPath, debugDescription: "Cannot encode JSONAny")
return EncodingError.invalidValue(value, context)
}
static func decode(from container: SingleValueDecodingContainer) throws -> Any {
if let value = try? container.decode(Bool.self) {
return value
}
if let value = try? container.decode(Int64.self) {
return value
}
if let value = try? container.decode(Double.self) {
return value
}
if let value = try? container.decode(String.self) {
return value
}
if container.decodeNil() {
return JSONNull()
}
throw decodingError(forCodingPath: container.codingPath)
}
static func decode(from container: inout UnkeyedDecodingContainer) throws -> Any {
if let value = try? container.decode(Bool.self) {
return value
}
if let value = try? container.decode(Int64.self) {
return value
}
if let value = try? container.decode(Double.self) {
return value
}
if let value = try? container.decode(String.self) {
return value
}
if let value = try? container.decodeNil() {
if value {
return JSONNull()
}
}
if var container = try? container.nestedUnkeyedContainer() {
return try decodeArray(from: &container)
}
if var container = try? container.nestedContainer(keyedBy: JSONCodingKey.self) {
return try decodeDictionary(from: &container)
}
throw decodingError(forCodingPath: container.codingPath)
}
static func decode(from container: inout KeyedDecodingContainer<JSONCodingKey>, forKey key: JSONCodingKey) throws -> Any {
if let value = try? container.decode(Bool.self, forKey: key) {
return value
}
if let value = try? container.decode(Int64.self, forKey: key) {
return value
}
if let value = try? container.decode(Double.self, forKey: key) {
return value
}
if let value = try? container.decode(String.self, forKey: key) {
return value
}
if let value = try? container.decodeNil(forKey: key) {
if value {
return JSONNull()
}
}
if var container = try? container.nestedUnkeyedContainer(forKey: key) {
return try decodeArray(from: &container)
}
if var container = try? container.nestedContainer(keyedBy: JSONCodingKey.self, forKey: key) {
return try decodeDictionary(from: &container)
}
throw decodingError(forCodingPath: container.codingPath)
}
static func decodeArray(from container: inout UnkeyedDecodingContainer) throws -> [Any] {
var arr: [Any] = []
while !container.isAtEnd {
let value = try decode(from: &container)
arr.append(value)
}
return arr
}
static func decodeDictionary(from container: inout KeyedDecodingContainer<JSONCodingKey>) throws -> [String: Any] {
var dict = [String: Any]()
for key in container.allKeys {
let value = try decode(from: &container, forKey: key)
dict[key.stringValue] = value
}
return dict
}
static func encode(to container: inout UnkeyedEncodingContainer, array: [Any]) throws {
for value in array {
if let value = value as? Bool {
try container.encode(value)
} else if let value = value as? Int64 {
try container.encode(value)
} else if let value = value as? Double {
try container.encode(value)
} else if let value = value as? String {
try container.encode(value)
} else if value is JSONNull {
try container.encodeNil()
} else if let value = value as? [Any] {
var container = container.nestedUnkeyedContainer()
try encode(to: &container, array: value)
} else if let value = value as? [String: Any] {
var container = container.nestedContainer(keyedBy: JSONCodingKey.self)
try encode(to: &container, dictionary: value)
} else {
throw encodingError(forValue: value, codingPath: container.codingPath)
}
}
}
static func encode(to container: inout KeyedEncodingContainer<JSONCodingKey>, dictionary: [String: Any]) throws {
for (key, value) in dictionary {
let key = JSONCodingKey(stringValue: key)!
if let value = value as? Bool {
try container.encode(value, forKey: key)
} else if let value = value as? Int64 {
try container.encode(value, forKey: key)
} else if let value = value as? Double {
try container.encode(value, forKey: key)
} else if let value = value as? String {
try container.encode(value, forKey: key)
} else if value is JSONNull {
try container.encodeNil(forKey: key)
} else if let value = value as? [Any] {
var container = container.nestedUnkeyedContainer(forKey: key)
try encode(to: &container, array: value)
} else if let value = value as? [String: Any] {
var container = container.nestedContainer(keyedBy: JSONCodingKey.self, forKey: key)
try encode(to: &container, dictionary: value)
} else {
throw encodingError(forValue: value, codingPath: container.codingPath)
}
}
}
static func encode(to container: inout SingleValueEncodingContainer, value: Any) throws {
if let value = value as? Bool {
try container.encode(value)
} else if let value = value as? Int64 {
try container.encode(value)
} else if let value = value as? Double {
try container.encode(value)
} else if let value = value as? String {
try container.encode(value)
} else if value is JSONNull {
try container.encodeNil()
} else {
throw encodingError(forValue: value, codingPath: container.codingPath)
}
}
public required init(from decoder: Decoder) throws {
if var arrayContainer = try? decoder.unkeyedContainer() {
self.value = try JSONAny.decodeArray(from: &arrayContainer)
} else if var container = try? decoder.container(keyedBy: JSONCodingKey.self) {
self.value = try JSONAny.decodeDictionary(from: &container)
} else {
let container = try decoder.singleValueContainer()
self.value = try JSONAny.decode(from: container)
}
}
public func encode(to encoder: Encoder) throws {
if let arr = self.value as? [Any] {
var container = encoder.unkeyedContainer()
try JSONAny.encode(to: &container, array: arr)
} else if let dict = self.value as? [String: Any] {
var container = encoder.container(keyedBy: JSONCodingKey.self)
try JSONAny.encode(to: &container, dictionary: dict)
} else {
var container = encoder.singleValueContainer()
try JSONAny.encode(to: &container, value: self.value)
}
}
}
There seem to be a lot of gibberish and the codes are not very concise but it will do the work and relatively quickly compared to us writing our own codes.
In order to obtain the codes we have copied and pasted the content of “https://pokeapi.co/ditto” into quictype.
Now what is going on with the model?
Basically, the model says how the data are inbricked into each other. Do we have a dictionary? What is in the dictionary? What are the keys etc. What are in the structs? Booleans, Integers, string, Do we have arrays, classes etc.
The model declares the variables that will be downloaded after a network call and how they are structured.
The ViewController
In our ViewController we have the following lines of codes:
import UIKit
class ViewController: UIViewController {
var data_: [Pokemon] = []
let network: NetworkManager = NetworkManager()
var PokePics: [UIImage] = []
var PokePics1: [Int] = []
var currentPage = -1
var names : [String] = []
override func viewDidLoad() {
super.viewDidLoad()
self.requestPage()
// Do any additional setup after loading the view.
}
private func requestPage(){
self.network.fetchPageData_int(val: self.currentPage+1){
result in switch result{
case .success (let page):
print("\(page)")
DispatchQueue.main.async{
//print(page, "Heeeeeerrrrreee")
let pokemon = page
self.data_ = [page]
let moves = pokemon.moves.compactMap { $0.move.name }.reduce("") { partialResult, move in
return partialResult + move + "\n"
}
let alert = UIAlertController(title: "\(pokemon.name)'s Moves", message: "\(moves)", preferredStyle: .alert)
alert.addAction(UIAlertAction(title: NSLocalizedString("OK", comment: "Default action"), style: .default, handler: { _ in
NSLog("The \"OK\" alert occured.")
}))
self.present(alert, animated: true, completion: nil)
}
case .failure(let error):
print(error)
}
}
}
}
In this file, we decide to make a network request with the NetworkManager() class. We call the function fetchPageData_int with a completion to print the decoded page and an alert in the DispatchQueue to display the moves of the pokemon that corresponds to the page number currentPage + 1.
This is the result:
This is it! Thank you for reading.