Customizing Codable with Property Wrappers

Conforming to the Codable protocol is the standard way in Swift to decode external formats of data into native Swift model types. If your JSON maps one-to-one with your model types, you hardly need to write any extra code. If, however, even if just one of your properties needs to deviate from the norm, you may end up writing a lot of boilerplate code. Today, we will learn how to use Property Wrappers for one type of customization that will reduce this verbosity. Before we dive into that, consider reading my earlier articles on Codable and Property Wrappers to get warmed up.

The problem

We know that when we adopt Decodable for this Person struct

struct Person: Decodable {
  let name: String
  let address: String
  let weight: Int
}

the compiler synthesized code approximates to

init(from decoder: Decoder) throws {
  let container = try decoder.container(keyedBy: CodingKeys.self)
  self.name = try container.decode(String.self, forKey: .name)
  self.address = try container.decode(String.self, forKey: .address)
  self.weight = try container.decode(Int.self, forKey: .weight)
}

Now, let’s assume the service that sends you the JSON can either send a string or an integer for weight. It sounds like something that should not happen in real life, but our goal here is to understand why and how we can use a property wrapper to improve our lives when decoding properties, and this example suits the purpose just fine. If we have to customize our decoding of this one property, we will have to rewrite the entire init(from:) initializer.

init(from decoder: Decoder) throws {
  let container = try decoder.container(keyedBy: CodingKeys.self)
  self.name = try container.decode(String.self, forKey: .name)
  self.address = try container.decode(String.self, forKey: .address)
  if let stringWeight = try? container.decode(String.self, forKey: .weight) {
    self.weight = Int(stringWeight) ?? 0
  } else {
    self.weight = try container.decode(Int.self, forKey: .weight)
  }
}

For customizing just one property, we had to “rewrite” the code for every property. Imagine your model type having many more properties, or having to do such a customization for many model types. It gets very boring very fast. 

The solution

As you may recall, a property wrapper is used to provide the implementation for a property. The wrapped property turns into a computed property and the wrapper itself turns into a stored instance property with an _ prefix. Adopting Decodable, understandably, will generate an initializer that tries to decode the JSON into all the stored properties, one of which has the type of the property wrapper.

struct Person: Decodable {
  let name: String
  let address: String
  @StringOrInt var weight: Int
}

will generate an initializer that looks like

init(from decoder: Decoder) throws {
  let container = try decoder.container(keyedBy: CodingKeys.self)
  self.name = try container.decode(String.self, forKey: .name)
  self.address = try container.decode(String.self, forKey: .address)
  self._weight = try container.decode(StringOrInt.self, forKey: .weight)
}

There are 3 interesting things happening in the last line (the line before the }, actually)

  1. _weight is the property being initialized, not weight (because there is no more a stored property with that name)
  2. It is trying to decode a StringOrInt type, the type of the property wrapper.
  3. It is looking for the value under the key .weight not ._weight. So, in spite of the property being called _weight, the JSON only needs to contain the weight field, which is exactly what we want after all! We hardly want our json to match a swift property wrapper implementation detail. This important point is mentioned here in property wrapper evolution proposal.

The above code doesn’t compile, of course, because there is no StringOrInt type yet.

Recall (or just believe me) that container.decode(StringOrInt.self, forKey: .weight) will eventually end up calling the init(from:) initializer belonging to StringOrInt. Armed with this information, we can move all our customization code for that property over there. Let’s go ahead and implement StringOrInt to solve the original problem.

@propertyWrapper
struct StringOrInt {
  let wrappedValue: Int
}

extension StringOrInt: Decodable {
  init(from decoder: Decoder) throws {
     let container = try decoder.singleValueContainer()
     if let stringValue = try? container.decode(String.self) {
       self.wrappedValue = Int(stringValue) ?? 0
     } else {
       self.wrappedValue = try container.decode(Int.self)
     }
  }
}

With this in place, we are equipped to apply this particular customization to any Integer property across our code base.

Conclusion

We learned how to customize the decoding behavior of a single property using a property wrapper, in a manner that removes nearly all boilerplate. The equivalent Encodable customization is left as an exercise for the interested reader.

4 Comments

  1. Rust42

    That actually is really nice use of property wrapper. I was writing similar code in the past and was thinking of some ways to make it more reusable. I had turned to create a Struct and single value container. This looks very good. Thanks Mayur !

Leave a Reply