📜 ⬆️ ⬇️

Codable Tips and Examples

I would like to share with you some tips and tricks that I used in this example.

Download Swift Playground with all the code from this article:

image
')
Codable is introduced in Swift 4 in order to replace the old NSCoding API. Unlike NSCoding , Codable has first-class JSON support, which makes it a promising option for using the JSON API.

Codable is great as NSCoding . If there is a need to encode or decode some local data that you must fully control, you can take advantage of automatic encoding and decoding .

In the real world, everything becomes quite complicated and fast. Attempting to build a fault-tolerant system that will manage JSON and model all product requirements is a problem.

One of the major drawbacks of Codable is that as soon as you need custom decoding logic — even for one key — you must provide everything: manually determine all encoding keys and fully implement init (from decoder: Decoder) throws . This is not perfect. But this is at least as good (or bad) as using third-party JSON libraries in Swift. The presence of such in the library is already a victory.

Therefore, if you want to start using Codable in your application (and you are already familiar with all of its main features), here are some tips and examples that may be useful:


Secure array decoding


Suppose you want to load and display a collection of posts (Posts) in your application. Each Post has id (required), title (required) and subtitle (optional).

final class Post: Decodable { let id: Id<Post> // More about this type later. let title: String let subtitle: String? } 

Post class models requirements well. It already accepts the Decodable protocol, so we are ready to decode some data:

 [ { "id": "pos_1", "title": "Codable: Tips and Tricks" }, { "id": "pos_2" } ] 

 do { let posts = try JSONDecoder().decode([Post].self, from: json.data(using: .utf8)!) } catch { print(error) //prints "No value associated with key title (\"title\")." } 

As expected, we got the error .keyNotFound , because the second post object has no title.

When the data does not match the expected format (for example, it may be the result of a misunderstanding, regression, or unexpected user input), the system should automatically report an error to give developers the opportunity to correct it. Swift provides a detailed error report in the form of a DecodingError at any time when decoding is performed with an error, which is extremely useful.

In most cases, no one wants one damaged post to prevent you from displaying a whole page of other completely correct posts. To avoid this, I use a special type of Safe <T>, which allows me to safely decode the object. If it detects an error during decoding, it will go into safe error detection mode and send a report:

 public struct Safe<Base: Decodable>: Decodable { public let value: Base? public init(from decoder: Decoder) throws { do { let container = try decoder.singleValueContainer() self.value = try container.decode(Base.self) } catch { assertionFailure("ERROR: \(error)") // TODO: automatically send a report about a corrupted data self.value = nil } } } 

Now, when I decode an array, I can indicate that I do not want to stop decoding in the case of a single corrupted element:

 do { let posts = try JSONDecoder().decode([Safe<Post>].self, from: json.data(using: .utf8)!) print(posts[0].value!.title) // prints "Codable: Tips and Tricks" print(posts[1].value) // prints "nil" } catch { print(error) } 

Keep in mind that decode decoding ([Safe <Post>]. Self, from: ... causes an error if the data does not contain an array. In general, such errors should be captured at a higher level. The general API contract is to always return an empty array if there are no items to return.

ID type and single value container


In the previous example, I used the special identifier Id <Post>. The type Id gets the parameters using the generic parameter Entity, which is not actually used by the Id itself, but is used by the compiler when comparing different types of Id. Thus, the compiler ensures that I cannot randomly transfer Id <Media> , where Id <Image> is expected.

I also used phantom types for security when using types, in the API client article in Swift .

The type Id itself is very simple; it is just a shell on top of the original String :

 public struct Id<Entity>: Hashable { public let raw: String public init(_ raw: String) { self.raw = raw } public var hashValue: Int { return raw.hashValue } public static func ==(lhs: Id, rhs: Id) -> Bool { return lhs.raw == rhs.raw } } 

Adding Codable to it is definitely a challenging task. This requires the special type SingleValueEncodingContainer :

A container that can support the storage and direct encoding of a single non-locked value.

 extension Id: Codable { public init(from decoder: Decoder) throws { let container = try decoder.singleValueContainer() let raw = try container.decode(String.self) if raw.isEmpty { throw DecodingError.dataCorruptedError( in: container, debugDescription: "Cannot initialize Id from an empty string" ) } self.init(raw) } public func encode(to encoder: Encoder) throws { var container = encoder.singleValueContainer() try container.encode(raw) } } 

As can be seen from the code above, Id also has a special rule that prevents its initialization from an empty String.

Secure decoding of Transfers


Swift has excellent support for decoding (and encoding) enums. In many cases, all you have to do is simply declare a Decodable match, which is automatically synthesized by the compiler (the raw enumeration type must be either String or Int).

Suppose you create a system that displays all your devices on a map. The device has a location (required) and a system (required), which it launches.

 enum System: String, Decodable { case ios, macos, tvos, watchos } struct Location: Decodable { let latitude: Double let longitude: Double } final class Device: Decodable { let location: Location let system: System } 

Now there is a question. What if more systems are added in the future? The product solution may be to still display these devices, but in some way indicate that the system is “unknown.” How should you model this in an application?

By default, Swift will cause a .dataCorrupted error if an unknown enum value is encountered:

 { "location": { "latitude": 37.3317, "longitude": 122.0302 }, "system": "caros" } 

 do { let device = try JSONDecoder().decode(Device.self, from: json.data(using: .utf8)!) } catch { print(error) // Prints "Cannot initialize System from invalid String value caros" } 

How can the system be modeled and decoded in a secure way? One way is to make the system property optional, which will mean “Unknown”. And the easiest way to safely decode a system is to implement a custom init (from decoder: Decoder) throws initializer:

 final class Device: Decodable { let location: Location let system: System? init(from decoder: Decoder) throws { let map = try decoder.container(keyedBy: CodingKeys.self) self.location = try map.decode(Location.self, forKey: .location) self.system = try? map.decode(System.self, forKey: .system) } private enum CodingKeys: CodingKey { case location case system } } 

Note that this version simply ignores all possible problems with the value system. This means that even “damaged” data (for example, a missing key system, the number 123, null, an empty object {} - depending on which API contract) is decoded as a nil value (“Unknown”). A more accurate way to say "decode unknown strings as nil":

 self.system = System(rawValue: try map.decode(String.self, forKey: .system)) 

Laconic manual decoding


In the previous example, we had to implement the user init init (from decoder: Decoder) throws, which turned out to be quite a lot of code. Fortunately, there are several ways to do this more succinctly.

Getting rid of certain types of parameters


One option is to get rid of explicit type parameters:

 extension KeyedDecodingContainer { public func decode<T: Decodable>(_ key: Key, as type: T.Type = T.self) throws -> T { return try self.decode(T.self, forKey: key) } public func decodeIfPresent<T: Decodable>(_ key: KeyedDecodingContainer.Key) throws -> T? { return try decodeIfPresent(T.self, forKey: key) } } 

Let's go back to our Post example and extend it using the webURL (optional) property. If we try to decode the data published below, we will get a .dataCorrupted error along with the main error:

 struct PatchParameters: Swift.Encodable { let name: Parameter<String>? } func encoded(_ params: PatchParameters) -> String { let data = try! JSONEncoder().encode(params) return String(data: data, encoding: .utf8)! } encoded(PatchParameters(name: nil)) // prints "{}" encoded(PatchParameters(name: .null)) //print "{"name":null}" encoded(PatchParameters(name: .value("Alex"))) //print "{"name":"Alex"}" 

Source: https://habr.com/ru/post/358748/


All Articles