Search code examples
swiftcodable

KeyedDecodingContainerProtocol.superDecoder(forKey:) doesn't work as documentation says


I have the following JSON returned by my backend:

[
   {
      "id":8,
      "title":"Title 1",
      "componentCategoryIcon":{
         "id":34,
         "name":"icon_name_1",
         "type":"IMAGE"
      }
   },
   {
      "id":8,
      "title":"Title 2",
      "componentCategoryIcon":{
         "id":35,
         "name":"icon_name_2",
         "type":"IMAGE"
      }
   },
   {
      "id":8,
      "title":"Title 3",
      "componentCategoryIcon": null
   }
]

And the following Codable structs:

struct ComponentCategory: Codable {
    let id: Int
    let title: String
    let componentCategoryIcon: Image?
    
    init(from decoder: Decoder) throws {
        let container = try decoder.container(keyedBy: CodingKeys.self)
        let id = try container.decode(Int.self, forKey: .id)
        
        self.id = id
        self.title = try container.decode(String.self, forKey: .title)
        
        let savedComponentCategory = (decoder.userInfo[.savedComponentCategories] as? [ComponentCategory])?.first(where: { $0.id == id })
        

        // (1)
        if let nestedDecoder = try? container.superDecoder(forKey: .componentCategoryIcon) {
            self.componentCategoryIcon = try Image(from: nestedDecoder, savedImage: savedComponentCategory?.componentCategoryIcon)
        } else {
            self.componentCategoryIcon = nil
        }
    }
}
struct Image: Codable {
    let id: Int
    let name: String
    let type: String?
    let path: String?
    
    let downloaded: Bool
    
    required init(from decoder: Decoder, savedImage: Image?) throws {
        let container = try decoder.container(keyedBy: CodingKeys.self)
        self.id = try container.decode(Int.self, forKey: .id)
        self.name = try container.decode(String.self, forKey: .name)
        self.type = try container.decodeIfPresent(String.self, forKey: .type)
        self.path = try container.decodeIfPresent(String.self, forKey: .path)
        self.downloaded = try container.decodeIfPresent(Bool.self, forKey: .downloaded) ?? savedImage?.downloaded ?? false
    }

Now I noted a behaviour different from documentation for superDecoder(forKey:) func. Documentation says:

Throws

DecodingError.valueNotFound if self has a null entry for the given key.

During third ComponentCategory decoding (componentCategoryIcon is null), I expected to go to else branch of the (1) if-else. Instead I obtain an apparently valid nestedContainer and I have an exception in the Image init.

Am I misunderstanding superDecoder(forKey:) documentation? Or is it wrong?


Solution

  • The current implementation indeed does not throw an error if there is no value for the given key, and instead returns an empty container; the documentation does not match this implementation. It appears that the upcoming swift-foundation package also maintains this behavior, so the documentation stands to get updated.

    This is likely worth filing an issue for (whether for a bug fix, or a documentation fix).

    In the meantime, you can work around this by checking for the explicit existence of the key you need, and use that as the condition for your if-statement.


    This being said, like the comments above mention, it seems surprising you would try to use a superDecoder for this. You may want to either add information to your question about why you need to do this, or open a new question asking for suggestions on what approach you might want to take instead.