Passing Context Into Swift Codable Using UserInfo
The Problem
I recently encountered a problem where I needed to provide a Codable
type with more context than what was available in the JSON payload. This led me to discover the userInfo
dictionary on the JSONDecoder
type.
For my specific problem I needed to transform values from a server response based on a theme that is only known at runtime. While I could have performed this transformation in a view model or during layout, I wanted to throw a decoding error if the mapping failed. That meant it had to happen earlier, during decoding.
Why Use JSONDecoder UserInfo?
As defined by Apple the userInfo
object is "a dictionary you use to customize the decoding process by providing contextual information". So this sounds like it could be perfect for my use case, allowing me to pass in some data to the JSONDecoder
to assist with decoding some of the properties. As it is a Dictionary type its also simple to work with and immediately understandable.
It's defined as:
@preconcurrency
var userInfo: [CodingUserInfoKey : any Sendable] { get set }
So as long as the value we want to pass into userInfo
is Sendable
, this solution should work.
This also works with JSONEncoder
, so you can use userInfo
when serialising types as well.
Basic Decodable Example
To demonstrate how we can use it, we'll use a simplified example: passing an extra property into a type during decoding, where the property value isn't included in the JSON payload itself. Below we will pass a parentID
into our Child decodable type.
Here's the type along with some mock JSON for use in a Playground:
import SwiftUI
struct Child: Decodable {
let name: String
let age: Int
// let parentID: String // not in JSON payload
}
let mockJSONString = """
{
"name": "Lucy",
"age": 4
}
"""
do {
let decoder = JSONDecoder()
let data = try decoder.decode(Child.self, from: mockJSONString.data(using: .utf8)!)
print(data)
} catch {
print(error.localizedDescription)
}
Running this will print:
"Child(name: "Lucy", age: 4)"
Now we want to pass in the parentID
, which we already know at the time of decoding; perhaps from another response or in-memory value.
Setting UserInfo
We can add this value to the decoder’s userInfo
property. We can really add any type to the dictionary as long as it is Sendable but for this example we are just adding a String.
First we make a key to store our value in the dictionary:
extension CodingUserInfoKey {
static let parentIDKey = CodingUserInfoKey(rawValue: "parentIDKey")
}
We then use this key to insert a value into the decoder’s userInfo
:
note: I have made an error type
ParentDecodingError
to use here, as I wish to throw an error if there is an issue decoding the parentID key. This is more for neatness though and to avoid force unwrapping, as I cannot see a reason why this would fail and there are discussions on Swift forums about it too.
enum ParentDecodingError: Error {
case noParentContextKey
}
do {
guard let parentIDKey = CodingUserInfoKey.parentIDKey else { throw ParentDecodingError.noParentContextKey }
let decoder = JSONDecoder()
decoder.userInfo[parentIDKey] = "abcdefg"
let data = try decoder.decode(Child.self, from: mockJSONString.data(using: .utf8)!)
print(data)
} catch {
print(error.localizedDescription)
}
Reading UserInfo
To read the value during decoding and assign it to parentID
, we need to implement a custom init(from decoder:)
and extract the value from decoder.userInfo
.
struct Child: Decodable {
let name: String
let age: Int
let parentID: String // not in JSON payload
enum CodingKeys: CodingKey {
case name
case age
}
init(from decoder: any Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
guard let parentIDKey = CodingUserInfoKey.parentIDKey,
let parentID = decoder.userInfo[parentIDKey] as? String else {
throw ParentDecodingError.noParentContextKey
}
self.parentID = parentID
self.name = try container.decode(String.self, forKey: .name)
self.age = try container.decode(Int.self, forKey: .age)
}
}
As you can see this isn't the cleanest thing to do as we need to check for the type's presence in the dictionary, as well as casting it to the correct type, so I would use this very sparingly, as realistically all of this is extra tests you need to write.
Complete Example
So to bring it all together:
import SwiftUI
extension CodingUserInfoKey {
static let parentIDKey = CodingUserInfoKey(rawValue: "parentIDKey")
}
struct Child: Decodable {
let name: String
let age: Int
let parentID: String // not in JSON payload
enum CodingKeys: CodingKey {
case name
case age
}
init(from decoder: any Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
guard let parentIDKey = CodingUserInfoKey.parentIDKey,
let parentID = decoder.userInfo[parentIDKey] as? String else {
throw ParentDecodingError.noParentContextKey
}
self.parentID = parentID
self.name = try container.decode(String.self, forKey: .name)
self.age = try container.decode(Int.self, forKey: .age)
}
}
enum ParentDecodingError: Error {
case noParentContextKey
}
let mockJSONString = """
{
"name": "Lucy",
"age": 4
}
"""
do {
guard let parentIDKey = CodingUserInfoKey.parentIDKey else { throw ParentDecodingError.noParentContextKey }
let decoder = JSONDecoder()
decoder.userInfo[parentIDKey] = "abcdefg"
let data = try decoder.decode(Child.self, from: mockJSONString.data(using: .utf8)!)
print(data)
} catch {
print(error.localizedDescription)
}
Running this in the Playground will print:
"Child(name: "Lucy", age: 4, parentID: "abcdefg")"
Final Thoughts
This is a useful feature of JSONDecoder
and can help in tricky situations. That said, I consider it a last resort. It makes your Codable
implementation messier by requiring custom decoding and introduces external dependencies that can reduce predictability.
Thanks for reading.