6

When using a decoder in a nested Codable struct, is there any way to access a property of a parent struct?

The only way I can think of that might work (haven't tested yet) is to use a manual decoder in the parent struct too, set the property in the userInfo dictionary, and then access userInfo in the child struct. But that would result in a lot of boilerplate code. I'm hoping there's a simpler solution.

struct Item: Decodable, Identifiable {
    let id: String
    let title: String
    let images: Images

    struct Images: Decodable {
        struct Image: Decodable, Identifiable {
            let id: String
            let width: Int
            let height: Int

            init(from decoder: Decoder) throws {
                let container = try decoder.container(keyedBy: CodingKeys.self)
                width = try container.decode(Int.self, forKey: .width)
                height = try container.decode(Int.self, forKey: .height)

                // How do I get `parent.parent.id` (`Item#id`) here?
                id = "\(parent.parent.id)\(width)\(height)"
            }
        }

        let original: Image
        let small: Image
        // …
    }
}

In the above example, the item ID coming from the server is only defined in the top-level properties in the JSON, but I need them in the children too, so I can also make them Identifiable.

Sindre Sorhus
  • 62,972
  • 39
  • 168
  • 232
  • @jawadAli It's not. – Sindre Sorhus Jun 07 '20 at 08:05
  • 2
    Your `userInfo` idea is almost certainly the right way to do this. The question is what kind of boilerplate it generates, and we can help eliminate that. (I've built systems like this in the past, and standard refactoring techniques apply to removing code duplication for it.) – Rob Napier Jun 09 '20 at 14:58
  • 6
    Multiple approaches discussed here by ItaiFerber, who was a lead engineer on Codable: https://forums.swift.org/t/codable-passing-data-to-child-decoder/12757/2 – New Dev Jun 10 '20 at 02:02

1 Answers1

1

I managed it using Itai Ferber's suggestion as mentioned by @New Dev in the following way:

  1. Create a new reference type whose only purpose is to contain a mutable value that can be passed between parent and child.
  2. Assign an instance of that type to the JSONDecoder's userInfo dictionary.
  3. Retrieve that instance when decoding the parent and assign to it the id that you're interested in passing.
  4. Whilst decoding the child, retrieve that id from the instance stored in the userInfo earlier.

I've modified your example above as follows:

struct Item: Decodable, Identifiable {

    enum CodingKeys: String, CodingKey {
        case id
        case title
        case images
    }

    let id: String
    let title: String
    let images: Images

    struct Images: Decodable {
        struct Image: Decodable, Identifiable {
            let id: String
            let width: Int
            let height: Int

            init(from decoder: Decoder) throws {
                let container = try decoder.container(keyedBy: CodingKeys.self)
                width = try container.decode(Int.self, forKey: .width)
                height = try container.decode(Int.self, forKey: .height)

                if let referenceTypeUsedOnlyToContainAChangeableIdentifier = decoder.userInfo[.referenceTypeUsedOnlyToContainAChangeableIdentifier] as? ReferenceTypeUsedOnlyToContainAChangeableIdentifier {
                    self.id = referenceTypeUsedOnlyToContainAChangeableIdentifier.changeableIdentifier
                } else {
                    self.id = "something went wrong"
                }
            }
        }

        let original: Image
        let small: Image
        // …

        init(from decoder: Decoder) throws {
            let container = try decoder.container(keyedBy: CodingKeys.self)
            id = try container.decode(String.self, forKey: .id)
            if let referenceTypeUsedOnlyToContainAChangeableIdentifier = decoder.userInfo[.referenceTypeUsedOnlyToContainAChangeableIdentifier] as? ReferenceTypeUsedOnlyToContainAChangeableIdentifier {
               referenceTypeUsedOnlyToContainAChangeableIdentifier.changeableIdentifier = id
            }
        }
    }
}

// Use this reference type to just store an id that's retrieved later.
class ReferenceTypeUsedOnlyToContainAChangeableIdentifier {
    var changeableIdentifier: String?
}

// Convenience extension.
extension CodingUserInfoKey {
    static let referenceTypeUsedOnlyToContainAChangeableIdentifier = CodingUserInfoKey(rawValue: "\(ReferenceTypeUsedOnlyToContainAChangeableIdentifier.self)")!
}

let decoder = JSONDecoder()
// Assign the reference type here to be used later during the decoding process first to assign the id in `Item` and then
// later to retrieve that value in `Images`
decoder.userInfo[.referenceTypeUsedOnlyToContainAChangeableIdentifier] = ReferenceTypeUsedOnlyToContainAChangeableIdentifier()
Aodh
  • 662
  • 1
  • 7
  • 24