Last time we looked at how Swift values get encoded to JSON thanks to JSONEncoder
+ the Encodable
and Encoder
protocols.
Now let’s go the other way: how does a JSON string like "[0, 1, 2]"
make it back to a Swift array of integers?
As you might expect, the overall decoding process works like the reverse of the encoding process:
- Use
JSONSerialization
to parse the JSON data toAny
- Feed the
Any
instance to_JSONDecoder
- Dispatch to each type’s
Decodable
initializer to reconstruct a series of Swift instances
JSON Deserialization
Let’s start with a JSON string representing an array of integers and turn that into some Data
:
let jsonString = "[0, 1, 2]"
let jsonData = jsonString.data(using: .utf8)!
Now we can run that through JSONDecoder
:
let decoder = JSONDecoder()
let dRes = try! decoder.decode([Int].self, from: jsonData)
Note that the decode()
method needs to know the type of the result instance.
In contrast to JSONSerialization
which understands JSON types such as strings and numbers and arrays and dictionaries, JSONDecoder
instantiates anything that’s Decodable
— that’s how you can decode JSON directly to your own types.
Decoder
As with encoding, JSONDecoder
is the class with a friendly interface and handy decode()
method called above. What does that method look like?
// class JSONDecoder
open func decode<T : Decodable>(_ type: T.Type, from data: Data) throws -> T {
let topLevel: Any
do {
topLevel = try JSONSerialization.jsonObject(with: data)
} catch {
throw DecodingError.dataCorrupted(DecodingError.Context(
codingPath: [],
debugDescription: "The given data was not valid JSON.",
underlyingError: error)
)
}
We’re relying on JSONSerialization
to do the heavy lifting. We don’t know if the resulting JSON will match our expected type
yet, since we’re getting back a general Any
for topLevel
inside the do
block.
So we have our topLevel
, presumably a top-level dictionary or array filled with other NSObject
-based instances. How do those make it over to Swift instances?
// decode() continued
let decoder = _JSONDecoder(referencing: topLevel, options: self.options)
return try T(from: decoder)
}
First we spin up a _JSONDecoder
, the private class that conforms to Decoder
and contains JSON-specific logic.
Remember T
is the generic parameter for the Swift equivalent to topLevel
; in this case, it’s [Int]
aka Array<Int>
so we can decode our array [0, 1, 2]
.
We’ll construct the final return value by using T
’s initializer init(from: Decoder)
, defined as part of the Decodable
protocol.
As with encoding, the flow seems backwards to how my brain works: rather than have the decoder do the work and return an instance of type T
, we’re using an initializer on T
to construct itself, with the decoder passed along as a parameter.
That’s the high-level view, but there are still a few pieces to dig into: _JSONDecoder
, the Decodable
protocol, and how our array and integers will get initialized.
JSON Decoder
We instantiated a _JSONDecoder
and passed it into the top-level container’s initializer. What’s inside the _JSONDecoder
?
// class _JSONDecoder : Decoder
fileprivate init(referencing container: Any,
at codingPath: [CodingKey] = [],
options: JSONDecoder._Options) {
self.storage = _JSONDecodingStorage()
self.storage.push(container: container)
self.codingPath = codingPath
self.options = options
}
The decoder will hold on to the top-level container in self.storage
. In our case, this will be an NSArray
holding NSNumber
objects.
Decoders conform to the Decoder
protocol, with the following definition:
/// A type that can decode values from a native format into in-memory representations.
public protocol Decoder {
func container<Key>(keyedBy type: Key.Type) throws -> KeyedDecodingContainer<Key>
func unkeyedContainer() throws -> UnkeyedDecodingContainer
func singleValueContainer() throws -> SingleValueDecodingContainer
}
If you remember from encoding, the array turned into an unkeyed container, and each item inside the array was a single value container.
We’ll expect the same thing here: the outer array will use unkeyedContainer()
, then we’ll loop over each item in the array and call singleValueContainer()
for each integer.
Array Reconstruction
Let’s return to the T(from: decoder)
initializer, which in this case expands out to Array<Int>(from: decoder)
. The init(from:)
initializer comes from the Decodable
protocol, which both Swift arrays and integers conform to.
/// A type that can decode itself from an external representation.
public protocol Decodable {
init(from decoder: Decoder) throws
}
Finally, we can start building the array:
extension Array : Decodable /* where Element : Decodable */ {
public init(from decoder: Decoder) throws {
// Initialize self here so we can get type(of: self).
self.init()
assertTypeIsDecodable(Element.self, in: type(of: self))
let metaType = (Element.self as! Decodable.Type)
var container = try decoder.unkeyedContainer()
Some standard initialization stuff, and two important values:
metaType
is the type stored in the array,Int
in our case. This type has to itself beDecodable
.container
is the unkeyed container, an array of anything[Any]
.
Integer Value Reconstruction
Now that we have our container with the elements to decode and we know the type of the elements, it’s time to loop:
// still inside Array.init(from decoder: Decoder)
while !container.isAtEnd {
let subdecoder = try container.superDecoder()
let element = try metaType.init(from: subdecoder)
self.append(element as! Element)
}
The variable is named subdecoder
but comes from a method called superDecoder()
— not confusing at all, right? 🤔
Since we could have an array of arrays, or array of dictionaries, or other nested containers, superDecoder()
returns a fresh _JSONDecoder
instance that wraps the next value in the container.
In our case, that means a _JSONDecoder
for a single Int
. We decode the integer into element
on the second line inside the loop. Remember, metaType
is a decodable Int
so think of the second line as reading like this, sort of:
let element = try Int(from: subdecoder)
Finally, we’ve reached the Int
initializer:
// from Codable.swift
extension Int : Codable {
public init(from decoder: Decoder) throws {
self = try decoder.singleValueContainer().decode(Int.self)
}
}
Remember, the decoder here is the sub-decoder for just a single value. What’s going on in the decode()
call?
// from JSONEncoder.swift
extension _JSONDecoder : SingleValueDecodingContainer {
public func decode(_ type: Int.Type) throws -> Int {
try expectNonNull(Int.self)
return try self.unbox(self.storage.topContainer, as: Int.self)!
}
}
OK, now we’re calling through to an unbox()
function. We’re dealing with NSArray
and NSNumber
instances, so we want to unbox the NSNumber
back to a plain Int
.
fileprivate func unbox(_ value: Any, as type: Int.Type) throws -> Int? {
guard !(value is NSNull) else { return nil }
guard let number = value as? NSNumber else {
throw DecodingError._typeMismatch(at: self.codingPath,
expectation: type,
reality: value)
}
let int = number.intValue
guard NSNumber(value: int) == number else {
throw DecodingError.dataCorrupted(DecodingError.Context(
codingPath: self.codingPath,
debugDescription: "Parsed JSON number <\(number)> does not fit in \(type)."))
}
return int
}
First, we have two guard
checks to make sure the value isn’t NSNull
and that it is indeed an NSNumber
.
Next is the code we’ve been waiting for: let int = number.intValue
🎉
Then there’s a final check to make sure we didn’t overflow by turning the integer back to an NSNumber
and making sure that value matches what we started with.
Now pop your mental stack all the way back to the array initializer: we were inside a while
loop, getting subdecoders, instantiating objects, and then:
// Array initializer, inside container while loop
self.append(element as! Element)
The final thing to do is append the integer to self
— we’re inside an array initializer here so self
is a Swift array.
What’s with the element as! Element
cast? The element
variable is of type Decodable
so we need to cast it to Element
(aka Int
in our example) before adding it to the array.
The Closing Brace
We made it! All the way from a JSON string, through decodable and decoders, to unkeyed containers, to loops, to individual values.
I like the split in logic here between decodables and decoders:
- Decodables only need
init(from:)
initializers, which are relatively simple. - Decoders contain the logic, state, and storage to do the decoding work.
You get two-way flexibility: you can add your own codable type and have it work with JSON; or, you could add your own encoding format such as XML and as long as you follow the protocol conformance, you can support all codable types.
One price to pay is lots of repeated code in decoders; if you look at the rest of JSONEncoder.swift, you’ll find 18 unbox()
functions to cover all the integer types, floats, strings, dates, etc.
What would the simplest custom encoder and decoder look like? That’s something I’m curious about and will be trying out next!
}