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.
Tweet