Skip to content

Instantly share code, notes, and snippets.

Embed
What would you like to do?
You don't need a library for: Parsing JSON
/// Provides methods to decode values from a JSON dictionary that throw JSONParseError based on type
extension Dictionary where Key: ExpressibleByStringLiteral, Value: JSON {
/// decode the value for `key` where `T` is not optional
func decode<T>(_ key: Key) throws -> T {
return try decodeNonOptionalValue(for: key)
}
/// decode the value for `key` where `T` can be optional. Being absent or NSNull is allowed
func decode<T: ExpressibleByNilLiteral>(_ key: Key) throws -> T {
let value = self[key]
if value == nil {
return nil
} else {
return try decodeNonOptionalValue(for: key)
}
}
/// performs the work of decoding the value for `key` where `T` is not optional
func decodeNonOptionalValue<T>(for key: Key) throws -> T {
switch self[key] {
case let value as T:
return value
case nil:
throw JSONParseError.missingKey(String(describing: key))
case .some:
throw JSONParseError.valueTypeMismatch(String(describing: key))
}
}
}
enum JSONParseError: Error {
/// Missing a required key while decoding
case missingKey(String)
/// Type mismatch on a value while decoding
case valueTypeMismatch(String)
/// Expecting a JSONDictionary but got something else (probably JSONArray or JSON)
case expectedJSONDictionary
/// Expecting a JSONArray but got something else (probably JSONDictionary or JSON)
case expectedJSONArray
}
// Example class conforming to our `ExpressibleByJSONDictionary` protocol
class Pokemon: ExpressibleByJSONDictionary {
let name: String
let id: Int
let trainerId: Int?
required init(dictionary: JSONDictionary) throws {
// The decode methods will throw the correct error if the value for the given key
// is either missing when required or the wrong type
self.name = try dictionary.decode("name")
self.id = try dictionary.decode("id")
self.trainerId = try dictionary.decode("trainerId")
}
}
// Let's create a Pokemon given a dictionary of JSON
do {
let pikachu = try Pokemon(dictionary: ["name": "Pikachu" as NSString,
"id": 25 as NSNumber,
"trainerId": 0 as NSNumber])
} catch {
print(error)
}
// No problems!
// We can leave out the "trainerId" key value pair because Pokemon's trainerId property is Optional
do {
let pikachu = try Pokemon(dictionary: ["name": "Pikachu" as NSString,
"id": 25 as NSNumber])
} catch {
print(error)
}
// No problems!
// But if we leave out the required "id" field
do {
let pikachu = try Pokemon(dictionary: ["name": "Pikachu" as NSString])
} catch {
print(error)
}
// Missing key "id" is thrown
// If we use the wrong type for a property, say a string for id
do {
let pikachu = try Pokemon(dictionary: ["name": "Pikachu" as NSString,
"id": "25" as NSString,
"trainerId": 0 as NSNumber])
} catch {
print(error)
}
// value type mismatch "id" is thrown
/// Extensions that provide bulk decoding and initing
extension ExpressibleByJSONDictionary {
/// Creates an instance of `Self` where `data` contains a valid JSON representation of `Self`
static func make(data: Data) throws -> Self {
guard let object = try JSONSerialization.jsonObject(with: data, options: []) as? JSONDictionary else {
throw JSONParseError.expectedJSONDictionary
}
return try Self.init(dictionary: object)
}
/// Creates instances of `Self` where `data` contains a valid JSON representation of an array of `Self`
static func make(data: Data) throws -> [Self] {
guard let object = try JSONSerialization.jsonObject(with: data, options: []) as? [JSONDictionary] else {
throw JSONParseError.expectedJSONArray
}
return try make(array: object)
}
/// Creates instances of `Self` where `array` contains valid JSON representations of `Self`
static func make(array: [JSONDictionary]) throws -> [Self] {
return try array.map { try Self.init(dictionary: $0) }
}
}
// Sometimes we may want to import our objects in bulk from a JSON array
let pairOfPokemon: [JSONDictionary] = [
["name": "Pikachu" as NSString, "id": 25 as NSNumber],
["name": "Bulbasaur" as NSString, "id": 1 as NSNumber],
]
// Using our nice extensions it's really easy
do {
let pokemon = try Pokemon.make(array: pairOfPokemon)
} catch {
print(error)
}
// No problem!
// When working with URLSession we're getting back Data that we want to decode. Again
// using our nice extensions on ExpressibleByJSONDictionary, we don't have to do the
// JSON deserialization work ourselves
do {
let pairOfPokemonData = try JSONSerialization.data(withJSONObject: pairOfPokemon, options: [])
let pokemon: [Pokemon] = try Pokemon.make(data: pairOfPokemonData)
} catch {
print(error)
}
// No problem!
protocol ExpressibleByJSONDictionary {
/// Init with JSONDictionary, throwing an error (likely JSONParseError) if the contents are invalid
init(dictionary: JSONDictionary) throws
}
typealias JSON = AnyObject
typealias JSONArray = [JSON]
typealias JSONDictionary = [String: JSON]
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment