For all of its strengths and overall convenience, one downside of Swift’s built-in Codable
API is that it doesn’t really offer any standard way to change or otherwise customize how a given type should be encoded or decoded.
While we can always write completely custom coding implementations for the types that we’ve defined ourselves, when working with external types (such as those that ship as part of the standard library), it’s possible to end up with a mismatch between how a given type is expected to be coded and the data format that an app is using.
As an example, let’s say that an app that we’re working on contains the following User
type, which has a timeZone
property that uses Foundation’s built-in TimeZone
type:
struct User: Identifiable, Codable {
let id: UUID
var name: String
var timeZone: TimeZone
}
Now let’s say that we’d like to encode and decode instances of the above User
type to and from JSON data that has the following format:
{
"id": "10CAAD2C-0942-4353-94AE-0319216296CB",
"name": "John",
"timeZone": "Europe/Warsaw"
}
So, what’s the problem? Although TimeZone
does already conform to Codable
out of the box, the way that implementation was written assumes that each such value will always be represented by a dictionary when encoded — and our JSON data instead uses a plain string, which gives us a mismatch.
One potential solution to this problem would of course be to change our JSON data to match the format that TimeZone
is expecting (by nesting each time zone identifier within a dictionary), but that’s not always possible. Our app might not be the only client that consumes the above data, or we might be requesting our JSON from a third-party web API that’s beyond our direct control.
If we instead focus on client-side solutions, one thing that we could do is to wrap the built-in TimeZone
type into a custom one that’ll essentially act as a RawRepresentable
wrapper — like this:
extension User {
struct TimeZoneWrapper: RawRepresentable {
var rawValue: TimeZone
}
}
RawRepresentable
is a simple, but powerful, built-in protocol that all raw value-backed enums implicitly conform to.
Since we’re now dealing with a type that’s under our own control, we can completely customize how we’d like each value to be encoded and decoded. So, in this case, we can start by decoding each time zone identifier as a String
, and then use that to initialize an instance of our underlying TimeZone
raw value. Then, when encoding, we can simply encode our time zone’s identifier as-is:
extension User.TimeZoneWrapper: Codable {
init(from decoder: Decoder) throws {
let container = try decoder.singleValueContainer()
let identifier = try container.decode(String.self)
guard let timeZone = TimeZone(identifier: identifier) else {
throw DecodingError.dataCorruptedError(
in: container,
debugDescription: "Unknown time zone '\(identifier)'"
)
}
rawValue = timeZone
}
func encode(to encoder: Encoder) throws {
var container = encoder.singleValueContainer()
try container.encode(rawValue.identifier)
}
}
If we now update our User
type from before to use the above TimeZoneWrapper
for its timeZone
property, then we’ve solved our problem. However, our current solution does come with a cost, in that we now always have to access timeZone.rawValue
whenever we want to use the actual TimeZone
instance that we’re wrapping.
Now, if we only wanted to use those TimeZone
values to access certain properties, then we could solve that using dynamic member lookup, since that would enable us to reference any TimeZone
property directly on instances of our TimeZoneWrapper
.
However, in this case, we’d likely want to pass our TimeZone
values themselves to various date-related system APIs, such as DateFormatter
— so let’s see if we can come up with a somewhat more transparent solution.
These days, whenever the phrase “wrapper type” comes up, I like to question whether such a type would perhaps be better implemented as an actual property wrapper instead. After all, wrapping values is exactly what that language feature is for, so let’s see if it could help us solve our problem in this case.
The good news is that converting our TimeZoneWrapper
type into a property wrapper just requires us to annotate it with the @propertyWrapper
attribute, and to give it a wrappedValue
property. In this case, though, let’s also give it a more descriptive name — StringCodedTimeZone
— to better signal what this type is actually for. But our Codable
implementation can remain exactly the same (apart from replacing rawValue
with wrappedValue
):
@propertyWrapper
struct StringCodedTimeZone {
var wrappedValue: TimeZone
}
extension StringCodedTimeZone: Codable {
init(from decoder: Decoder) throws {
let container = try decoder.singleValueContainer()
let identifier = try container.decode(String.self)
guard let timeZone = TimeZone(identifier: identifier) else {
throw DecodingError.dataCorruptedError(
in: container,
debugDescription: "Unknown time zone '\(identifier)'"
)
}
wrappedValue = timeZone
}
func encode(to encoder: Encoder) throws {
var container = encoder.singleValueContainer()
try container.encode(wrappedValue.identifier)
}
}
With the above change in place, we can now let our timeZone
property remain a proper TimeZone
instance — all that we have to do is to annotate that property with our new wrapper’s attribute, and we’ll once again be able to encode and decode our JSON data without having to change its format in any way:
struct User: Identifiable, Codable {
let id: UUID
var name: String
@StringCodedTimeZone var timeZone: TimeZone
}
Pretty neat! If the above property wrapper-based solution looks familiar, it might be because I’ve used in several other articles as well — such as “Ignoring invalid JSON elements when using Codable”, and “Annotating properties with default decoding values”. Property wrappers are, without a doubt, one of my go-to solutions when it comes to Codable customization. While they do require a bit of boilerplate, the fact that they let us customize these kinds of behaviors without having to change the actual types of our properties is incredibly powerful.
Now, if we only needed to solve the above kind of problem once within our entire code base, then we can stop right here. In general, there’s no need to invent new abstractions to solve one-off problems, but let’s say that we instead wanted to come up with a more general-purpose solution that we could use in multiple places across our code base.
Currently, if we were to write multiple instances of our Codable
customization solution, we’d end up with a fair amount of boilerplate, since we’d need to write that same (quite verbose!) encoding and decoding code from scratch each time. So let’s address that by introducing a protocol that’ll let us express that a type is codable by transform:
protocol CodableByTransform: Codable {
associatedtype CodingValue: Codable
static func transformDecodedValue(_ value: CodingValue) throws -> Self?
static func transformValueForEncoding(_ value: Self) throws -> CodingValue
}
Fun fact: The above protocol can sort of be seen as a “spiritual successor” to the UnboxableByTransform
protocol that Unbox, my pre-Codable
JSON decoder, shipped with.
Note how we’re making our new protocol extend Codable
itself. That’s a technique that’s often referred to as “protocol specialization”, which essentially lets us create a more specific, tailored version of any protocol by inheriting all of its requirements, extensions, and capabilities.
What’s really cool about that pattern is that it then lets us write default implementations of our base protocol’s requirements. In this case, that’ll let us use our new transformation methods to automatically provide any conforming type with a default Codable
implementation — like this:
extension CodableByTransform {
init(from decoder: Decoder) throws {
let container = try decoder.singleValueContainer()
let decoded = try container.decode(CodingValue.self)
guard let value = try Self.transformDecodedValue(decoded) else {
throw DecodingError.dataCorruptedError(
in: container,
debugDescription: """
Decoding transformation failed for '\(decoded)'
"""
)
}
self = value
}
func encode(to encoder: Encoder) throws {
let encodable = try Self.transformValueForEncoding(self)
var container = encoder.singleValueContainer()
try container.encode(encodable)
}
}
With the above in place, we can now go back to our StringCodedTimeZone
property wrapper and make it much simpler. We no longer have to write any specific Codable
implementation for it, or any other wrappers like it, and can instead focus on performing the actual transformations to and from its coding representation:
@propertyWrapper
struct StringCodedTimeZone: CodableByTransform {
static func transformDecodedValue(_ value: String) throws -> Self? {
TimeZone(identifier: value).map(Self.init)
}
static func transformValueForEncoding(_ value: Self) throws -> String {
value.wrappedValue.identifier
}
var wrappedValue: TimeZone
}
We now have a general-purpose abstraction that makes it easy to implement any kind of Codable
transformations — either for our own types, or for external types that we’re looking to customize.
While I certainly wish that Codable
included more lightweight customization options out of the box, the fact that we can use property wrappers (and other language features) to essentially write small encoding/decoding plugins is really useful. Doing so might require a bit of setup code, but once that’s done, we should be able to tweak any type’s coding process however we’d like.
So, the next time you encounter an external type that doesn’t quite encode or the decode the way you’d like it to, I hope that one of the techniques from this article will come in handy.
Thanks for reading!