Using Swift for iOS 13+, I need to decode a JSON response. It contains a nested object which needs a value (type) of the parent object for it decoding.
The JSON structure:
{
"name":"My email",
"type":"Email",
"content":{
"issued":"2023-08-25T12:58:39Z",
"attributes":{
"email":"[email protected]"
}
}
}
OR
{
"name":"My telephone",
"type":"Telephone",
"content":{
"issued":"2023-08-25T12:58:39Z",
"attributes":{
"telephone":"+33123456789"
}
}
}
attributes content depends of type. So content nested object needs to know type to be able to decode attributes.
The structures:
struct Response: Decodable {
let name: String
let type: String
let content: ContentResponse?
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
name = try container.decode(String.self, forKey: .name)
type = try container.decode(String.self, forKey: .type)
// !!! Here I need to pass the "type" value to content decoding
content = try container.decodeIfPresent(ContentResponse.self, forKey: .content)
}
}
struct ContentResponse: Decodable {
let issued: Date
let attributes: AttributesResponse?
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
issued = try container.decode(Date.self, forKey: .issued)
if container.contains(.attributes) {
// !!! Here I can't access the "type" value from parent object
switch Type.fromString(type: type) {
case .telephone:
attributes = try container.decode(AttributesTelephoneResponse.self, forKey: .attributes)
case .email:
attributes = try container.decode(AttributesEmailResponse.self, forKey: .attributes)
default:
// Unsupported type
throw DecodingError.dataCorruptedError(forKey: .attributes, in: contentContainer, debugDescription: "Type \"\(type)\" not supported for attributes")
}
} else {
attributes = nil
}
}
}
class DocumentResponse: Decodable {}
class AttributesEmailResponse: DocumentResponse {
let email: String
}
class AttributesTelephoneResponse: DocumentResponse {
let telephone: String
}
As you can see, init(from decoder: Decoder) of ContentResponse needs to know the type to be able to know which class to use for attributes decoding.
How can I pass the type decoded on Response to the nested object ContentResponse for it decoding?
NOT WORKING #1
I found maybe a solution here https://www.andyibanez.com/posts/the-mysterious-codablewithconfiguration-protocol/ using CodableWithConfiguration with decodeIfPresent(_:forKey:configuration:), but it targets iOS 15+ while I target iOS 13+.
NOT WORKING #2
I could have used userInfo of decoder in init(from decoder: Decoder) to pass type, but it's read-only:
var userInfo: [CodingUserInfoKey : Any] { get }
SOLUTION
Thanks to @Joakim Danielson for the answer.
The solution is to do the whole decoding in Response:
struct Response: Decodable {
let name: String
let type: String
let content: ContentResponse?
enum CodingKeys: String, CodingKey {
case name
case type
case content
}
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
name = try container.decode(String.self, forKey: .name)
type = try container.decode(String.self, forKey: .type)
// Content
if container.contains(.content) {
let issued = try contentContainer.decode(Date.self, forKey: .issued)
let attributes: DocumentResponse?
switch DocumentType.fromString(type: type) {
case .telephone:
let value = try contentContainer.decode(DocumentTelephoneResponse.self, forKey: .attributes)
attributes = .telephone(value)
case .email:
let value = try contentContainer.decode(DocumentEmailResponse.self, forKey: .attributes)
attributes = .email(value)
default:
throw DecodingError.dataCorruptedError(forKey: .attributes, in: contentContainer, debugDescription: "Type \"\(type)\" not supported for attributes")
}
content = ContentResponse(issued: issued, expires: expires, attributes: attributes, controls: controls)
} else {
content = nil
}
}
}
with:
public enum DocumentType: String, Codable {
case telephone = "Telephone"
case email = "Email"
static public func fromString(type: String) -> Type? {
Type(rawValue: type) ?? nil
}
}
struct ContentResponse: Decodable {
let issued: Date
let attributes: DocumentResponse?
enum CodingKeys: String, CodingKey {
case issued
case attributes
}
}
enum DocumentResponse: Decodable {
case email(DocumentEmailResponse)
case telephone(DocumentTelephoneResponse)
}
I would use enums for this, one for the type of content and one to hold the decoded content.
First the enum for the
typeAnd then the one for
contentI changed the types used in
Contentto be structuresThen all custom decoding takes part in
Responsewhere theContentvalues is decoded using a nested container.Both the enumerations contains a
nonecase but depending on what might be optional or not and personal preferences you might want to remove them if possible, I wasn't completely sure which and when values can be nil.As noted by @Rob in the comments it might be good to handle unknown types without failing the decoding process. Below is an alternative way to do this and also see the link in the comments for another alternative.
The new custom init for decoding is
This requires a change to the
Contentenum