How To Decode Swift Types That Can’t Be Decoded

The Swift Codable protocols have made serializing / deserializing model objects significantly easier and more robust. One catch that I have run into is that in order to make a type Decodable it needs to be able to add init(from: Decoder) to it. This is fine if you are working with a struct, but there are a couple situations where you just can’t make a class Decodable:

In this post, I’ll explain an option for making decoding these types easier and then build off of this post about updating objects from a Decoder, which also introduces some of the pieces that we’ll be working with, so that is worth a look first.

DecodingFactory

To decode non-Decodable types, we’re going to start by defining a DecodingFactory protocol which is able to create some type of object from a decoder:

protocol DecodingFactory {
    associatedtype Model
    func create(from decoder: Decoder) throws -> Model
}

Here’s an example of a DecodingFactory, one that creates CLLocations:

struct CLLocationFactory: DecodingFactory {
    enum Key: String, CodingKey {
        case latitude
        case longitude
    }

    func create(from decoder: Decoder) throws -> CLLocation {
        let container = try decoder.container(keyedBy: Key.self)
        let latitude = try container.decode(Double.self, forKey: .latitude)
        let longitude = try container.decode(Double.self, forKey: .longitude)
        return CLLocation(latitude: latitude, longitude: longitude)
    }
}

This looks fairly similar to a manually written Decodable class, and should be pretty easy to follow.

To make this useful, we will need to add some conveniences: one for DecodingFormat and one for KeyedDecodingContainer.

DecodingFormat

DecodingFormat is a protocol created in the DecoderUpdatable blog post, which JSONDecoder and PropertyListDecoder are made to conform to.

We can add an extension to this protocol, so that DecodingFactorys can be used just as easily as Decodable types:

extension DecodingFormat {
    func decode<Factory: DecodingFactory>(using factory: Factory, from data: Data) throws -> Factory.Model {
        return try factory.create(from: decoder(for: data))
    }
}

In practice, using the CLLocationFactory it would look like this:

let location = try JSONDecoder().decode(using: CLLocationFactory(), from: data)

and as you’d expect, the compiler is able to infer that this returns a CLLocation.

KeyedDecodingContainer

A KeyedDecodingContainer is the type that you get when calling container(keyedBy:) on a Decoder. Adding a convenience to it will make it easier to decode nested objects using a DecodingFactory:

extension KeyedDecodingContainer {
    func decode<Factory: DecodingFactory>(using factory: Factory, forKey key: K, userInfo: [CodingUserInfoKey : Any] = [:]) throws -> Factory.Model {
        let nestedDecoder = NestedDecoder(from: self, key: key, userInfo: userInfo)
        return try factory.create(from: nestedDecoder)
    }

    func decodeIfPresent<Factory: DecodingFactory>(using factory: Factory, forKey key: Key, userInfo: [CodingUserInfoKey : Any] = [:]) throws -> Factory.Model? {
        guard try contains(key) && !decodeNil(forKey: key) else { return nil }
        return try decode(using: factory, forKey: key, userInfo: userInfo)
    }
}

This uses a NestedDecoder which is also from the DecoderUpdatable post.

Now we are able to to use our factories to decode CLLocations on an object.

struct Place: Decodable {
    let name: String
    let location: CLLocation

    enum Key: String, CodingKey {
        case name
        case location
    }

    init(from decoder: Decoder) throws {
        let container = try decoder.container(keyedBy: Key.self)
        name = try container.decode(String.self, forKey: .name)
        location = try container.decode(using: CLLocationFactory(), forKey: .location)
    }
}

 Managed Objects

We can use DecodingFactory to decode NSManagedObjects as well. With the create(from:) function being an instance method instead of a static method, we are able to pass along other necessary context to the factory as well.
In the case of NSManagedObject, specifically an NSManagedObjectContext.

For this example, we’ll need to add another protocol, ManagedDecodable, for our NSManagedObjects to conform to, which inherits from DecoderUpdatable and also has an entityName.

protocol ManagedDecodable: DecoderUpdatable {
    static var entityName: String { get }
}

Here’s an example of an NSManagedObject that also conforms to ManagedDecodable:

class User: NSManagedObject, ManagedDecodable {
    static var entityName: String = "User"

    @NSManaged var id: Int
    @NSManaged var name: String

    enum Key: String, CodingKey {
        case id
        case name
    }

    func update(from decoder: Decoder) throws {
        let container = try decoder.container(keyedBy: Key.self)
        id = try container.decode(Int.self, forKey: .id)
        name = try container.decode(String.self, forKey: .name)
    }
}

Other than writing the decoding code in update(from:) instead of an init(from:) method, this again looks pretty similar to standard decoding.

We can now write a DecodingFactory that works for decoding any ManagedDecodable NSManagedObject:

struct ManagedDecodingFactory<Entity: NSManagedObject & ManagedDecodable>: DecodingFactory {
    let context: NSManagedObjectContext

    func create(from decoder: Decoder) throws -> Entity {
        var entity = NSEntityDescription.insertNewObject(forEntityName: Entity.entityName, into: context) as! Entity
        try entity.update(from: decoder)
        return entity
    }
}

Now creating an NSManagedObject from data is as simple as:

let factory = ManagedDecodingFactory<User>(context: myContext)
let user = try JSONDecoder().decode(using: factory, from: data)

Alternatives and Tradeoffs

DecodingFactory makes it possible to decode types that you would otherwise not be able to. A downside of this is that you are left writing boilerplate Decodable code.

An alternative option would be to write a Decodable wrapper object that can then create the type you wanted, so in the case of CLLocation:

class LocationWrapper: Decodable {
    let latitude: Double
    let longitude: Double
    lazy var value: CLLocation = {
       return CLLocation(latitude: latitude, longitude: longitude)
    }()
}

Using a wrapper you don’t need to make another protocol or write boilerplate decoding code. However, working with these wrapper objects makes it harder to use the object after it is decoded.

For decoding a top-level object, you still need to pass in a class different than the one you actually want, and then also unwrapping it:

let location = try JSONDecoder().decode(LocationWrapper.self, from: data).value

As a nested object, in order to avoid boilerplate code, you have to keep the wrapper around indefinitely and unwrap it whenever you want the value:

struct Place: Decodable {
    let name: String
    let location: LocationWrapper
}

let place = try JSONDecoder().decode(Place.self, from: data)
doSomethingWithALocation(place.location.value)

Also, unlike DecodingFactory there is no way of passing additional information for the decoding, making it significantly harder to decode something like an NSManagedObject.

As a side benefit, DecodingFactory isn’t restricted to being used with classes. While it is possible to make a struct you didn’t write conform to Decodable, that may not always be a good idea.
You can run into collision issues where multiple extensions try to make a struct conform to Decodable using different keys.

This is not an issue with DecodingFactory as you can have multiple ones for the same model type and be explicit about when you want to use which factory. You can even have it call different initializers:

struct AnotherCLLocationFactory: DecodingFactory {
    enum Key: String, CodingKey {
        case latitude
        case longitude
        case altitude
        case horizontalAccuracy
        case verticalAccuracy
        case timestamp
    }

    func create(from decoder: Decoder) throws -> CLLocation {
        let container = try decoder.container(keyedBy: Key.self)
        let latitude = try container.decode(Double.self, forKey: .latitude)
        let longitude = try container.decode(Double.self, forKey: .longitude)
        let altitude = try container.decode(Double.self, forKey: .altitude)
        let horizontalAccuracy = try container.decode(Double.self, forKey: .horizontalAccuracy)
        let verticalAccuracy = try container.decode(Double.self, forKey: .verticalAccuracy)
        let timestamp = try container.decode(Date.self, forKey: .timestamp)

        let coordinate = CLLocationCoordinate2D(latitude: latitude, longitude: longitude)

        return CLLocation(
            coordinate: coordinate,
            altitude: altitude,
            horizontalAccuracy: horizontalAccuracy,
            verticalAccuracy: verticalAccuracy,
            timestamp: timestamp
        )
    }
}

let locationA = try JSONDecoder().decode(using: CLLocationFactory(), from: data)
let locationB = try JSONDecoder().decode(using: AnotherCLLocationFactory(), from: data)

conclusion

I believe having to write a little extra boilerplate code is preferable to having model objects that are awkward to work with, or not being able to be decoded at all.

Also, if this boilerplate code is overwhelming, you always have the option to do your own code gen on your model objects, instead of relying on Xcode.

Say hello to your new mobile product team.

  • * By filling out this form, I accept stable|kernel’s Privacy Policy.
  • This field is for validation purposes and should be left unchanged.


0 replies

Leave a Reply

Want to join the discussion?
Feel free to contribute!

Leave a Reply

Your email address will not be published. Required fields are marked *