Handling Empty Lists with Realm’s Property Wrappers

At work we recently updated from a pretty old version of Realm (10.8) to a pretty new version (10.20). Despite being just a point release, there have been some significant changes. I was very excited that the library is taking advantage of property wrappers to get rid of the cumbersome @objc dynamic decorators on every model property. We jumped in to converting all our model classes, and ran into one significant issue.

An interesting quirk about our REST API is that rather than return an empty JSON array when a model has an empty property (like { "things": [] }) it will sometimes omit the key entirely. This makes sense from the backend point of view, but it’s a challenge when we want to reduce optionality on the client side.

This wasn’t a problem for us with Realm’s old syntax, because due to some quirk of the library’s init(from decoder: Decoder) implementation, it would respect default property values rather than crashing. So by declaring our List properties like so:

var things = List<Thing>()

We ended up getting the right result either way: an empty list whether or not the API included the corresponding empty array in its response.

Unfortunately, this interesting quirk does not apply to Realm’s @Persisted properties, which go through a different decoding path. Instead, after much trial and error, we realized that we needed to override KeyedDecodingContainer‘s decode function for the specific case we care about (a persisted RealmCollection), like so:

public extension KeyedDecodingContainer {
 func decode<T>(_: T.Type, forKey key: Self.Key) throws -> T 
  where T: Persistable, T.Value: RealmCollection, T: Decodable {
 try decodeIfPresent(T.self, forKey: key) 
     ?? Persisted(wrappedValue: List<T.Value.Element>() as! T.Value) as! T
 }
}

public protocol Persistable {
 associatedtype Value: _Persistable
}

extension Persisted: Persistable {}

What’s going on here?

KeyedDecodingContainer is part of the Decodable implementation chain, responsible for holding and providing access to decoded values.

This decode function that we’re overriding is responsible for decoding values of type T, which we’re requiring to be a Persisted struct–that is, a model property that’s declared with Realm’s @Persisted property wrapper. (The Persistable/Persisted dance is just a weird requirement of the type system and can safely be ignored.) We’re further requiring T to have a Value type, that is, for the property wrapper to be wrapping, some kind of RealmCollection. All put together, that means we’re overriding the decode implementation only when the system attempts to decode something declared like @Persisted var things: List<Thing>.

Once the system has decided that’s what it wants to decode, our implementation kicks in. We call into decodeIfPresent in order to account for the case where the array is absent without throwing an error. And we use the nil coalescing operator ?? to provide our own value in that case. What value do we produce instead? Why, a Persisted struct wrapping an empty List, just the type we were trying to decode in the first place. We have to do some casting to make sure we produce exactly the type expected, but it ends up being the right thing.

This took some effort to figure out, but once done, it’s comforting to have the quirks of the decoding process under our control rather than depending on an undocumented feature of the library. Hopefully this blog post will help the next person figure it out a little more quickly.

About Joel Kin

Developing on Apple platforms for, holy shit, like twenty years now. Find me on linkedin and twitter. My personal website is joelk.in.
This entry was posted in Code and tagged , , . Bookmark the permalink.