[swift unboxed]

Safely unboxing the Swift language & standard library


JSON to Swift with Decoder and Decodable

Let’s decode some decodables, from JSON to Swift.

17 July 2017 ∙ Standard Library ∙ written by

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?

JSON decoding magic

As you might expect, the overall decoding process works like the reverse of the encoding process:

  1. Use JSONSerialization to parse the JSON data to Any
  2. Feed the Any instance to _JSONDecoder
  3. 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.

JSON decoding overview

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 be Decodable.
  • 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.

JSON decoding loop for array of 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!

}